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