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