0da74c021c40b290e7425056d4feba3a1842a263
[booh] / bin / booh
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
29
30 require 'gettext'
31 include GetText
32 bindtextdomain("booh")
33
34 require 'rexml/document'
35 include REXML
36
37 require 'booh/booh-lib'
38 include Booh
39 require 'booh/UndoHandler'
40
41
42 #- options
43 $options = [
44     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
45
46     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 ]
48
49 #- default values for some globals 
50 $xmldir = nil
51 $modified = false
52 $current_cursor = nil
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
56
57 def usage
58     puts _("Usage: %s [OPTION]...") % File.basename($0)
59     $options.each { |ary|
60         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
61     }
62 end
63
64 def handle_options
65     parser = GetoptLong.new
66     parser.set_options(*$options.collect { |ary| ary[0..2] })
67     begin
68         parser.each_option do |name, arg|
69             case name
70             when '--help'
71                 usage
72                 exit(0)
73
74             when '--verbose-level'
75                 $verbose_level = arg.to_i
76
77             end
78         end
79     rescue
80         puts $!
81         usage
82         exit(1)
83     end
84 end
85
86 def read_config
87     $config = {}
88     $config_file = File.expand_path('~/.booh-gui-rc')
89     if File.readable?($config_file)
90         $xmldoc = REXML::Document.new(File.new($config_file))
91         $xmldoc.root.elements.each { |element|
92             txt = element.get_text
93             if txt
94                 if txt.value =~ /~~~/ || element.name == 'last-opens'
95                     $config[element.name] = txt.value.split(/~~~/)
96                 else
97                     $config[element.name] = txt.value
98                 end
99             elsif element.elements.size == 0
100                 $config[element.name] = ''
101             else
102                 $config[element.name] = {}
103                 element.each { |chld|
104                     txt = chld.get_text
105                     $config[element.name][chld.name] = txt ? txt.value : nil
106                 }
107             end
108         }
109     end
110     $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111     $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
112     $config['comments-format'] ||= '%t'
113     if !FileTest.directory?(File.expand_path('~/.booh'))
114         system("mkdir ~/.booh")
115     end
116
117     $tempfiles = []
118     $todelete = []
119 end
120
121 def check_config
122     if !system("which convert >/dev/null 2>/dev/null")
123         show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
124 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
125         exit 1
126     end
127     if !system("which identify >/dev/null 2>/dev/null")
128         show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
129 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
130     end
131     missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
132     if missing != []
133         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
134     end
135
136     viewer_binary = $config['video-viewer'].split.first
137     if viewer_binary && !File.executable?(viewer_binary)
138         show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
139 You should fix this in Edit/Preferences so that you can view videos.
140
141 Problem was: '%s' is not an executable file.
142 Hint: don't forget to specify the full path to the executable,
143 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
144     end
145     browser_binary = $config['browser'].split.first
146     if browser_binary && !File.executable?(browser_binary)
147         show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
148 You should fix this in Edit/Preferences so that you can open URLs.
149
150 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
151     end
152 end
153
154 def write_config
155     if $config['last-opens'] && $config['last-opens'].size > 10
156         $config['last-opens'] = $config['last-opens'][-10, 10]
157     end
158
159     ios = File.open($config_file, "w")
160     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
161     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
162     $config.each_pair { |key, value|
163         elem = $xmldoc.root.add_element key
164         if value.is_a? Hash
165             $config[key].each_pair { |subkey, subvalue|
166                 subelem = elem.add_element subkey
167                 subelem.add_text subvalue.to_s
168             }
169         elsif value.is_a? Array
170             elem.add_text value.join('~~~')
171         else
172             if !value
173                 elem.remove
174             else
175                 elem.add_text value.to_s
176             end
177         end
178     }
179     $xmldoc.write(ios, 0)
180     ios.close
181
182     $tempfiles.each { |f|
183         system("rm -f #{f}")
184     }
185 end
186
187 def set_mousecursor(what, *widget)
188     cursor = what.nil? ? nil : Gdk::Cursor.new(what)
189     if widget[0] && widget[0].window
190         widget[0].window.cursor = cursor
191     end
192     if $main_window && $main_window.window
193         $main_window.window.cursor = cursor
194     end
195     $current_cursor = what
196 end
197 def set_mousecursor_wait(*widget)
198     gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
199     if Thread.current == Thread.main
200         Gtk.main_iteration while Gtk.events_pending?
201     end
202 end
203 def set_mousecursor_normal(*widget)
204     gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
205 end
206 def push_mousecursor_wait(*widget)
207     if $current_cursor != Gdk::Cursor::WATCH
208         $save_cursor = $current_cursor
209         gtk_thread_protect { set_mousecursor_wait(*widget) }
210     end
211 end
212 def pop_mousecursor(*widget)
213     gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
214 end
215
216 def current_dest_dir
217     source = $xmldoc.root.attributes['source']
218     dest = $xmldoc.root.attributes['destination']
219     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
220 end
221
222 def full_src_dir_to_rel(path, source)
223     return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
224 end
225
226 def build_full_dest_filename(filename)
227     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
228 end
229
230 def save_undo(name, closure, *params)
231     UndoHandler.save_undo(name, closure, [ *params ])
232     $undo_tb.sensitive = $undo_mb.sensitive = true
233     $redo_tb.sensitive = $redo_mb.sensitive = false
234 end
235
236 def view_element(filename, closures)
237     if entry2type(filename) == 'video'
238         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
239         msg 2, cmd
240         system(cmd)
241         return
242     end
243
244     w = Gtk::Window.new.set_title(filename)
245
246     msg 3, "filename: #{filename}"
247     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
248     #- typically this file won't exist in case of videos; try with the largest thumbnail around
249     if !File.exists?(dest_img)
250         if entry2type(filename) == 'video'
251             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
252             if not alternatives.empty?
253                 dest_img = alternatives[-1]
254             end
255         else
256             push_mousecursor_wait
257             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
258             pop_mousecursor
259             if !File.exists?(dest_img)
260                 msg 2, _("Could not generate fullscreen thumbnail!")
261                 return
262                 end
263         end
264     end
265     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)))
266     evt.signal_connect('button-press-event') { |this, event|
267         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
268             $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
269         end
270         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
271             menu = Gtk::Menu.new
272             menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
273             delete_item.signal_connect('activate') {
274                 w.destroy
275                 closures[:delete].call
276             }
277             menu.show_all
278             menu.popup(nil, nil, event.button, event.time)
279         end
280     }
281     evt.signal_connect('button-release-event') { |this, event|
282         if $gesture_press
283             if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
284                 msg 3, "gesture delete: click-drag right button to the bottom"
285                 w.destroy
286                 closures[:delete].call
287                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
288             end
289         end
290     }
291     tooltips = Gtk::Tooltips.new
292     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
293
294     w.signal_connect('key-press-event') { |w,event|
295         if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
296             w.destroy
297             closures[:delete].call
298         end
299     }
300
301     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
302     b.signal_connect('clicked') { w.destroy }
303
304     vb = Gtk::VBox.new
305     vb.pack_start(evt, false, false)
306     vb.pack_end(bottom, false, false)
307
308     w.add(vb)
309     w.signal_connect('delete-event') { w.destroy }
310     w.window_position = Gtk::Window::POS_CENTER
311     w.show_all
312 end
313
314 def scroll_upper(scrolledwindow, ypos_top)
315     newval = scrolledwindow.vadjustment.value -
316         ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
317     if newval < scrolledwindow.vadjustment.lower
318         newval = scrolledwindow.vadjustment.lower
319     end
320     scrolledwindow.vadjustment.value = newval
321 end
322
323 def scroll_lower(scrolledwindow, ypos_bottom)
324     newval = scrolledwindow.vadjustment.value +
325         ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
326     if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
327         newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
328     end
329     scrolledwindow.vadjustment.value = newval
330 end
331
332 def autoscroll_if_needed(scrolledwindow, image, textview)
333     #- autoscroll if cursor or image is not visible, if possible
334     if image && image.window || textview.window
335         ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
336         ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
337         current_miny_visible = scrolledwindow.vadjustment.value
338         current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
339         if ypos_top < current_miny_visible
340             scroll_upper(scrolledwindow, ypos_top)
341         elsif ypos_bottom > current_maxy_visible
342             scroll_lower(scrolledwindow, ypos_bottom)
343         end
344     end
345 end
346
347 def create_editzone(scrolledwindow, pagenum, image)
348     frame = Gtk::Frame.new
349     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
350     frame.set_shadow_type(Gtk::SHADOW_IN)
351     textview.signal_connect('key-press-event') { |w, event|
352         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
353         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
354             scrolledwindow.signal_emit('key-press-event', event)
355         end
356         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
357            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
358             if event.keyval == Gdk::Keyval::GDK_Up
359                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
360                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
361                 else
362                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
363                 end
364             else
365                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
366                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
367                 else
368                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
369                 end
370             end
371         end
372         false  #- propagate
373     }
374
375     candidate_undo_text = nil
376     textview.signal_connect('focus-in-event') { |w, event|
377         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378         candidate_undo_text = textview.buffer.text
379         false  #- propagate
380     }
381
382     textview.signal_connect('key-release-event') { |w, event|
383         if candidate_undo_text && candidate_undo_text != textview.buffer.text
384             $modified = true
385             save_undo(_("text edit"),
386                       proc { |text|
387                           save_text = textview.buffer.text
388                           textview.buffer.text = text
389                           textview.grab_focus
390                           $notebook.set_page(pagenum)
391                           proc {
392                               textview.buffer.text = save_text
393                               textview.grab_focus
394                               $notebook.set_page(pagenum)
395                           }
396                       }, candidate_undo_text)
397             candidate_undo_text = nil
398         end
399
400         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)
401             autoscroll_if_needed(scrolledwindow, image, textview)
402         end
403         false  #- propagate
404     }
405
406     return [ frame, textview ]
407 end
408
409 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
410
411     if !$modified_pixbufs[thumbnail_img]
412         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
413     elsif !$modified_pixbufs[thumbnail_img][:orig]
414         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
415     end
416
417     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
418
419     #- rotate
420     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
421         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
422         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
423         if pixbuf.height > desired_y
424             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
425         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
426             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
427         end
428     end
429
430     #- fix white balance
431     if $modified_pixbufs[thumbnail_img][:whitebalance]
432         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
433     end
434
435     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
436 end
437
438 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
439     $modified = true
440
441     #- update rotate attribute
442     xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
443
444     $modified_pixbufs[thumbnail_img] ||= {}
445     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
446     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
447
448     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
449 end
450
451 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
452     $modified = true
453
454     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
455
456     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
457               proc { |angle|
458                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
459                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
460                   proc {
461                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
462                       $notebook.set_page(0)
463                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
464                   }
465               }, -angle)
466 end
467
468 def color_swap(xmldir, attributes_prefix)
469     $modified = true
470     if xmldir.attributes["#{attributes_prefix}color-swap"]
471         xmldir.delete_attribute("#{attributes_prefix}color-swap")
472     else
473         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
474     end
475 end
476
477 def enhance(xmldir, attributes_prefix)
478     $modified = true
479     if xmldir.attributes["#{attributes_prefix}enhance"]
480         xmldir.delete_attribute("#{attributes_prefix}enhance")
481     else
482         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
483     end
484 end
485
486 def change_frame_offset(xmldir, attributes_prefix, value)
487     $modified = true
488     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
489 end
490
491 def ask_new_frame_offset(xmldir, attributes_prefix)
492     if xmldir
493         value = xmldir.attributes["#{attributes_prefix}frame-offset"]
494     else
495         value = ''
496     end
497
498     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
499                              $main_window,
500                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
501                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
502                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
503
504     lbl = Gtk::Label.new
505     lbl.markup = utf8(
506 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
507 from. There are approximately 25 frames per second in a video.
508 "))
509     dialog.vbox.add(lbl)
510     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
511     entry.signal_connect('key-press-event') { |w, event|
512         if event.keyval == Gdk::Keyval::GDK_Return
513             dialog.response(Gtk::Dialog::RESPONSE_OK)
514             true
515         elsif event.keyval == Gdk::Keyval::GDK_Escape
516             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
517             true
518         else
519             false  #- propagate if needed
520         end
521     }
522
523     dialog.window_position = Gtk::Window::POS_MOUSE
524     dialog.show_all
525
526     dialog.run { |response|
527         newval = entry.text
528         dialog.destroy
529         if response == Gtk::Dialog::RESPONSE_OK
530             $modified = true
531             msg 3, "changing frame offset to #{newval}"
532             return { :old => value, :new => newval }
533         else
534             return nil
535         end
536     }
537 end
538
539 def change_pano_amount(xmldir, attributes_prefix, value)
540     $modified = true
541     if value.nil?
542         xmldir.delete_attribute("#{attributes_prefix}pano-amount")
543     else
544         xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
545     end
546 end
547
548 def ask_new_pano_amount(xmldir, attributes_prefix)
549     if xmldir
550         value = xmldir.attributes["#{attributes_prefix}pano-amount"]
551     else
552         value = nil
553     end
554
555     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
556                              $main_window,
557                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
558                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
559                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
560
561     lbl = Gtk::Label.new
562     lbl.markup = utf8(
563 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
564 of this panorama image compared to other regular images. For example, if the panorama
565 was taken out of four photos on one row, counting the necessary overlap, the width of
566 this panorama image should probably be roughly three times the width of regular images.
567
568 With this information, booh will be able to generate panorama thumbnails looking
569 the right 'size'.
570 "))
571     dialog.vbox.add(lbl)
572     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)")))).
573                                                                          add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
574                                                                          add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
575                                                                          add(Gtk::Label.new(utf8(_("times the width of other images"))))))
576     dialog.window_position = Gtk::Window::POS_MOUSE
577     dialog.show_all
578     if value
579         spin.value = value.to_f
580         rb_yes.active = true
581         spin.grab_focus
582     else
583         rb_no.active = true
584     end
585
586     dialog.run { |response|
587         if rb_no.active?
588             newval = nil
589         else
590             newval = spin.value.to_f
591         end
592         dialog.destroy
593         if response == Gtk::Dialog::RESPONSE_OK
594             $modified = true
595             msg 3, "changing panorama amount to #{newval}"
596             return { :old => value, :new => newval }
597         else
598             return nil
599         end
600     }
601 end
602
603 def change_whitebalance(xmlelem, attributes_prefix, value)
604     $modified = true
605     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
606 end
607
608 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
609
610     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
611     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
612         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
613         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
614         destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
615         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
616                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
617         $modified_pixbufs[thumbnail_img] ||= {}
618         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
619         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
620         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
621     end
622
623     $modified_pixbufs[thumbnail_img] ||= {}
624     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
625
626     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
627 end
628
629 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
630     #- init $modified_pixbufs correctly
631 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
632
633     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
634
635     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
636                              $main_window,
637                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
638                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
639                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
640
641     lbl = Gtk::Label.new
642     lbl.markup = utf8(
643 _("You can fix the <b>white balance</b> of the image, if your image is too blue
644 or too yellow because your camera didn't detect the light correctly. Drag the
645 slider below the image to the left for more blue, to the right for more yellow.
646 "))
647     dialog.vbox.add(lbl)
648     if img_
649         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
650     end
651     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
652     
653     dialog.window_position = Gtk::Window::POS_MOUSE
654     dialog.show_all
655
656     lastval = nil
657     timeout = Gtk.timeout_add(100) {
658         if hs.value != lastval
659             lastval = hs.value
660             if img_
661                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
662             end
663         end
664         true
665     }
666
667     dialog.run { |response|
668         Gtk.timeout_remove(timeout)
669         if response == Gtk::Dialog::RESPONSE_OK
670             $modified = true
671             newval = hs.value.to_s
672             msg 3, "changing white balance to #{newval}"
673             dialog.destroy
674             return { :old => value, :new => newval }
675         else
676             if thumbnail_img
677                 $modified_pixbufs[thumbnail_img] ||= {}
678                 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
679                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
680             end
681             dialog.destroy
682             return nil
683         end
684     }
685 end
686
687 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
688     system("rm -f '#{destfile}'")
689     #- type can be 'element' or 'subdir'
690     if type == 'element'
691         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
692     else
693         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
694     end
695 end
696
697 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
698     Thread.new {
699         push_mousecursor_wait
700         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
701         gtk_thread_protect {
702             img.set(destfile)
703             $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
704         }
705         pop_mousecursor
706     }
707 end
708
709 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
710     distribute_multiple_call = Proc.new { |action, arg|
711         $selected_elements.each_key { |path|
712             $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
713         }
714         if possible_actions[:can_multiple] && $selected_elements.length > 0
715             UndoHandler.begin_batch
716             $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
717             UndoHandler.end_batch
718         else
719             closures[action].call(arg)
720         end
721         $selected_elements = {}
722     }
723     menu = Gtk::Menu.new
724     if optionals.include?('change_image')
725         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
726         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
727         changeimg.signal_connect('activate') { closures[:change].call }
728         menu.append(Gtk::SeparatorMenuItem.new)
729     end
730     if !possible_actions[:can_multiple] || $selected_elements.length == 0
731         if closures[:view]
732             if type == 'image'
733                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
734                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
735                 view.signal_connect('activate') { closures[:view].call }
736             else
737                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
738                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
739                 view.signal_connect('activate') { closures[:view].call }
740                 menu.append(Gtk::SeparatorMenuItem.new)
741             end
742         end
743         if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
744             menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
745             exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
746             exif.signal_connect('activate') { show_popup($main_window,
747                                                          utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
748                                                          { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
749             menu.append(Gtk::SeparatorMenuItem.new)
750         end
751     end
752     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
753     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
754     r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
755     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
756     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
757     r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
758     if !possible_actions[:can_multiple] || $selected_elements.length == 0
759         menu.append(Gtk::SeparatorMenuItem.new)
760         if !possible_actions[:forbid_left]
761             menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
762             moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
763             moveleft.signal_connect('activate') { closures[:move].call('left') }
764             if !possible_actions[:can_left]
765                 moveleft.sensitive = false
766             end
767         end
768         if !possible_actions[:forbid_right]
769             menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
770             moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
771             moveright.signal_connect('activate') { closures[:move].call('right') }
772             if !possible_actions[:can_right]
773                 moveright.sensitive = false
774             end
775         end
776         if optionals.include?('move_top')
777             menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
778             movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
779             movetop.signal_connect('activate') { closures[:move].call('top') }
780             if !possible_actions[:can_top]
781                 movetop.sensitive = false
782             end
783         end
784         menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
785         moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
786         moveup.signal_connect('activate') { closures[:move].call('up') }
787         if !possible_actions[:can_up]
788             moveup.sensitive = false
789         end
790         menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
791         movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
792         movedown.signal_connect('activate') { closures[:move].call('down') }
793         if !possible_actions[:can_down]
794             movedown.sensitive = false
795         end
796         if optionals.include?('move_bottom')
797             menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
798             movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
799             movebottom.signal_connect('activate') { closures[:move].call('bottom') }
800             if !possible_actions[:can_bottom]
801                 movebottom.sensitive = false
802             end
803         end
804     end
805     if type == 'video'
806         if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
807             menu.append(Gtk::SeparatorMenuItem.new)
808             menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
809             color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
810             color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
811             menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
812             flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
813             flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
814             menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
815             frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
816             frame_offset.signal_connect('activate') {
817                 if possible_actions[:can_multiple] && $selected_elements.length > 0
818                     if values = ask_new_frame_offset(nil, '')
819                         distribute_multiple_call.call(:frame_offset, values)
820                     end
821                 else
822                     closures[:frame_offset].call
823                 end
824             }
825         end
826     end
827     menu.append(               Gtk::SeparatorMenuItem.new)
828     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
829     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
830     whitebalance.signal_connect('activate') { 
831         if possible_actions[:can_multiple] && $selected_elements.length > 0
832             if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
833                 distribute_multiple_call.call(:whitebalance, values)
834             end
835         else
836             closures[:whitebalance].call
837         end
838     }
839     if !possible_actions[:can_multiple] || $selected_elements.length == 0
840         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
841                                                                                                              _("Enhance constrast"))))
842     else
843         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
844     end
845     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
846     enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
847     if type == 'image' && possible_actions[:can_panorama]
848         menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
849         panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
850         panorama.signal_connect('activate') {
851             if possible_actions[:can_multiple] && $selected_elements.length > 0
852                 if values = ask_new_pano_amount(nil, '')
853                     distribute_multiple_call.call(:pano, values)
854                 end
855             else
856                 distribute_multiple_call.call(:pano)
857             end
858        }
859     end
860     if optionals.include?('delete')
861         menu.append(               Gtk::SeparatorMenuItem.new)
862         menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
863         cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
864         if !possible_actions[:can_multiple] || $selected_elements.length == 0
865             menu.append(paste_item   = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
866             paste_item.signal_connect('activate') { closures[:paste].call }
867             menu.append(clear_item   = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
868             clear_item.signal_connect('activate') { $cuts = [] }
869             if $cuts.size == 0
870                 paste_item.sensitive = clear_item.sensitive = false
871             end
872         end
873         menu.append(               Gtk::SeparatorMenuItem.new)
874         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
875         delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
876     end
877     menu.show_all
878     menu.popup(nil, nil, event.button, event.time)
879 end
880
881 def delete_current_subalbum
882     $modified = true
883     sel = $albums_tv.selection.selected_rows
884     $xmldir.elements.each { |e|
885         if e.name == 'image' || e.name == 'video'
886             e.add_attribute('deleted', 'true')
887         end
888     }
889     #- branch if we have a non deleted subalbum
890     if $xmldir.child_byname_notattr('dir', 'deleted')
891         $xmldir.delete_attribute('thumbnails-caption')
892         $xmldir.delete_attribute('thumbnails-captionfile')
893     else
894         $xmldir.add_attribute('deleted', 'true')
895         moveup = $xmldir
896         while moveup.parent.name == 'dir'
897             moveup = moveup.parent
898             if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
899                 moveup.add_attribute('deleted', 'true')
900             else
901                 break
902             end
903         end
904         sel[0].up!
905     end
906     save_changes('forced')
907     populate_subalbums_treeview(false)
908     $albums_tv.selection.select_path(sel[0])
909 end
910
911 def restore_deleted
912     $modified = true
913     save_changes
914     $current_path = nil  #- prevent save_changes from being rerun again
915     sel = $albums_tv.selection.selected_rows
916     restore_one = proc { |xmldir|
917         xmldir.elements.each { |e|
918             if e.name == 'dir' && e.attributes['deleted']
919                 restore_one.call(e)
920             end
921             e.delete_attribute('deleted')
922         }
923     }
924     restore_one.call($xmldir)
925     populate_subalbums_treeview(false)
926     $albums_tv.selection.select_path(sel[0])
927 end
928
929 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
930
931     img = nil
932     frame1 = Gtk::Frame.new
933     fullpath = from_utf8("#{$current_path}/#{filename}")
934
935     my_gen_real_thumbnail = proc {
936         gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
937     }
938
939     if type == 'video'
940         pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
941         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
942                                  pack_start(img = Gtk::Image.new).
943                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
944         px, mask = pxb.render_pixmap_and_mask
945         da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
946         da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
947     else
948         frame1.add(img = Gtk::Image.new)
949     end
950
951     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
952     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
953         my_gen_real_thumbnail.call
954     else
955         img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
956     end
957
958     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
959
960     tooltips = Gtk::Tooltips.new
961     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
962     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
963
964     frame2, textview = create_editzone($autotable_sw, 1, img)
965     textview.buffer.text = caption
966     textview.set_justification(Gtk::Justification::CENTER)
967
968     vbox = Gtk::VBox.new(false, 5)
969     vbox.pack_start(evtbox, false, false)
970     vbox.pack_start(frame2, false, false)
971     autotable.append(vbox, filename)
972
973     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
974     $vbox2widgets[vbox] = { :textview => textview, :image => img }
975
976     #- to be able to find widgets by name
977     $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
978
979     cleanup_all_thumbnails = proc {
980         #- remove out of sync images
981         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
982         for sizeobj in $images_size
983             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
984         end
985
986     }
987
988     rotate_and_cleanup = proc { |angle|
989         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
990         cleanup_all_thumbnails.call
991     }
992
993     move = proc { |direction|
994         do_method = "move_#{direction}"
995         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
996         perform = proc {
997             done = autotable.method(do_method).call(vbox)
998             textview.grab_focus  #- because if moving, focus is stolen
999             done
1000         }
1001         if perform.call
1002             save_undo(_("move %s") % direction,
1003                       proc {
1004                           autotable.method(undo_method).call(vbox)
1005                           textview.grab_focus  #- because if moving, focus is stolen
1006                           autoscroll_if_needed($autotable_sw, img, textview)
1007                           $notebook.set_page(1)
1008                           proc {
1009                               autotable.method(do_method).call(vbox)
1010                               textview.grab_focus  #- because if moving, focus is stolen
1011                               autoscroll_if_needed($autotable_sw, img, textview)
1012                               $notebook.set_page(1)
1013                           }
1014                       })
1015         end
1016     }
1017
1018     color_swap_and_cleanup = proc {
1019         perform_color_swap_and_cleanup = proc {
1020             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1021             my_gen_real_thumbnail.call
1022         }
1023
1024         cleanup_all_thumbnails.call
1025         perform_color_swap_and_cleanup.call
1026
1027         save_undo(_("color swap"),
1028                   proc {
1029                       perform_color_swap_and_cleanup.call
1030                       textview.grab_focus
1031                       autoscroll_if_needed($autotable_sw, img, textview)
1032                       $notebook.set_page(1)
1033                       proc {
1034                           perform_color_swap_and_cleanup.call
1035                           textview.grab_focus
1036                           autoscroll_if_needed($autotable_sw, img, textview)
1037                           $notebook.set_page(1)
1038                       }
1039                   })
1040     }
1041
1042     change_frame_offset_and_cleanup_real = proc { |values|
1043         perform_change_frame_offset_and_cleanup = proc { |val|
1044             change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1045             my_gen_real_thumbnail.call
1046         }
1047         perform_change_frame_offset_and_cleanup.call(values[:new])
1048         
1049         save_undo(_("specify frame offset"),
1050                   proc {
1051                       perform_change_frame_offset_and_cleanup.call(values[:old])
1052                       textview.grab_focus
1053                       autoscroll_if_needed($autotable_sw, img, textview)
1054                       $notebook.set_page(1)
1055                       proc {
1056                           perform_change_frame_offset_and_cleanup.call(values[:new])
1057                           textview.grab_focus
1058                           autoscroll_if_needed($autotable_sw, img, textview)
1059                           $notebook.set_page(1)
1060                       }
1061                   })
1062     }
1063
1064     change_frame_offset_and_cleanup = proc {
1065         if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1066             change_frame_offset_and_cleanup_real.call(values)
1067         end
1068     }
1069
1070     change_pano_amount_and_cleanup_real = proc { |values|
1071         perform_change_pano_amount_and_cleanup = proc { |val|
1072             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1073         }
1074         perform_change_pano_amount_and_cleanup.call(values[:new])
1075         
1076         save_undo(_("change panorama amount"),
1077                   proc {
1078                       perform_change_pano_amount_and_cleanup.call(values[:old])
1079                       textview.grab_focus
1080                       autoscroll_if_needed($autotable_sw, img, textview)
1081                       $notebook.set_page(1)
1082                       proc {
1083                           perform_change_pano_amount_and_cleanup.call(values[:new])
1084                           textview.grab_focus
1085                           autoscroll_if_needed($autotable_sw, img, textview)
1086                           $notebook.set_page(1)
1087                       }
1088                   })
1089     }
1090
1091     change_pano_amount_and_cleanup = proc {
1092         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1093             change_pano_amount_and_cleanup_real.call(values)
1094         end
1095     }
1096
1097     whitebalance_and_cleanup_real = proc { |values|
1098         perform_change_whitebalance_and_cleanup = proc { |val|
1099             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1100             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1101                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1102             cleanup_all_thumbnails.call
1103         }
1104         perform_change_whitebalance_and_cleanup.call(values[:new])
1105
1106         save_undo(_("fix white balance"),
1107                   proc {
1108                       perform_change_whitebalance_and_cleanup.call(values[:old])
1109                       textview.grab_focus
1110                       autoscroll_if_needed($autotable_sw, img, textview)
1111                       $notebook.set_page(1)
1112                       proc {
1113                           perform_change_whitebalance_and_cleanup.call(values[:new])
1114                           textview.grab_focus
1115                           autoscroll_if_needed($autotable_sw, img, textview)
1116                           $notebook.set_page(1)
1117                       }
1118                   })
1119     }
1120
1121     whitebalance_and_cleanup = proc {
1122         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1123                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1124             whitebalance_and_cleanup_real.call(values)
1125         end
1126     }
1127
1128     enhance_and_cleanup = proc {
1129         perform_enhance_and_cleanup = proc {
1130             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1131             my_gen_real_thumbnail.call
1132         }
1133
1134         cleanup_all_thumbnails.call
1135         perform_enhance_and_cleanup.call
1136
1137         save_undo(_("enhance"),
1138                   proc {
1139                       perform_enhance_and_cleanup.call
1140                       textview.grab_focus
1141                       autoscroll_if_needed($autotable_sw, img, textview)
1142                       $notebook.set_page(1)
1143                       proc {
1144                           perform_enhance_and_cleanup.call
1145                           textview.grab_focus
1146                           autoscroll_if_needed($autotable_sw, img, textview)
1147                           $notebook.set_page(1)
1148                       }
1149                   })
1150     }
1151
1152     delete = proc { |isacut|
1153         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 })
1154             $modified = true
1155             after = nil
1156             perform_delete = proc {
1157                 after = autotable.get_next_widget(vbox)
1158                 if !after
1159                     after = autotable.get_previous_widget(vbox)
1160                 end
1161                 if $config['deleteondisk'] && !isacut
1162                     msg 3, "scheduling for delete: #{fullpath}"
1163                     $todelete << fullpath
1164                 end
1165                 autotable.remove(vbox)
1166                 if after
1167                     $vbox2widgets[after][:textview].grab_focus
1168                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1169                 end
1170             }
1171             
1172             previous_pos = autotable.get_current_number(vbox)
1173             perform_delete.call
1174
1175             if !after
1176                 delete_current_subalbum
1177             else
1178                 save_undo(_("delete"),
1179                           proc { |pos|
1180                               autotable.reinsert(pos, vbox, filename)
1181                               $notebook.set_page(1)
1182                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1183                               $cuts = []
1184                               msg 3, "removing deletion schedule of: #{fullpath}"
1185                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1186                               proc {
1187                                   perform_delete.call
1188                                   $notebook.set_page(1)
1189                               }
1190                           }, previous_pos)
1191             end
1192         end
1193     }
1194
1195     cut = proc {
1196         delete.call(true)
1197         $cuts << { :vbox => vbox, :filename => filename }
1198         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1199     }
1200     paste = proc {
1201         if $cuts.size > 0
1202             $cuts.each { |elem|
1203                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1204             }
1205             last = $cuts[-1]
1206             autotable.queue_draws << proc {
1207                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1208                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1209             }
1210             save_undo(_("paste"),
1211                       proc { |cuts|
1212                           cuts.each { |elem| autotable.remove(elem[:vbox]) }
1213                           $notebook.set_page(1)
1214                           proc {
1215                               cuts.each { |elem|
1216                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1217                               }
1218                               $notebook.set_page(1)
1219                           }
1220                       }, $cuts)
1221             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1222             $cuts = []
1223         end
1224     }
1225
1226     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1227                                  :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1228                                  :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1229
1230     textview.signal_connect('key-press-event') { |w, event|
1231         propagate = true
1232         if event.state != 0
1233             x, y = autotable.get_current_pos(vbox)
1234             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1235             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1236             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1237             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1238                 if control_pressed
1239                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1240                         $vbox2widgets[widget_up][:textview].grab_focus
1241                     end
1242                 end
1243                 if shift_pressed
1244                     move.call('up')
1245                 end
1246             end
1247             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1248                 if control_pressed
1249                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1250                         $vbox2widgets[widget_down][:textview].grab_focus
1251                     end
1252                 end
1253                 if shift_pressed
1254                     move.call('down')
1255                 end
1256             end
1257             if event.keyval == Gdk::Keyval::GDK_Left
1258                 if x > 0
1259                     if control_pressed
1260                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1261                     end
1262                     if shift_pressed
1263                         move.call('left')
1264                     end
1265                 end
1266                 if alt_pressed
1267                     rotate_and_cleanup.call(-90)
1268                 end
1269             end
1270             if event.keyval == Gdk::Keyval::GDK_Right
1271                 next_ = autotable.get_next_widget(vbox)
1272                 if next_ && autotable.get_current_pos(next_)[0] > x
1273                     if control_pressed
1274                         $vbox2widgets[next_][:textview].grab_focus
1275                     end
1276                     if shift_pressed
1277                         move.call('right')
1278                     end
1279                 end
1280                 if alt_pressed
1281                     rotate_and_cleanup.call(90)
1282                 end
1283             end
1284             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1285                 delete.call(false)
1286             end
1287             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1288                 view_element(filename, { :delete => delete })
1289                 propagate = false
1290             end
1291             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1292                 perform_undo
1293             end
1294             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1295                 perform_redo
1296             end
1297         end
1298         !propagate  #- propagate if needed
1299     }
1300
1301     $ignore_next_release = false
1302     evtbox.signal_connect('button-press-event') { |w, event|
1303         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1304             if event.state & Gdk::Window::BUTTON3_MASK != 0
1305                 #- gesture redo: hold right mouse button then click left mouse button
1306                 $config['nogestures'] or perform_redo
1307                 $ignore_next_release = true
1308             else
1309                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1310                 if $r90.active?
1311                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1312                 elsif $r270.active?
1313                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1314                 elsif $enhance.active?
1315                     enhance_and_cleanup.call
1316                 elsif $delete.active?
1317                     delete.call(false)
1318                 else
1319                     textview.grab_focus
1320                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1321                 end
1322             end
1323             $button1_pressed_autotable = true
1324         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1325             if event.state & Gdk::Window::BUTTON1_MASK != 0
1326                 #- gesture undo: hold left mouse button then click right mouse button
1327                 $config['nogestures'] or perform_undo
1328                 $ignore_next_release = true
1329             end
1330         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1331             view_element(filename, { :delete => delete })
1332         end
1333         false   #- propagate
1334     }
1335
1336     evtbox.signal_connect('button-release-event') { |w, event|
1337         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1338             if !$ignore_next_release
1339                 x, y = autotable.get_current_pos(vbox)
1340                 next_ = autotable.get_next_widget(vbox)
1341                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1342                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1343                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1344                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1345                                        :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1346                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1347                                        :pano => change_pano_amount_and_cleanup })
1348             end
1349             $ignore_next_release = false
1350             $gesture_press = nil
1351         end
1352         false   #- propagate
1353     }
1354
1355     #- handle reordering with drag and drop
1356     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1357     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1358     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1359         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1360     }
1361
1362     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1363         done = false
1364         #- mouse gesture first (dnd disables button-release-event)
1365         if $gesture_press && $gesture_press[:filename] == filename
1366             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1367                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1368                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1369                 rotate_and_cleanup.call(angle)
1370                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1371                 done = true
1372             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1373                 msg 3, "gesture delete: click-drag right button to the bottom"
1374                 delete.call(false)
1375                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1376                 done = true
1377             end
1378         end
1379         if !done
1380             ctxt.targets.each { |target|
1381                 if target.name == 'reorder-elements'
1382                     move_dnd = proc { |from,to|
1383                         if from != to
1384                             $modified = true
1385                             autotable.move(from, to)
1386                             save_undo(_("reorder"),
1387                                       proc { |from, to|
1388                                           if to > from
1389                                               autotable.move(to - 1, from)
1390                                           else
1391                                               autotable.move(to, from + 1)
1392                                           end
1393                                           $notebook.set_page(1)
1394                                           proc {
1395                                               autotable.move(from, to)
1396                                               $notebook.set_page(1)
1397                                           }
1398                                       }, from, to)
1399                         end
1400                     }
1401                     if $multiple_dnd.size == 0
1402                         move_dnd.call(selection_data.data.to_i,
1403                                       autotable.get_current_number(vbox))
1404                     else
1405                         UndoHandler.begin_batch
1406                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1407                                       each { |path|
1408                             #- need to update current position between each call
1409                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1410                                           autotable.get_current_number(vbox))
1411                         }
1412                         UndoHandler.end_batch
1413                     end
1414                     $multiple_dnd = []
1415                 end
1416             }
1417         end
1418     }
1419
1420     vbox.show_all
1421 end
1422
1423 def create_auto_table
1424
1425     $autotable = Gtk::AutoTable.new(5)
1426
1427     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1428     thumbnails_vb = Gtk::VBox.new(false, 5)
1429
1430     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1431     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1432     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1433     thumbnails_vb.add($autotable)
1434
1435     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1436     $autotable_sw.add_with_viewport(thumbnails_vb)
1437
1438     #- follows stuff for handling multiple elements selection
1439     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1440     gc = nil
1441     update_selected = proc {
1442         $autotable.current_order.each { |path|
1443             w = $name2widgets[path][:evtbox].window
1444             xm = w.position[0] + w.size[0]/2
1445             ym = w.position[1] + w.size[1]/2
1446             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1447                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1448                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1449                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1450                 end
1451             end
1452             if $selected_elements[path] && ! $selected_elements[path][:keep]
1453                 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))
1454                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1455                     $selected_elements.delete(path)
1456                 end
1457             end
1458         }
1459     }
1460     $autotable.signal_connect('realize') { |w,e|
1461         gc = Gdk::GC.new($autotable.window)
1462         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1463         gc.function = Gdk::GC::INVERT
1464         #- autoscroll handling for DND and multiple selections
1465         Gtk.timeout_add(100) {
1466             if ! $autotable.window.nil?
1467                 w, x, y, mask = $autotable.window.pointer
1468                 if mask & Gdk::Window::BUTTON1_MASK != 0
1469                     if y < $autotable_sw.vadjustment.value
1470                         if pos_x
1471                             $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]])
1472                         end
1473                         if $button1_pressed_autotable || press_x
1474                             scroll_upper($autotable_sw, y)
1475                         end
1476                         if not press_x.nil?
1477                             w, pos_x, pos_y = $autotable.window.pointer
1478                             $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]])
1479                             update_selected.call
1480                         end
1481                     end
1482                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1483                         if pos_x
1484                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1485                         end
1486                         if $button1_pressed_autotable || press_x
1487                             scroll_lower($autotable_sw, y)
1488                         end
1489                         if not press_x.nil?
1490                             w, pos_x, pos_y = $autotable.window.pointer
1491                             $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]])
1492                             update_selected.call
1493                         end
1494                     end
1495                 end
1496             end
1497             ! $autotable.window.nil?
1498         }
1499     }
1500
1501     $autotable.signal_connect('button-press-event') { |w,e|
1502         if e.button == 1
1503             if !$button1_pressed_autotable
1504                 press_x = e.x
1505                 press_y = e.y
1506                 if e.state & Gdk::Window::SHIFT_MASK == 0
1507                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1508                     $selected_elements = {}
1509                     $statusbar.push(0, utf8(_("Nothing selected.")))
1510                 else
1511                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1512                 end
1513                 set_mousecursor(Gdk::Cursor::TCROSS)
1514             end
1515         end
1516     }
1517     $autotable.signal_connect('button-release-event') { |w,e|
1518         if e.button == 1
1519             if $button1_pressed_autotable
1520                 #- unselect all only now
1521                 $multiple_dnd = $selected_elements.keys
1522                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1523                 $selected_elements = {}
1524                 $button1_pressed_autotable = false
1525             else
1526                 if pos_x
1527                     $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]])
1528                     if $selected_elements.length > 0
1529                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1530                     end
1531                 end
1532                 press_x = press_y = pos_x = pos_y = nil
1533                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1534             end
1535         end
1536     }
1537     $autotable.signal_connect('motion-notify-event') { |w,e|
1538         if ! press_x.nil?
1539             if pos_x
1540                 $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]])
1541             end
1542             pos_x = e.x
1543             pos_y = e.y
1544             $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]])
1545             update_selected.call
1546         end
1547     }
1548
1549 end
1550
1551 def create_subalbums_page
1552
1553     subalbums_hb = Gtk::HBox.new
1554     $subalbums_vb = Gtk::VBox.new(false, 5)
1555     subalbums_hb.pack_start($subalbums_vb, false, false)
1556     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1557     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1558     $subalbums_sw.add_with_viewport(subalbums_hb)
1559 end
1560
1561 def save_current_file
1562     save_changes
1563
1564     if $filename
1565         begin
1566             begin
1567                 ios = File.open($filename, "w")
1568                 $xmldoc.write(ios, 0)
1569                 ios.close
1570             rescue Iconv::IllegalSequence
1571                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1572                 if ! ios.nil? && ! ios.closed?
1573                     ios.close
1574                 end
1575                 $xmldoc.xml_decl.encoding = 'UTF-8'
1576                 ios = File.open($filename, "w")
1577                 $xmldoc.write(ios, 0)
1578                 ios.close
1579             end
1580             return true
1581         rescue Exception
1582             return false
1583         end
1584     end
1585 end
1586
1587 def save_current_file_user
1588     save_tempfilename = $filename
1589     $filename = $orig_filename
1590     if ! save_current_file
1591         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1592         $filename = save_tempfilename
1593         return
1594     end
1595     $modified = false
1596     $generated_outofline = false
1597     $filename = save_tempfilename
1598
1599     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1600     $todelete.each { |f|
1601         system("rm -f #{f}")
1602     }
1603 end
1604
1605 def mark_document_as_dirty
1606     $xmldoc.elements.each('//dir') { |elem|
1607         elem.delete_attribute('already-generated')
1608     }
1609 end
1610
1611 #- ret: true => ok  false => cancel
1612 def ask_save_modifications(msg1, msg2, *options)
1613     ret = true
1614     options = options.size > 0 ? options[0] : {}
1615     if $modified
1616         if options[:disallow_cancel]
1617             dialog = Gtk::Dialog.new(msg1,
1618                                      $main_window,
1619                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1620                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1621                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1622         else
1623             dialog = Gtk::Dialog.new(msg1,
1624                                      $main_window,
1625                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1626                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1627                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1628                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1629         end
1630         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1631         dialog.vbox.add(Gtk::Label.new(msg2))
1632         dialog.window_position = Gtk::Window::POS_CENTER
1633         dialog.show_all
1634         
1635         dialog.run { |response|
1636             dialog.destroy
1637             if response == Gtk::Dialog::RESPONSE_YES
1638                 if ! save_current_file_user
1639                     return ask_save_modifications(msg1, msg2, options)
1640                 end
1641             else
1642                 #- if we have generated an album but won't save modifications, we must remove 
1643                 #- already-generated markers in original file
1644                 if $generated_outofline
1645                     begin
1646                         $xmldoc = REXML::Document.new File.new($orig_filename)
1647                         mark_document_as_dirty
1648                         ios = File.open($orig_filename, "w")
1649                         $xmldoc.write(ios, 0)
1650                         ios.close
1651                     rescue Exception
1652                         puts "exception: #{$!}"
1653                     end
1654                 end
1655             end
1656             if response == Gtk::Dialog::RESPONSE_CANCEL
1657                 ret = false
1658             end
1659             $todelete = []  #- unconditionally clear the list of images/videos to delete
1660         }
1661     end
1662     return ret
1663 end
1664
1665 def try_quit(*options)
1666     if ask_save_modifications(utf8(_("Save before quitting?")),
1667                               utf8(_("Do you want to save your changes before quitting?")),
1668                               *options)
1669         Gtk.main_quit
1670     end
1671 end
1672
1673 def show_popup(parent, msg, *options)
1674     dialog = Gtk::Dialog.new
1675     if options[0] && options[0][:title]
1676         dialog.title = options[0][:title]
1677     else
1678         dialog.title = utf8(_("Booh message"))
1679     end
1680     lbl = Gtk::Label.new
1681     if options[0] && options[0][:nomarkup]
1682         lbl.text = msg
1683     else
1684         lbl.markup = msg
1685     end
1686     if options[0] && options[0][:centered]
1687         lbl.set_justify(Gtk::Justification::CENTER)
1688     end
1689     if options[0] && options[0][:selectable]
1690         lbl.selectable = true
1691     end
1692     if options[0] && options[0][:topwidget]
1693         dialog.vbox.add(options[0][:topwidget])
1694     end
1695     if options[0] && options[0][:scrolled]
1696         sw = Gtk::ScrolledWindow.new(nil, nil)
1697         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1698         sw.add_with_viewport(lbl)
1699         dialog.vbox.add(sw)
1700         dialog.set_default_size(500, 600)
1701     else
1702         dialog.vbox.add(lbl)
1703         dialog.set_default_size(200, 120)
1704     end
1705     if options[0] && options[0][:okcancel]
1706         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1707     end
1708     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1709
1710     if options[0] && options[0][:pos_centered]
1711         dialog.window_position = Gtk::Window::POS_CENTER
1712     else
1713         dialog.window_position = Gtk::Window::POS_MOUSE
1714     end
1715
1716     if options[0] && options[0][:linkurl]
1717         linkbut = Gtk::Button.new('')
1718         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1719         linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1720         linkbut.relief = Gtk::RELIEF_NONE
1721         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1722         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1723         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1724     end
1725
1726     dialog.show_all
1727
1728     if !options[0] || !options[0][:not_transient]
1729         dialog.transient_for = parent
1730         dialog.run { |response|
1731             dialog.destroy
1732             if options[0] && options[0][:okcancel]
1733                 return response == Gtk::Dialog::RESPONSE_OK
1734             end
1735         }
1736     else
1737         dialog.signal_connect('response') { dialog.destroy }
1738     end
1739 end
1740
1741 def backend_wait_message(parent, msg, infopipe_path, mode)
1742     w = Gtk::Window.new
1743     w.set_transient_for(parent)
1744     w.modal = true
1745
1746     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1747     vb.pack_start(Gtk::Label.new(msg), false, false)
1748
1749     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1750     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1751     if mode != 'one dir scan'
1752         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1753     end
1754     if mode == 'web-album'
1755         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1756         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1757     end
1758     vb.pack_start(Gtk::HSeparator.new, false, false)
1759
1760     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1761     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1762     vb.pack_end(bottom, false, false)
1763
1764     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1765     refresh_thread = Thread.new {
1766         directories_counter = 0
1767         while line = infopipe.gets
1768             if line =~ /^directories: (\d+), sizes: (\d+)/
1769                 directories = $1.to_f + 1
1770                 sizes = $2.to_f
1771             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1772                 elements = $3.to_f + 1
1773                 if mode == 'web-album'
1774                     elements += sizes
1775                 end
1776                 element_counter = 0
1777                 gtk_thread_protect { pb1_1.fraction = 0 }
1778                 if mode != 'one dir scan'
1779                     newtext = utf8(full_src_dir_to_rel($1, $2))
1780                     newtext = '/' if newtext == ''
1781                     gtk_thread_protect { pb1_2.text = newtext }
1782                     directories_counter += 1
1783                     gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1784                 end
1785             elsif line =~ /^processing element$/
1786                 element_counter += 1
1787                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1788             elsif line =~ /^processing size$/
1789                 element_counter += 1
1790                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1791             elsif line =~ /^finished processing sizes$/
1792                 gtk_thread_protect { pb1_1.fraction = 1 }
1793             elsif line =~ /^creating index.html$/
1794                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1795                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1796                 directories_counter = 0
1797             elsif line =~ /^index.html: (.+)\|(.+)/
1798                 newtext = utf8(full_src_dir_to_rel($1, $2))
1799                 newtext = '/' if newtext == ''
1800                 gtk_thread_protect { pb2.text = newtext }
1801                 directories_counter += 1
1802                 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1803             elsif line =~ /^die: (.*)$/
1804                 $diemsg = $1
1805             end
1806         end
1807     }
1808
1809     w.add(vb)
1810     w.signal_connect('delete-event') { w.destroy }
1811     w.signal_connect('destroy') {
1812         Thread.kill(refresh_thread)
1813         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1814         if infopipe_path
1815             infopipe.close
1816             system("rm -f #{infopipe_path}")
1817         end
1818     }
1819     w.window_position = Gtk::Window::POS_CENTER
1820     w.show_all
1821
1822     return [ b, w ]
1823 end
1824
1825 def call_backend(cmd, waitmsg, mode, params)
1826     pipe = Tempfile.new("boohpipe")
1827     pipe.close!
1828     system("mkfifo #{pipe.path}")
1829     cmd += " --info-pipe #{pipe.path}"
1830     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1831     pid = nil
1832     Thread.new {
1833         msg 2, cmd
1834         if pid = fork
1835             id, exitstatus = Process.waitpid2(pid)
1836             gtk_thread_protect { w8.destroy }
1837             if exitstatus == 0
1838                 if params[:successmsg]
1839                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1840                 end
1841                 if params[:closure_after]
1842                     gtk_thread_protect(&params[:closure_after])
1843                 end
1844             elsif exitstatus == 15
1845                 #- say nothing, user aborted
1846             else
1847                 gtk_thread_protect { show_popup($main_window,
1848                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1849             end
1850         else
1851             exec(cmd)
1852         end
1853     }
1854     button.signal_connect('clicked') {
1855         Process.kill('SIGTERM', pid)
1856     }
1857 end
1858
1859 def save_changes(*forced)
1860     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1861         return
1862     end
1863
1864     $xmldir.delete_attribute('already-generated')
1865
1866     propagate_children = proc { |xmldir|
1867         if xmldir.attributes['subdirs-caption']
1868             xmldir.delete_attribute('already-generated')
1869         end
1870         xmldir.elements.each('dir') { |element|
1871             propagate_children.call(element)
1872         }
1873     }
1874
1875     if $xmldir.child_byname_notattr('dir', 'deleted')
1876         new_title = $subalbums_title.buffer.text
1877         if new_title != $xmldir.attributes['subdirs-caption']
1878             parent = $xmldir.parent
1879             if parent.name == 'dir'
1880                 parent.delete_attribute('already-generated')
1881             end
1882             propagate_children.call($xmldir)
1883         end
1884         $xmldir.add_attribute('subdirs-caption', new_title)
1885         $xmldir.elements.each('dir') { |element|
1886             if !element.attributes['deleted']
1887                 path = element.attributes['path']
1888                 newtext = $subalbums_edits[path][:editzone].buffer.text
1889                 if element.attributes['subdirs-caption']
1890                     if element.attributes['subdirs-caption'] != newtext
1891                         propagate_children.call(element)
1892                     end
1893                     element.add_attribute('subdirs-caption',     newtext)
1894                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1895                 else
1896                     if element.attributes['thumbnails-caption'] != newtext
1897                         element.delete_attribute('already-generated')
1898                     end
1899                     element.add_attribute('thumbnails-caption',     newtext)
1900                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1901                 end
1902             end
1903         }
1904     end
1905
1906     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1907         if $xmldir.attributes['thumbnails-caption']
1908             path = $xmldir.attributes['path']
1909             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1910         end
1911     elsif $xmldir.attributes['thumbnails-caption']
1912         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1913     end
1914
1915     #- remove and reinsert elements to reflect new ordering
1916     saves = {}
1917     cpt = 0
1918     $xmldir.elements.each { |element|
1919         if element.name == 'image' || element.name == 'video'
1920             saves[element.attributes['filename']] = element.remove
1921             cpt += 1
1922         end
1923     }
1924     $autotable.current_order.each { |path|
1925         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1926         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1927         saves.delete(path)
1928     }
1929     saves.each_key { |path|
1930         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1931         chld.add_attribute('deleted', 'true')
1932     }
1933 end
1934
1935 def sort_by_exif_date
1936     $modified = true
1937     save_changes
1938     current_order = []
1939     $xmldir.elements.each { |element|
1940         if element.name == 'image' || element.name == 'video'
1941             current_order << element.attributes['filename']
1942         end
1943     }
1944
1945     #- look for EXIF dates
1946     w = Gtk::Window.new
1947     w.set_transient_for($main_window)
1948     w.modal = true
1949     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1950     vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1951     vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1952     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1953     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1954     vb.pack_end(bottom, false, false)
1955     w.add(vb)
1956     w.signal_connect('delete-event') { w.destroy }
1957     w.window_position = Gtk::Window::POS_CENTER
1958     w.show_all
1959
1960     aborted = false
1961     b.signal_connect('clicked') { aborted = true }
1962     dates = {}
1963     i = 0
1964     current_order.each { |f|
1965         i += 1
1966         if entry2type(f) == 'image'
1967             pb.text = f
1968             pb.fraction = i.to_f / current_order.size
1969             Gtk.main_iteration while Gtk.events_pending?
1970             date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1971             if $? == 0 && date_time != ''
1972                 dates[f] = date_time
1973             end
1974         end
1975         if aborted
1976             break
1977         end
1978     }
1979     w.destroy
1980     if aborted
1981         return
1982     end
1983
1984     saves = {}
1985     $xmldir.elements.each { |element|
1986         if element.name == 'image' || element.name == 'video'
1987             saves[element.attributes['filename']] = element.remove
1988         end
1989     }
1990
1991     #- find a good fallback for all entries without a date (still next to the item they were next to)
1992     neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
1993     for i in 0 .. current_order.size - 1
1994         if ! neworder.include?(current_order[i])
1995             j = i - 1
1996             while j > 0 && ! neworder.include?(current_order[j])
1997                 j -= 1
1998             end
1999             neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2000         end
2001     end
2002     neworder.each { |f|
2003         $xmldir.add_element(saves[f].name, saves[f].attributes)
2004     }
2005
2006     #- let the auto-table reflect new ordering
2007     change_dir
2008 end
2009
2010 def remove_all_captions
2011     $modified = true
2012     texts = {}
2013     $autotable.current_order.each { |path|
2014         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2015         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2016     }
2017     save_undo(_("remove all captions"),
2018               proc { |texts|
2019                   texts.each_key { |key|
2020                       $name2widgets[key][:textview].buffer.text = texts[key]
2021                   }
2022                   $notebook.set_page(1)
2023                   proc {
2024                       texts.each_key { |key|
2025                           $name2widgets[key][:textview].buffer.text = ''
2026                       }
2027                       $notebook.set_page(1)
2028                   }
2029               }, texts)
2030 end
2031
2032 def change_dir
2033     $selected_elements.each_key { |path|
2034         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2035     }
2036     $autotable.clear
2037     $vbox2widgets = {}
2038     $name2widgets = {}
2039     $name2closures = {}
2040     $selected_elements = {}
2041     $cuts = []
2042     $multiple_dnd = []
2043     UndoHandler.cleanup
2044     $undo_tb.sensitive = $undo_mb.sensitive = false
2045     $redo_tb.sensitive = $redo_mb.sensitive = false
2046
2047     if !$current_path
2048         return
2049     end
2050
2051     $subalbums_vb.children.each { |chld|
2052         $subalbums_vb.remove(chld)
2053     }
2054     $subalbums = Gtk::Table.new(0, 0, true)
2055     current_y_sub_albums = 0
2056
2057     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2058     $subalbums_edits = {}
2059     subalbums_counter = 0
2060     subalbums_edits_bypos = {}
2061
2062     add_subalbum = proc { |xmldir, counter|
2063         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2064         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2065         if xmldir == $xmldir
2066             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2067             caption = xmldir.attributes['thumbnails-caption']
2068             captionfile, dummy = find_subalbum_caption_info(xmldir)
2069             infotype = 'thumbnails'
2070         else
2071             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2072             captionfile, caption = find_subalbum_caption_info(xmldir)
2073             infotype = find_subalbum_info_type(xmldir)
2074         end
2075         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2076         hbox = Gtk::HBox.new
2077         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2078         f = Gtk::Frame.new
2079         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2080
2081         img = nil
2082         my_gen_real_thumbnail = proc {
2083             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2084         }
2085
2086         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2087             f.add(img = Gtk::Image.new)
2088             my_gen_real_thumbnail.call
2089         else
2090             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2091         end
2092         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2093         $subalbums.attach(hbox,
2094                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2095
2096         frame, textview = create_editzone($subalbums_sw, 0, img)
2097         textview.buffer.text = caption
2098         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2099                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2100
2101         change_image = proc {
2102             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2103                                             nil,
2104                                             Gtk::FileChooser::ACTION_OPEN,
2105                                             nil,
2106                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2107             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2108             fc.transient_for = $main_window
2109             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))
2110             f.add(preview_img = Gtk::Image.new)
2111             preview.show_all
2112             fc.signal_connect('update-preview') { |w|
2113                 begin
2114                     if fc.preview_filename
2115                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2116                         fc.preview_widget_active = true
2117                     end
2118                 rescue Gdk::PixbufError
2119                     fc.preview_widget_active = false
2120                 end
2121             }
2122             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2123                 $modified = true
2124                 old_file = captionfile
2125                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2126                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2127                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2128                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2129
2130                 new_file = fc.filename
2131                 msg 3, "new captionfile is: #{fc.filename}"
2132                 perform_changefile = proc {
2133                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2134                     $modified_pixbufs.delete(thumbnail_file)
2135                     xmldir.delete_attribute("#{infotype}-rotate")
2136                     xmldir.delete_attribute("#{infotype}-color-swap")
2137                     xmldir.delete_attribute("#{infotype}-enhance")
2138                     xmldir.delete_attribute("#{infotype}-frame-offset")
2139                     my_gen_real_thumbnail.call
2140                 }
2141                 perform_changefile.call
2142
2143                 save_undo(_("change caption file for sub-album"),
2144                           proc {
2145                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2146                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2147                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2148                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2149                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2150                               my_gen_real_thumbnail.call
2151                               $notebook.set_page(0)
2152                               proc {
2153                                   perform_changefile.call
2154                                   $notebook.set_page(0)
2155                               }
2156                           })
2157             end
2158             fc.destroy
2159         }
2160
2161         rotate_and_cleanup = proc { |angle|
2162             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2163             system("rm -f '#{thumbnail_file}'")
2164         }
2165
2166         move = proc { |direction|
2167             $modified = true
2168
2169             save_changes('forced')
2170             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2171             if direction == 'up'
2172                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2173                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2174             end
2175             if direction == 'down'
2176                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2177                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2178             end
2179             if direction == 'top'
2180                 for i in 1 .. oldpos - 1
2181                     subalbums_edits_bypos[i][:position] += 1
2182                 end
2183                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2184             end
2185             if direction == 'bottom'
2186                 for i in oldpos + 1 .. subalbums_counter
2187                     subalbums_edits_bypos[i][:position] -= 1
2188                 end
2189                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2190             end
2191
2192             elems = []
2193             $xmldir.elements.each('dir') { |element|
2194                 if (!element.attributes['deleted'])
2195                     elems << [ element.attributes['path'], element.remove ]
2196                 end
2197             }
2198             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2199                   each { |e| $xmldir.add_element(e[1]) }
2200             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2201             $xmldir.elements.each('descendant::dir') { |elem|
2202                 elem.delete_attribute('already-generated')
2203             }
2204
2205             sel = $albums_tv.selection.selected_rows
2206             change_dir
2207             populate_subalbums_treeview(false)
2208             $albums_tv.selection.select_path(sel[0])
2209         }
2210
2211         color_swap_and_cleanup = proc {
2212             perform_color_swap_and_cleanup = proc {
2213                 color_swap(xmldir, "#{infotype}-")
2214                 my_gen_real_thumbnail.call
2215             }
2216             perform_color_swap_and_cleanup.call
2217
2218             save_undo(_("color swap"),
2219                       proc {
2220                           perform_color_swap_and_cleanup.call
2221                           $notebook.set_page(0)
2222                           proc {
2223                               perform_color_swap_and_cleanup.call
2224                               $notebook.set_page(0)
2225                           }
2226                       })
2227         }
2228
2229         change_frame_offset_and_cleanup = proc {
2230             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2231                 perform_change_frame_offset_and_cleanup = proc { |val|
2232                     change_frame_offset(xmldir, "#{infotype}-", val)
2233                     my_gen_real_thumbnail.call
2234                 }
2235                 perform_change_frame_offset_and_cleanup.call(values[:new])
2236
2237                 save_undo(_("specify frame offset"),
2238                           proc {
2239                               perform_change_frame_offset_and_cleanup.call(values[:old])
2240                               $notebook.set_page(0)
2241                               proc {
2242                                   perform_change_frame_offset_and_cleanup.call(values[:new])
2243                                   $notebook.set_page(0)
2244                               }
2245                           })
2246             end
2247         }
2248
2249         whitebalance_and_cleanup = proc {
2250             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2251                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2252                 perform_change_whitebalance_and_cleanup = proc { |val|
2253                     change_whitebalance(xmldir, "#{infotype}-", val)
2254                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2255                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2256                     system("rm -f '#{thumbnail_file}'")
2257                 }
2258                 perform_change_whitebalance_and_cleanup.call(values[:new])
2259                 
2260                 save_undo(_("fix white balance"),
2261                           proc {
2262                               perform_change_whitebalance_and_cleanup.call(values[:old])
2263                               $notebook.set_page(0)
2264                               proc {
2265                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2266                                   $notebook.set_page(0)
2267                               }
2268                           })
2269             end
2270         }
2271
2272         enhance_and_cleanup = proc {
2273             perform_enhance_and_cleanup = proc {
2274                 enhance(xmldir, "#{infotype}-")
2275                 my_gen_real_thumbnail.call
2276             }
2277             
2278             perform_enhance_and_cleanup.call
2279             
2280             save_undo(_("enhance"),
2281                       proc {
2282                           perform_enhance_and_cleanup.call
2283                           $notebook.set_page(0)
2284                           proc {
2285                               perform_enhance_and_cleanup.call
2286                               $notebook.set_page(0)
2287                           }
2288                       })
2289         }
2290
2291         evtbox.signal_connect('button-press-event') { |w, event|
2292             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2293                 if $r90.active?
2294                     rotate_and_cleanup.call(90)
2295                 elsif $r270.active?
2296                     rotate_and_cleanup.call(-90)
2297                 elsif $enhance.active?
2298                     enhance_and_cleanup.call
2299                 end
2300             end
2301             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2302                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2303                                      { :forbid_left => true, :forbid_right => true,
2304                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2305                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2306                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2307                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2308             end
2309             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2310                 change_image.call
2311                 true   #- handled
2312             end
2313         }
2314         evtbox.signal_connect('button-press-event') { |w, event|
2315             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2316             false
2317         }
2318
2319         evtbox.signal_connect('button-release-event') { |w, event|
2320             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2321                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2322                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2323                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2324                     msg 3, "gesture rotate: #{angle}"
2325                     rotate_and_cleanup.call(angle)
2326                 end
2327             end
2328             $gesture_press = nil
2329         }
2330                 
2331         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2332         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2333         current_y_sub_albums += 1
2334     }
2335
2336     if $xmldir.child_byname_notattr('dir', 'deleted')
2337         #- title edition
2338         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2339         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2340         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2341         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2342         #- this album image/caption
2343         if $xmldir.attributes['thumbnails-caption']
2344             add_subalbum.call($xmldir, 0)
2345         end
2346     end
2347     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2348     $xmldir.elements.each { |element|
2349         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2350             #- element (image or video) of this album
2351             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2352             msg 3, "dest_img: #{dest_img}"
2353             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2354             total[element.name] += 1
2355         end
2356         if element.name == 'dir' && !element.attributes['deleted']
2357             #- sub-album image/caption
2358             add_subalbum.call(element, subalbums_counter += 1)
2359             total[element.name] += 1
2360         end
2361     }
2362     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2363                                                                                 total['image'], total['video'], total['dir'] ]))
2364     $subalbums_vb.add($subalbums)
2365     $subalbums_vb.show_all
2366
2367     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2368         $notebook.get_tab_label($autotable_sw).sensitive = false
2369         $notebook.set_page(0)
2370         $thumbnails_title.buffer.text = ''
2371     else
2372         $notebook.get_tab_label($autotable_sw).sensitive = true
2373         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2374     end
2375
2376     if !$xmldir.child_byname_notattr('dir', 'deleted')
2377         $notebook.get_tab_label($subalbums_sw).sensitive = false
2378         $notebook.set_page(1)
2379     else
2380         $notebook.get_tab_label($subalbums_sw).sensitive = true
2381     end
2382 end
2383
2384 def pixbuf_or_nil(filename)
2385     begin
2386         return Gdk::Pixbuf.new(filename)
2387     rescue
2388         return nil
2389     end
2390 end
2391
2392 def theme_choose(current)
2393     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2394                              $main_window,
2395                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2396                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2397                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2398
2399     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2400     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2401     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2402     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2403     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2404     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2405     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2406     treeview.signal_connect('button-press-event') { |w, event|
2407         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2408             dialog.response(Gtk::Dialog::RESPONSE_OK)
2409         end
2410     }
2411
2412     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2413
2414     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2415         dir.chomp!
2416         iter = model.append
2417         iter[0] = File.basename(dir)
2418         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2419         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2420         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2421         if File.basename(dir) == current
2422             treeview.selection.select_iter(iter)
2423         end
2424     }
2425
2426     dialog.set_default_size(700, 400)
2427     dialog.vbox.show_all
2428     dialog.run { |response|
2429         iter = treeview.selection.selected
2430         dialog.destroy
2431         if response == Gtk::Dialog::RESPONSE_OK && iter
2432             return model.get_value(iter, 0)
2433         end
2434     }
2435     return nil
2436 end
2437
2438 def show_password_protections
2439     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2440         child_iter = $albums_iters[xmldir.attributes['path']]
2441         if xmldir.attributes['password-protect']
2442             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2443             already_protected = true
2444         elsif already_protected
2445             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2446             if pix
2447                 pix = pix.saturate_and_pixelate(1, true)
2448             end
2449             child_iter[2] = pix
2450         else
2451             child_iter[2] = nil
2452         end
2453         xmldir.elements.each('dir') { |elem|
2454             if !elem.attributes['deleted']
2455                 examine_dir_elem.call(child_iter, elem, already_protected)
2456             end
2457         }
2458     }
2459     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2460 end
2461
2462 def populate_subalbums_treeview(select_first)
2463     $albums_ts.clear
2464     $autotable.clear
2465     $albums_iters = {}
2466     $subalbums_vb.children.each { |chld|
2467         $subalbums_vb.remove(chld)
2468     }
2469
2470     source = $xmldoc.root.attributes['source']
2471     msg 3, "source: #{source}"
2472
2473     xmldir = $xmldoc.elements['//dir']
2474     if !xmldir || xmldir.attributes['path'] != source
2475         msg 1, _("Corrupted booh file...")
2476         return
2477     end
2478
2479     append_dir_elem = proc { |parent_iter, xmldir|
2480         child_iter = $albums_ts.append(parent_iter)
2481         child_iter[0] = File.basename(xmldir.attributes['path'])
2482         child_iter[1] = xmldir.attributes['path']
2483         $albums_iters[xmldir.attributes['path']] = child_iter
2484         msg 3, "puttin location: #{xmldir.attributes['path']}"
2485         xmldir.elements.each('dir') { |elem|
2486             if !elem.attributes['deleted']
2487                 append_dir_elem.call(child_iter, elem)
2488             end
2489         }
2490     }
2491     append_dir_elem.call(nil, xmldir)
2492     show_password_protections
2493
2494     $albums_tv.expand_all
2495     if select_first
2496         $albums_tv.selection.select_iter($albums_ts.iter_first)
2497     end
2498 end
2499
2500 def open_file(filename)
2501
2502     $filename = nil
2503     $modified = false
2504     $current_path = nil   #- invalidate
2505     $modified_pixbufs = {}
2506     $albums_ts.clear
2507     $autotable.clear
2508     $subalbums_vb.children.each { |chld|
2509         $subalbums_vb.remove(chld)
2510     }
2511
2512     if !File.exists?(filename)
2513         return utf8(_("File not found."))
2514     end
2515
2516     begin
2517         $xmldoc = REXML::Document.new File.new(filename)
2518     rescue Exception
2519         $xmldoc = nil
2520     end
2521
2522     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2523         if entry2type(filename).nil?
2524             return utf8(_("Not a booh file!"))
2525         else
2526             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."))
2527         end
2528     end
2529
2530     if !source = $xmldoc.root.attributes['source']
2531         return utf8(_("Corrupted booh file..."))
2532     end
2533
2534     if !dest = $xmldoc.root.attributes['destination']
2535         return utf8(_("Corrupted booh file..."))
2536     end
2537
2538     if !theme = $xmldoc.root.attributes['theme']
2539         return utf8(_("Corrupted booh file..."))
2540     end
2541
2542     if $xmldoc.root.attributes['version'] < '0.8.4'
2543         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2544         mark_document_as_dirty
2545         if $xmldoc.root.attributes['version'] < '0.8.4'
2546             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2547             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2548                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2549                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2550                 if old_dest_dir != new_dest_dir
2551                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2552                 end
2553                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2554                     xmldir.elements.each { |element|
2555                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2556                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2557                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2558                             Dir[old_name + '*'].each { |file|
2559                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2560                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2561                             }
2562                         end
2563                         if element.name == 'dir' && !element.attributes['deleted']
2564                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2565                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2566                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2567                         end
2568                     }
2569                 else
2570                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2571                 end
2572             }
2573         end
2574         $xmldoc.root.add_attribute('version', $VERSION)
2575     end
2576
2577     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2578     optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2579     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2580
2581     $filename = filename
2582     select_theme(theme, limit_sizes, optimizefor32, nperrow)
2583     $default_size['thumbnails'] =~ /(.*)x(.*)/
2584     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2585     $albums_thumbnail_size =~ /(.*)x(.*)/
2586     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2587
2588     populate_subalbums_treeview(true)
2589
2590     $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
2591     return nil
2592 end
2593
2594 def open_file_user(filename)
2595     result = open_file(filename)
2596     if !result
2597         $config['last-opens'] ||= []
2598         if $config['last-opens'][-1] != utf8(filename)
2599             $config['last-opens'] << utf8(filename)
2600         end
2601         $orig_filename = $filename
2602         tmp = Tempfile.new("boohtemp")
2603         tmp.close!
2604         #- for security
2605         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2606         ios.close
2607         $tempfiles << $filename << "#{$filename}.backup"
2608     else
2609         $orig_filename = nil
2610     end
2611     return result
2612 end
2613
2614 def open_file_popup
2615     if !ask_save_modifications(utf8(_("Save this album?")),
2616                                utf8(_("Do you want to save the changes to this album?")),
2617                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2618         return
2619     end
2620     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2621                                     nil,
2622                                     Gtk::FileChooser::ACTION_OPEN,
2623                                     nil,
2624                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2625     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2626     fc.set_current_folder(File.expand_path("~/.booh"))
2627     fc.transient_for = $main_window
2628     ok = false
2629     while !ok
2630         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2631             push_mousecursor_wait(fc)
2632             msg = open_file_user(fc.filename)
2633             pop_mousecursor(fc)
2634             if msg
2635                 show_popup(fc, msg)
2636                 ok = false
2637             else
2638                 ok = true
2639             end
2640         else
2641             ok = true
2642         end
2643     end
2644     fc.destroy
2645 end
2646
2647 def open_url(url)
2648     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2649     msg 2, cmd
2650     system(cmd)
2651 end
2652
2653 def additional_booh_options
2654     options = ''
2655     if $config['mproc']
2656         options += "--mproc #{$config['mproc'].to_i} "
2657     end
2658     options += "--comments-format '#{$config['comments-format']}'"
2659     return options
2660 end
2661
2662 def new_album
2663     if !ask_save_modifications(utf8(_("Save this album?")),
2664                                utf8(_("Do you want to save the changes to this album?")),
2665                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2666         return
2667     end
2668     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2669                              $main_window,
2670                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2671                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2672                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2673     
2674     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2675     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2676                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2677     tbl.attach(src = Gtk::Entry.new,
2678                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2679     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2680                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2681     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2682                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2683     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2684                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2685     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2686                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2687     tbl.attach(dest = Gtk::Entry.new,
2688                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2689     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2690                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2691     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2692                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2693     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2694                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2695     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2696                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2697
2698     tooltips = Gtk::Tooltips.new
2699     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2700     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2701                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2702     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2703                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2704     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2705     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)
2706     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2707                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2708     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2709                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2710     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)
2711
2712     src_nb_calculated_for = ''
2713     src_nb_thread = nil
2714     process_src_nb = proc {
2715         if src.text != src_nb_calculated_for
2716             src_nb_calculated_for = src.text
2717             if src_nb_thread
2718                 Thread.kill(src_nb_thread)
2719                 src_nb_thread = nil
2720             end
2721             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2722                 if File.readable?(from_utf8(src_nb_calculated_for))
2723                     src_nb_thread = Thread.new {
2724                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2725                         total = { 'image' => 0, 'video' => 0, nil => 0 }
2726                         `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2727                             if File.basename(dir) =~ /^\./
2728                                 next
2729                             else
2730                                 begin
2731                                     Dir.entries(dir.chomp).each { |file|
2732                                         total[entry2type(file)] += 1
2733                                     }
2734                                 rescue Errno::EACCES, Errno::ENOENT
2735                                 end
2736                             end
2737                         }
2738                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2739                         src_nb_thread = nil
2740                     }
2741                 else
2742                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2743                 end
2744             else
2745                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2746             end
2747         end
2748         true
2749     }
2750     timeout_src_nb = Gtk.timeout_add(100) {
2751         process_src_nb.call
2752     }
2753
2754     src_browse.signal_connect('clicked') {
2755         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2756                                         nil,
2757                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2758                                         nil,
2759                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2760         fc.transient_for = $main_window
2761         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2762             src.text = utf8(fc.filename)
2763             process_src_nb.call
2764             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2765         end
2766         fc.destroy
2767     }
2768
2769     dest_browse.signal_connect('clicked') {
2770         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2771                                         nil,
2772                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2773                                         nil,
2774                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2775         fc.transient_for = $main_window
2776         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2777             dest.text = utf8(fc.filename)
2778         end
2779         fc.destroy
2780     }
2781
2782     conf_browse.signal_connect('clicked') {
2783         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2784                                         nil,
2785                                         Gtk::FileChooser::ACTION_SAVE,
2786                                         nil,
2787                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2788         fc.transient_for = $main_window
2789         fc.add_shortcut_folder(File.expand_path("~/.booh"))
2790         fc.set_current_folder(File.expand_path("~/.booh"))
2791         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2792             conf.text = utf8(fc.filename)
2793         end
2794         fc.destroy
2795     }
2796
2797     theme_sizes = []
2798     nperrows = []
2799     recreate_theme_config = proc {
2800         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2801         theme_sizes = []
2802         select_theme(theme_button.label, 'all', optimize432.active?, nil)
2803         $images_size.each { |s|
2804             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2805             if !s['optional']
2806                 cb.active = true
2807             end
2808             tooltips.set_tip(cb, utf8(s['description']), nil)
2809             theme_sizes << { :widget => cb, :value => s['name'] }
2810         }
2811         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2812         tooltips = Gtk::Tooltips.new
2813         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2814         theme_sizes << { :widget => cb, :value => 'original' }
2815         sizes.show_all
2816
2817         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2818         nperrow_group = nil
2819         nperrows = []
2820         $allowed_N_values.each { |n|
2821             if nperrow_group
2822                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2823             else
2824                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2825             end
2826             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2827             if $default_N == n
2828                 rb.active = true
2829             end
2830             nperrows << { :widget => rb, :value => n }
2831         }
2832         nperrowradios.show_all
2833     }
2834     recreate_theme_config.call
2835
2836     theme_button.signal_connect('clicked') {
2837         if newtheme = theme_choose(theme_button.label)
2838             theme_button.label = newtheme
2839             recreate_theme_config.call
2840         end
2841     }
2842
2843     dialog.vbox.add(frame1)
2844     dialog.vbox.add(frame2)
2845     dialog.window_position = Gtk::Window::POS_MOUSE
2846     dialog.show_all
2847
2848     keepon = true
2849     ok = true
2850     while keepon
2851         dialog.run { |response|
2852             if response == Gtk::Dialog::RESPONSE_OK
2853                 srcdir = from_utf8(src.text)
2854                 destdir = from_utf8(dest.text)
2855                 if !File.directory?(srcdir)
2856                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2857                     src.grab_focus
2858                 elsif conf.text == ''
2859                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2860                     conf.grab_focus
2861                 elsif File.directory?(from_utf8(conf.text))
2862                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2863                     conf.grab_focus
2864                 elsif destdir != make_dest_filename(destdir)
2865                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2866                     dest.grab_focus
2867                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2868                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2869                     dest.grab_focus
2870                 elsif File.exists?(destdir) && !File.directory?(destdir)
2871                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2872                     dest.grab_focus
2873                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2874                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2875                 else
2876                     system("mkdir '#{destdir}'")
2877                     if !File.directory?(destdir)
2878                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2879                         dest.grab_focus
2880                     else
2881                         keepon = false
2882                     end
2883                 end
2884             else
2885                 keepon = ok = false
2886             end
2887         }
2888     end
2889     srcdir = from_utf8(src.text)
2890     destdir = from_utf8(dest.text)
2891     configskel = File.expand_path(from_utf8(conf.text))
2892     theme = theme_button.label
2893     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2894     nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2895     opt432 = optimize432.active?
2896     madewith = madewithentry.text
2897     if src_nb_thread
2898         Thread.kill(src_nb_thread)
2899         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2900     end
2901     dialog.destroy
2902     Gtk.timeout_remove(timeout_src_nb)
2903
2904     if ok
2905         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2906                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2907                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2908                      utf8(_("Please wait while scanning source directory...")),
2909                      'full scan',
2910                      { :closure_after => proc { open_file_user(configskel) } })
2911     end
2912 end
2913
2914 def properties
2915     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2916                              $main_window,
2917                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2918                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2919                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2920     
2921     source = $xmldoc.root.attributes['source']
2922     dest = $xmldoc.root.attributes['destination']
2923     theme = $xmldoc.root.attributes['theme']
2924     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2925     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2926     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2927     if limit_sizes
2928         limit_sizes = limit_sizes.split(/,/)
2929     end
2930     madewith = $xmldoc.root.attributes['made-with']
2931
2932     tooltips = Gtk::Tooltips.new
2933     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2934     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2935                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2936     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2937                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2938     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2939                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2940     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2941                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2942     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2943                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2944     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2945                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2946
2947     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2948     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2949                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2950     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2951                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2952     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2953     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)
2954     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2955                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2956     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2957                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2958     if madewith
2959         madewithentry.text = madewith
2960     end
2961     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)
2962
2963     theme_sizes = []
2964     nperrows = []
2965     recreate_theme_config = proc {
2966         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2967         theme_sizes = []
2968         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2969
2970         $images_size.each { |s|
2971             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2972             if limit_sizes
2973                 if limit_sizes.include?(s['name'])
2974                     cb.active = true
2975                 end
2976             else
2977                 if !s['optional']
2978                     cb.active = true
2979                 end
2980             end
2981             tooltips.set_tip(cb, utf8(s['description']), nil)
2982             theme_sizes << { :widget => cb, :value => s['name'] }
2983         }
2984         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2985         tooltips = Gtk::Tooltips.new
2986         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2987         if limit_sizes && limit_sizes.include?('original')
2988             cb.active = true
2989         end
2990         theme_sizes << { :widget => cb, :value => 'original' }
2991         sizes.show_all
2992
2993         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2994         nperrow_group = nil
2995         nperrows = []
2996         $allowed_N_values.each { |n|
2997             if nperrow_group
2998                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2999             else
3000                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3001             end
3002             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3003             nperrowradios.add(Gtk::Label.new('  '))
3004             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3005                 rb.active = true
3006             end
3007             nperrows << { :widget => rb, :value => n.to_s }
3008         }
3009         nperrowradios.show_all
3010     }
3011     recreate_theme_config.call
3012
3013     theme_button.signal_connect('clicked') {
3014         if newtheme = theme_choose(theme_button.label)
3015             limit_sizes = nil
3016             nperrow = nil
3017             theme_button.label = newtheme
3018             recreate_theme_config.call
3019         end
3020     }
3021
3022     dialog.vbox.add(frame1)
3023     dialog.vbox.add(frame2)
3024     dialog.window_position = Gtk::Window::POS_MOUSE
3025     dialog.show_all
3026
3027     keepon = true
3028     ok = true
3029     while keepon
3030         dialog.run { |response|
3031             if response == Gtk::Dialog::RESPONSE_OK
3032                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3033                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3034                 else
3035                     keepon = false
3036                 end
3037             else
3038                 keepon = ok = false
3039             end
3040         }
3041     end
3042     save_theme = theme_button.label
3043     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3044     save_opt432 = optimize432.active?
3045     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3046     save_madewith = madewithentry.text
3047     dialog.destroy
3048
3049     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3050         mark_document_as_dirty
3051         save_current_file
3052         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3053                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3054                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3055                      utf8(_("Please wait while scanning source directory...")),
3056                      'full scan',
3057                      { :closure_after => proc {
3058                              open_file($filename)
3059                              $modified = true
3060                          } })
3061     end
3062 end
3063
3064 def merge_current
3065     save_current_file
3066
3067     sel = $albums_tv.selection.selected_rows
3068
3069     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3070                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3071                  utf8(_("Please wait while scanning source directory...")),
3072                  'one dir scan',
3073                  { :closure_after => proc {
3074                          open_file($filename)
3075                          $albums_tv.selection.select_path(sel[0])
3076                          $modified = true
3077                      } })
3078 end
3079
3080 def merge_newsubs
3081     save_current_file
3082
3083     sel = $albums_tv.selection.selected_rows
3084
3085     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3086                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3087                  utf8(_("Please wait while scanning source directory...")),
3088                  'subdirs scan',
3089                  { :closure_after => proc {
3090                          open_file($filename)
3091                          $albums_tv.selection.select_path(sel[0])
3092                          $modified = true
3093                      } })
3094 end
3095
3096 def merge
3097     save_current_file
3098
3099     theme = $xmldoc.root.attributes['theme']
3100     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3101     if limit_sizes
3102         limit_sizes = "--sizes #{limit_sizes}"
3103     end
3104     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3105                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3106                  utf8(_("Please wait while scanning source directory...")),
3107                  'full scan',
3108                  { :closure_after => proc {
3109                          open_file($filename)
3110                          $modified = true
3111                      } })
3112 end
3113
3114 def save_as_do
3115     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3116                                     nil,
3117                                     Gtk::FileChooser::ACTION_SAVE,
3118                                     nil,
3119                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3120     fc.transient_for = $main_window
3121     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3122     fc.set_current_folder(File.expand_path("~/.booh"))
3123     fc.filename = $orig_filename
3124     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3125         $orig_filename = fc.filename
3126         if ! save_current_file_user
3127             fc.destroy
3128             return save_as_do
3129         end
3130         $config['last-opens'] ||= []
3131         $config['last-opens'] << $orig_filename
3132     end
3133     fc.destroy
3134 end
3135
3136 def preferences
3137     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3138                              $main_window,
3139                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3140                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3141                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3142
3143     dialog.vbox.add(notebook = Gtk::Notebook.new)
3144     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3145     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3146                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3147     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)),
3148                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3149     tooltips = Gtk::Tooltips.new
3150     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3151 for example: /usr/bin/mplayer %f")), nil)
3152     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3153                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3154     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3155                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3156     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3157 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3158     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3159                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3160     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)),
3161                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3162     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)
3163     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3164                0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3165     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)
3166     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3167                0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3168     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)
3169
3170     smp_check.signal_connect('toggled') {
3171         if smp_check.active?
3172             smp_hbox.sensitive = true
3173         else
3174             smp_hbox.sensitive = false
3175         end
3176     }
3177     if $config['mproc']
3178         smp_check.active = true
3179         smp_spin.value = $config['mproc'].to_i
3180     end
3181     nogestures_check.active = $config['nogestures']
3182     deleteondisk_check.active = $config['deleteondisk']
3183
3184     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3185     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3186                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3187     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3188                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3189     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3190                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3191     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3192                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3193     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3194                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195     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)
3196     commentsformat_help.signal_connect('clicked') {
3197         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3198 hence you should look at ImageMagick/identify documentation for the most    
3199 accurate and up-to-date documentation. Last time I checked, documentation
3200 was:
3201
3202 Print information about the image in a format of your choosing. You can
3203 include the image filename, type, width, height, Exif data, or other image
3204 attributes by embedding special format characters:                          
3205
3206                      %O   page offset
3207                      %P   page width and height                             
3208                      %b   file size                                         
3209                      %c   comment                                           
3210                      %d   directory                                         
3211                      %e   filename extension                                
3212                      %f   filename                                          
3213                      %g   page geometry                                     
3214                      %h   height                                            
3215                      %i   input filename                                    
3216                      %k   number of unique colors                           
3217                      %l   label                                             
3218                      %m   magick                                            
3219                      %n   number of scenes                                  
3220                      %o   output filename                                   
3221                      %p   page number                                       
3222                      %q   quantum depth                                     
3223                      %r   image class and colorspace                        
3224                      %s   scene number                                      
3225                      %t   top of filename                                   
3226                      %u   unique temporary filename                         
3227                      %w   width                                             
3228                      %x   x resolution                                      
3229                      %y   y resolution                                      
3230                      %z   image depth                                       
3231                      %@   bounding box                                      
3232                      %#   signature                                         
3233                      %%   a percent sign                                    
3234                                                                             
3235 For example,                                                                
3236                                                                             
3237     %m:%f %wx%h
3238                                                                             
3239 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3240 width is 512 and height is 480.                
3241                                                                             
3242 If the first character of string is @, the format is read from a file titled
3243 by the remaining characters in the string.
3244                                                                             
3245 You can also use the following special formatting syntax to print Exif
3246 information contained in the file:
3247                                                                             
3248     %[EXIF:tag]                                                             
3249                                                                             
3250 Where tag can be one of the following:                                      
3251                                                                             
3252     *  (print all Exif tags, in keyword=data format)                        
3253     !  (print all Exif tags, in tag_number data format)                     
3254     #hhhh (print data for Exif tag #hhhh)                                   
3255     ImageWidth                                                              
3256     ImageLength                                                             
3257     BitsPerSample                                                           
3258     Compression                                                             
3259     PhotometricInterpretation                                               
3260     FillOrder                                                               
3261     DocumentName                                                            
3262     ImageDescription                                                        
3263     Make                                                                    
3264     Model                                                                   
3265     StripOffsets                                                            
3266     Orientation                                                             
3267     SamplesPerPixel                                                         
3268     RowsPerStrip                                                            
3269     StripByteCounts                                                         
3270     XResolution                                                             
3271     YResolution                                                             
3272     PlanarConfiguration                                                     
3273     ResolutionUnit                                                          
3274     TransferFunction                                                        
3275     Software                                                                
3276     DateTime                                                                
3277     Artist                                                                  
3278     WhitePoint                                                              
3279     PrimaryChromaticities                                                   
3280     TransferRange                                                           
3281     JPEGProc                                                                
3282     JPEGInterchangeFormat                                                   
3283     JPEGInterchangeFormatLength                                             
3284     YCbCrCoefficients                                                       
3285     YCbCrSubSampling                                                        
3286     YCbCrPositioning                                                        
3287     ReferenceBlackWhite                                                     
3288     CFARepeatPatternDim                                                     
3289     CFAPattern                                                              
3290     BatteryLevel                                                            
3291     Copyright                                                               
3292     ExposureTime                                                            
3293     FNumber                                                                 
3294     IPTC/NAA                                                                
3295     ExifOffset                                                              
3296     InterColorProfile                                                       
3297     ExposureProgram                                                         
3298     SpectralSensitivity                                                     
3299     GPSInfo                                                                 
3300     ISOSpeedRatings                                                         
3301     OECF                                                                    
3302     ExifVersion                                                             
3303     DateTimeOriginal                                                        
3304     DateTimeDigitized                                                       
3305     ComponentsConfiguration                                                 
3306     CompressedBitsPerPixel                                                  
3307     ShutterSpeedValue                                                       
3308     ApertureValue                                                           
3309     BrightnessValue                                                         
3310     ExposureBiasValue                                                       
3311     MaxApertureValue                                                        
3312     SubjectDistance                                                         
3313     MeteringMode                                                            
3314     LightSource                                                             
3315     Flash                                                                   
3316     FocalLength                                                             
3317     MakerNote                                                               
3318     UserComment                                                             
3319     SubSecTime                                                              
3320     SubSecTimeOriginal                                                      
3321     SubSecTimeDigitized                                                     
3322     FlashPixVersion                                                         
3323     ColorSpace                                                              
3324     ExifImageWidth                                                          
3325     ExifImageLength                                                         
3326     InteroperabilityOffset                                                  
3327     FlashEnergy                                                             
3328     SpatialFrequencyResponse                                                
3329     FocalPlaneXResolution                                                   
3330     FocalPlaneYResolution                                                   
3331     FocalPlaneResolutionUnit                                                
3332     SubjectLocation                                                         
3333     ExposureIndex                                                           
3334     SensingMethod                                                           
3335     FileSource                                                              
3336     SceneType")), { :scrolled => true })
3337     }
3338
3339     dialog.vbox.show_all
3340     dialog.run { |response|
3341         if response == Gtk::Dialog::RESPONSE_OK
3342             $config['video-viewer'] = video_viewer_entry.text
3343             $config['browser'] = browser_entry.text
3344             if smp_check.active?
3345                 $config['mproc'] = smp_spin.value.to_i
3346             else
3347                 $config.delete('mproc')
3348             end
3349             $config['nogestures'] = nogestures_check.active?
3350             $config['deleteondisk'] = deleteondisk_check.active?
3351
3352             $config['convert-enhance'] = enhance_entry.text
3353             $config['comments-format'] = commentsformat_entry.text.gsub(/'/, '')
3354         end
3355     }
3356     dialog.destroy
3357 end
3358
3359 def perform_undo
3360     if $undo_tb.sensitive?
3361         $redo_tb.sensitive = $redo_mb.sensitive = true
3362         if not more_undoes = UndoHandler.undo($statusbar)
3363             $undo_tb.sensitive = $undo_mb.sensitive = false
3364         end
3365     end
3366 end
3367
3368 def perform_redo
3369     if $redo_tb.sensitive?
3370         $undo_tb.sensitive = $undo_mb.sensitive = true
3371         if not more_redoes = UndoHandler.redo($statusbar)
3372             $redo_tb.sensitive = $redo_mb.sensitive = false
3373         end
3374     end
3375 end
3376
3377 def show_one_click_explanation(intro)
3378     show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3379
3380 %s When such a tool is activated
3381 (<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
3382 on a thumbnail will immediately apply the desired action.
3383
3384 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3385 ") % intro), { :pos_centered => true })
3386 end
3387
3388 def get_license
3389     return <<"EOF"
3390                     GNU GENERAL PUBLIC LICENSE
3391                        Version 2, June 1991
3392
3393  Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3394                           675 Mass Ave, Cambridge, MA 02139, USA
3395  Everyone is permitted to copy and distribute verbatim copies
3396  of this license document, but changing it is not allowed.
3397
3398                             Preamble
3399
3400   The licenses for most software are designed to take away your
3401 freedom to share and change it.  By contrast, the GNU General Public
3402 License is intended to guarantee your freedom to share and change free
3403 software--to make sure the software is free for all its users.  This
3404 General Public License applies to most of the Free Software