*** empty log message ***
[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 open_file(filename)
2707
2708     $filename = nil
2709     $modified = false
2710     $current_path = nil   #- invalidate
2711     $modified_pixbufs = {}
2712     $albums_ts.clear
2713     $autotable.clear
2714     $subalbums_vb.children.each { |chld|
2715         $subalbums_vb.remove(chld)
2716     }
2717
2718     if !File.exists?(filename)
2719         return utf8(_("File not found."))
2720     end
2721
2722     begin
2723         $xmldoc = REXML::Document.new File.new(filename)
2724     rescue Exception
2725         $xmldoc = nil
2726     end
2727
2728     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2729         if entry2type(filename).nil?
2730             return utf8(_("Not a booh file!"))
2731         else
2732             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."))
2733         end
2734     end
2735
2736     if !source = $xmldoc.root.attributes['source']
2737         return utf8(_("Corrupted booh file..."))
2738     end
2739
2740     if !dest = $xmldoc.root.attributes['destination']
2741         return utf8(_("Corrupted booh file..."))
2742     end
2743
2744     if !theme = $xmldoc.root.attributes['theme']
2745         return utf8(_("Corrupted booh file..."))
2746     end
2747
2748     if $xmldoc.root.attributes['version'] < '0.8.4'
2749         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2750         mark_document_as_dirty
2751         if $xmldoc.root.attributes['version'] < '0.8.4'
2752             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2753             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2754                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2755                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2756                 if old_dest_dir != new_dest_dir
2757                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2758                 end
2759                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2760                     xmldir.elements.each { |element|
2761                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2762                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2763                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2764                             Dir[old_name + '*'].each { |file|
2765                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2766                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2767                             }
2768                         end
2769                         if element.name == 'dir' && !element.attributes['deleted']
2770                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2771                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2772                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2773                         end
2774                     }
2775                 else
2776                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2777                 end
2778             }
2779         end
2780         $xmldoc.root.add_attribute('version', $VERSION)
2781     end
2782
2783     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2784     optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2785     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2786
2787     $filename = filename
2788     select_theme(theme, limit_sizes, optimizefor32, nperrow)
2789     $default_size['thumbnails'] =~ /(.*)x(.*)/
2790     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2791     $albums_thumbnail_size =~ /(.*)x(.*)/
2792     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2793
2794     populate_subalbums_treeview(true)
2795
2796     $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
2797     return nil
2798 end
2799
2800 def open_file_user(filename)
2801     result = open_file(filename)
2802     if !result
2803         $config['last-opens'] ||= []
2804         if $config['last-opens'][-1] != utf8(filename)
2805             $config['last-opens'] << utf8(filename)
2806         end
2807         $orig_filename = $filename
2808         tmp = Tempfile.new("boohtemp")
2809         tmp.close!
2810         #- for security
2811         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2812         ios.close
2813         $tempfiles << $filename << "#{$filename}.backup"
2814     else
2815         $orig_filename = nil
2816     end
2817     return result
2818 end
2819
2820 def open_file_popup
2821     if !ask_save_modifications(utf8(_("Save this album?")),
2822                                utf8(_("Do you want to save the changes to this album?")),
2823                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2824         return
2825     end
2826     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2827                                     nil,
2828                                     Gtk::FileChooser::ACTION_OPEN,
2829                                     nil,
2830                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2831     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2832     fc.set_current_folder(File.expand_path("~/.booh"))
2833     fc.transient_for = $main_window
2834     ok = false
2835     while !ok
2836         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2837             push_mousecursor_wait(fc)
2838             msg = open_file_user(fc.filename)
2839             pop_mousecursor(fc)
2840             if msg
2841                 show_popup(fc, msg)
2842                 ok = false
2843             else
2844                 ok = true
2845             end
2846         else
2847             ok = true
2848         end
2849     end
2850     fc.destroy
2851 end
2852
2853 def open_url(url)
2854     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2855     msg 2, cmd
2856     system(cmd)
2857 end
2858
2859 def additional_booh_options
2860     options = ''
2861     if $config['mproc']
2862         options += "--mproc #{$config['mproc'].to_i} "
2863     end
2864     options += "--comments-format '#{$config['comments-format']}'"
2865     return options
2866 end
2867
2868 def new_album
2869     if !ask_save_modifications(utf8(_("Save this album?")),
2870                                utf8(_("Do you want to save the changes to this album?")),
2871                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2872         return
2873     end
2874     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2875                              $main_window,
2876                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2877                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2878                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2879     
2880     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2881     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2882                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2883     tbl.attach(src = Gtk::Entry.new,
2884                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2885     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2886                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2887     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2888                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2889     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2890                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2891     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2892                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2893     tbl.attach(dest = Gtk::Entry.new,
2894                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2895     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2896                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2897     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2898                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2899     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2900                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2901     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2902                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2903
2904     tooltips = Gtk::Tooltips.new
2905     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2906     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2907                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2908     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2909                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2910     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2911     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)
2912     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2913                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2914     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2915                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2916     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)
2917     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2918                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2919     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)
2920
2921     src_nb_calculated_for = ''
2922     src_nb_thread = nil
2923     process_src_nb = proc {
2924         if src.text != src_nb_calculated_for
2925             src_nb_calculated_for = src.text
2926             if src_nb_thread
2927                 Thread.kill(src_nb_thread)
2928                 src_nb_thread = nil
2929             end
2930             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2931                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2932             else
2933                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2934                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
2935                         src_nb_thread = Thread.new {
2936                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2937                             total = { 'image' => 0, 'video' => 0, nil => 0 }
2938                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2939                                 if File.basename(dir) =~ /^\./
2940                                     next
2941                                 else
2942                                     begin
2943                                         Dir.entries(dir.chomp).each { |file|
2944                                             total[entry2type(file)] += 1
2945                                         }
2946                                     rescue Errno::EACCES, Errno::ENOENT
2947                                     end
2948                                 end
2949                             }
2950                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2951                             src_nb_thread = nil
2952                         }
2953                     else
2954                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2955                     end
2956                 else
2957                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2958                 end
2959             end
2960         end
2961         true
2962     }
2963     timeout_src_nb = Gtk.timeout_add(100) {
2964         process_src_nb.call
2965     }
2966
2967     src_browse.signal_connect('clicked') {
2968         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2969                                         nil,
2970                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2971                                         nil,
2972                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2973         fc.transient_for = $main_window
2974         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2975             src.text = utf8(fc.filename)
2976             process_src_nb.call
2977             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2978         end
2979         fc.destroy
2980     }
2981
2982     dest_browse.signal_connect('clicked') {
2983         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2984                                         nil,
2985                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2986                                         nil,
2987                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2988         fc.transient_for = $main_window
2989         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2990             dest.text = utf8(fc.filename)
2991         end
2992         fc.destroy
2993     }
2994
2995     conf_browse.signal_connect('clicked') {
2996         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2997                                         nil,
2998                                         Gtk::FileChooser::ACTION_SAVE,
2999                                         nil,
3000                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3001         fc.transient_for = $main_window
3002         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3003         fc.set_current_folder(File.expand_path("~/.booh"))
3004         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3005             conf.text = utf8(fc.filename)
3006         end
3007         fc.destroy
3008     }
3009
3010     theme_sizes = []
3011     nperrows = []
3012     recreate_theme_config = proc {
3013         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3014         theme_sizes = []
3015         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3016         $images_size.each { |s|
3017             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3018             if !s['optional']
3019                 cb.active = true
3020             end
3021             tooltips.set_tip(cb, utf8(s['description']), nil)
3022             theme_sizes << { :widget => cb, :value => s['name'] }
3023         }
3024         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3025         tooltips = Gtk::Tooltips.new
3026         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3027         theme_sizes << { :widget => cb, :value => 'original' }
3028         sizes.show_all
3029
3030         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3031         nperrow_group = nil
3032         nperrows = []
3033         $allowed_N_values.each { |n|
3034             if nperrow_group
3035                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3036             else
3037                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3038             end
3039             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3040             if $default_N == n
3041                 rb.active = true
3042             end
3043             nperrows << { :widget => rb, :value => n }
3044         }
3045         nperrowradios.show_all
3046     }
3047     recreate_theme_config.call
3048
3049     theme_button.signal_connect('clicked') {
3050         if newtheme = theme_choose(theme_button.label)
3051             theme_button.label = newtheme
3052             recreate_theme_config.call
3053         end
3054     }
3055
3056     dialog.vbox.add(frame1)
3057     dialog.vbox.add(frame2)
3058     dialog.window_position = Gtk::Window::POS_MOUSE
3059     dialog.show_all
3060
3061     keepon = true
3062     ok = true
3063     while keepon
3064         dialog.run { |response|
3065             if response == Gtk::Dialog::RESPONSE_OK
3066                 srcdir = from_utf8_safe(src.text)
3067                 destdir = from_utf8_safe(dest.text)
3068                 confpath = from_utf8_safe(conf.text)
3069                 if src.text != '' && srcdir == ''
3070                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3071                     src.grab_focus
3072                 elsif !File.directory?(srcdir)
3073                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3074                     src.grab_focus
3075                 elsif dest.text != '' && destdir == ''
3076                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3077                     dest.grab_focus
3078                 elsif destdir != make_dest_filename(destdir)
3079                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3080                     dest.grab_focus
3081                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3082                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3083                     dest.grab_focus
3084                 elsif File.exists?(destdir) && !File.directory?(destdir)
3085                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3086                     dest.grab_focus
3087                 elsif conf.text == ''
3088                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3089                     conf.grab_focus
3090                 elsif conf.text != '' && confpath == ''
3091                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3092                     conf.grab_focus
3093                 elsif File.directory?(confpath)
3094                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3095                     conf.grab_focus
3096                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3097                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3098                 else
3099                     system("mkdir '#{destdir}'")
3100                     if !File.directory?(destdir)
3101                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3102                         dest.grab_focus
3103                     else
3104                         keepon = false
3105                     end
3106                 end
3107             else
3108                 keepon = ok = false
3109             end
3110         }
3111     end
3112     if ok
3113         srcdir = from_utf8(src.text)
3114         destdir = from_utf8(dest.text)
3115         configskel = File.expand_path(from_utf8(conf.text))
3116         theme = theme_button.label
3117         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3118         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3119         opt432 = optimize432.active?
3120         madewith = madewithentry.text
3121         indexlink = indexlinkentry.text
3122     end
3123     if src_nb_thread
3124         Thread.kill(src_nb_thread)
3125         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3126     end
3127     dialog.destroy
3128     Gtk.timeout_remove(timeout_src_nb)
3129
3130     if ok
3131         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3132                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3133                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3134                      utf8(_("Please wait while scanning source directory...")),
3135                      'full scan',
3136                      { :closure_after => proc { open_file_user(configskel) } })
3137     end
3138 end
3139
3140 def properties
3141     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3142                              $main_window,
3143                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3144                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3145                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3146     
3147     source = $xmldoc.root.attributes['source']
3148     dest = $xmldoc.root.attributes['destination']
3149     theme = $xmldoc.root.attributes['theme']
3150     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3151     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3152     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3153     if limit_sizes
3154         limit_sizes = limit_sizes.split(/,/)
3155     end
3156     madewith = $xmldoc.root.attributes['made-with']
3157     indexlink = $xmldoc.root.attributes['index-link']
3158
3159     tooltips = Gtk::Tooltips.new
3160     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3161     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3162                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3163     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3164                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3165     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3166                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3167     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3168                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3169     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3170                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3171     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3172                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3173
3174     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3175     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3176                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3177     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3178                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3179     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3180     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)
3181     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3182                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3183
3184     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3185                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3186     if indexlink
3187         indexlinkentry.text = indexlink
3188     end
3189     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)
3190     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3191                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3192     if madewith
3193         madewithentry.text = madewith
3194     end
3195     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)
3196
3197     theme_sizes = []
3198     nperrows = []
3199     recreate_theme_config = proc {
3200         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3201         theme_sizes = []
3202         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3203
3204         $images_size.each { |s|
3205             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3206             if limit_sizes
3207                 if limit_sizes.include?(s['name'])
3208                     cb.active = true
3209                 end
3210             else
3211                 if !s['optional']
3212                     cb.active = true
3213                 end
3214             end
3215             tooltips.set_tip(cb, utf8(s['description']), nil)
3216             theme_sizes << { :widget => cb, :value => s['name'] }
3217         }
3218         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3219         tooltips = Gtk::Tooltips.new
3220         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3221         if limit_sizes && limit_sizes.include?('original')
3222             cb.active = true
3223         end
3224         theme_sizes << { :widget => cb, :value => 'original' }
3225         sizes.show_all
3226
3227         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3228         nperrow_group = nil
3229         nperrows = []
3230         $allowed_N_values.each { |n|
3231             if nperrow_group
3232                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3233             else
3234                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3235             end
3236             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3237             nperrowradios.add(Gtk::Label.new('  '))
3238             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3239                 rb.active = true
3240             end
3241             nperrows << { :widget => rb, :value => n.to_s }
3242         }
3243         nperrowradios.show_all
3244     }
3245     recreate_theme_config.call
3246
3247     theme_button.signal_connect('clicked') {
3248         if newtheme = theme_choose(theme_button.label)
3249             limit_sizes = nil
3250             nperrow = nil
3251             theme_button.label = newtheme
3252             recreate_theme_config.call
3253         end
3254     }
3255
3256     dialog.vbox.add(frame1)
3257     dialog.vbox.add(frame2)
3258     dialog.window_position = Gtk::Window::POS_MOUSE
3259     dialog.show_all
3260
3261     keepon = true
3262     ok = true
3263     while keepon
3264         dialog.run { |response|
3265             if response == Gtk::Dialog::RESPONSE_OK
3266                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3267                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3268                 else
3269                     keepon = false
3270                 end
3271             else
3272                 keepon = ok = false
3273             end
3274         }
3275     end
3276     save_theme = theme_button.label
3277     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3278     save_opt432 = optimize432.active?
3279     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3280     save_madewith = madewithentry.text
3281     save_indexlink = indexlinkentry.text
3282     dialog.destroy
3283
3284     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3285         mark_document_as_dirty
3286         save_current_file
3287         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3288                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3289                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3290                      utf8(_("Please wait while scanning source directory...")),
3291                      'full scan',
3292                      { :closure_after => proc {
3293                              open_file($filename)
3294                              $modified = true
3295                          } })
3296     end
3297 end
3298
3299 def merge_current
3300     save_current_file
3301
3302     sel = $albums_tv.selection.selected_rows
3303
3304     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3305                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3306                  utf8(_("Please wait while scanning source directory...")),
3307                  'one dir scan',
3308                  { :closure_after => proc {
3309                          open_file($filename)
3310                          $albums_tv.selection.select_path(sel[0])
3311                          $modified = true
3312                      } })
3313 end
3314
3315 def merge_newsubs
3316     save_current_file
3317
3318     sel = $albums_tv.selection.selected_rows
3319
3320     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3321                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3322                  utf8(_("Please wait while scanning source directory...")),
3323                  'subdirs scan',
3324                  { :closure_after => proc {
3325                          open_file($filename)
3326                          $albums_tv.selection.select_path(sel[0])
3327                          $modified = true
3328                      } })
3329 end
3330
3331 def merge
3332     save_current_file
3333
3334     theme = $xmldoc.root.attributes['theme']
3335     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3336     if limit_sizes
3337         limit_sizes = "--sizes #{limit_sizes}"
3338     end
3339     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3340                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3341                  utf8(_("Please wait while scanning source directory...")),
3342                  'full scan',
3343                  { :closure_after => proc {
3344                          open_file($filename)
3345                          $modified = true
3346                      } })
3347 end
3348
3349 def save_as_do
3350     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3351                                     nil,
3352                                     Gtk::FileChooser::ACTION_SAVE,
3353                                     nil,
3354                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3355     fc.transient_for = $main_window
3356     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3357     fc.set_current_folder(File.expand_path("~/.booh"))
3358     fc.filename = $orig_filename
3359     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3360         $orig_filename = fc.filename
3361         if ! save_current_file_user
3362             fc.destroy
3363             return save_as_do
3364         end
3365         $config['last-opens'] ||= []
3366         $config['last-opens'] << $orig_filename
3367     end
3368     fc.destroy
3369 end
3370
3371 def preferences
3372     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3373                              $main_window,
3374                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3375                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3376                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3377
3378     dialog.vbox.add(notebook = Gtk::Notebook.new)
3379     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3380     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3381                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3382     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)),
3383                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3384     tooltips = Gtk::Tooltips.new
3385     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3386 for example: /usr/bin/mplayer %f")), nil)
3387     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3388                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3389     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3390                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3391     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3392 for example: /usr/bin/gimp-remote %f")), nil)
3393     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3394                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3395     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3396                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3397     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3398 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3399     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3400                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3401     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)),
3402                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3403     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)
3404     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3405                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3406     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)
3407     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3408                0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3409     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)
3410
3411     smp_check.signal_connect('toggled') {
3412         if smp_check.active?
3413             smp_hbox.sensitive = true
3414         else
3415             smp_hbox.sensitive = false
3416         end
3417     }
3418     if $config['mproc']
3419         smp_check.active = true
3420         smp_spin.value = $config['mproc'].to_i
3421     end
3422     nogestures_check.active = $config['nogestures']
3423     deleteondisk_check.active = $config['deleteondisk']
3424
3425     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3426     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3427                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3428     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3429                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3431                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3432     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3433                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3435                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3436     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)
3437     commentsformat_help.signal_connect('clicked') {
3438         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3439 hence you should look at ImageMagick/identify documentation for the most    
3440 accurate and up-to-date documentation. Last time I checked, documentation
3441 was:
3442
3443 Print information about the image in a format of your choosing. You can
3444 include the image filename, type, width, height, Exif data, or other image
3445 attributes by embedding special format characters:                          
3446
3447                      %O   page offset
3448                      %P   page width and height                             
3449                      %b   file size                                         
3450                      %c   comment                                           
3451                      %d   directory                                         
3452                      %e   filename extension                                
3453                      %f   filename                                          
3454                      %g   page geometry                                     
3455                      %h   height                                            
3456                      %i   input filename                                    
3457                      %k   number of unique colors                           
3458                      %l   label                                             
3459                      %m   magick                                            
3460                      %n   number of scenes                                  
3461                      %o   output filename                                   
3462                      %p   page number                                       
3463                      %q   quantum depth                                     
3464                      %r   image class and colorspace                        
3465                      %s   scene number                                      
3466                      %t   top of filename                                   
3467                      %u   unique temporary filename                         
3468                      %w   width                                             
3469                      %x   x resolution                                      
3470                      %y   y resolution                                      
3471                      %z   image depth                                       
3472                      %@   bounding box                                      
3473                      %#   signature