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