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