handle save of modifications when opening a new file
[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 <gc3 at bluewin.ch>
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
25 require 'gtk2'
26 require 'booh/gtkadds'
27 require 'booh/GtkAutoTable'
28
29 require 'gettext'
30 include GetText
31 bindtextdomain("booh")
32
33 require 'rexml/document'
34 include REXML
35
36 require 'booh/booh-lib'
37 include Booh
38 require 'booh/UndoHandler'
39
40
41 #- options
42 $options = [
43     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
44
45     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 ]
47
48 def usage
49     puts _("Usage: %s [OPTION]...") % File.basename($0)
50     $options.each { |ary|
51         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
52     }
53 end
54
55 def handle_options
56     parser = GetoptLong.new
57     parser.set_options(*$options.collect { |ary| ary[0..2] })
58     begin
59         parser.each_option do |name, arg|
60             case name
61             when '--help'
62                 usage
63                 exit(0)
64
65             when '--verbose-level'
66                 $verbose_level = arg.to_i
67
68             end
69         end
70     rescue
71         puts $!
72         usage
73         exit(1)
74     end
75 end
76
77 def read_config
78     $config = {}
79     $config_file = File.expand_path('~/.booh-gui-rc')
80     if File.readable?($config_file)
81         $xmldoc = REXML::Document.new(File.new($config_file))
82         $xmldoc.root.elements.each { |element|
83             txt = element.get_text
84             if txt 
85                 if txt.value =~ /~~~/ || element.name == 'last-opens'
86                     $config[element.name] = txt.value.split(/~~~/)
87                 else
88                     $config[element.name] = txt.value
89                 end
90             else
91                 $config[element.name] = {}
92                 element.each { |chld|
93                     txt = chld.get_text
94                     $config[element.name][chld.name] = txt ? txt.value : nil
95                 }
96             end
97         }
98     end
99     $config['video-viewer'] ||= 'mplayer %f'
100     if !FileTest.directory?(File.expand_path('~/.booh'))
101         system("mkdir ~/.booh")
102     end
103 end
104
105 def write_config
106     if $config['last-opens'] && $config['last-opens'].size > 5
107         $config['last-opens'] = $config['last-opens'][-5, 5]
108     end
109
110     ios = File.open($config_file, "w")
111     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
112     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
113     $config.each_pair { |key, value|
114         elem = $xmldoc.root.add_element key
115         if value.is_a? Hash
116             $config[key].each_pair { |subkey, subvalue|
117                 subelem = elem.add_element subkey
118                 subelem.add_text subvalue.to_s
119             }
120         elsif value.is_a? Array
121             elem.add_text value.join('~~~')
122         else
123             elem.add_text value.to_s
124         end
125     }
126     $xmldoc.write(ios, 0)
127     ios.close
128 end
129
130 def set_mousecursor(what, *widget)
131     if widget[0] && widget[0].window
132         widget[0].window.set_cursor(Gdk::Cursor.new(what))
133     end
134     if $main_window.window
135         $main_window.window.set_cursor(Gdk::Cursor.new(what))
136     end
137     $current_cursor = what
138 end
139 def set_mousecursor_wait(*widget)
140     set_mousecursor(Gdk::Cursor::WATCH, *widget)
141     if Thread.current == Thread.main
142         Gtk.main_iteration while Gtk.events_pending?
143     end
144 end
145 def set_mousecursor_normal(*widget)
146     set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
147 end
148 def push_mousecursor_wait(*widget)
149     if $current_cursor != Gdk::Cursor::WATCH
150         $save_cursor = $current_cursor
151         set_mousecursor_wait(*widget)
152     end
153 end
154 def pop_mousecursor(*widget)
155     set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
156 end
157
158 def current_dest_dir
159     source = $xmldoc.root.attributes['source']
160     dest = $xmldoc.root.attributes['destination']
161     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
162 end
163
164 def full_src_dir_to_rel(path)
165     source = from_utf8($xmldoc.root.attributes['source'])
166     return path.sub(/^#{Regexp.quote(source)}/, '')
167 end
168
169 def build_full_dest_filename(filename)
170     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
171 end
172
173 def save_undo(name, closure, *params)
174     UndoHandler.save_undo(name, closure, [ *params ])
175     $undo_tb.sensitive = $undo_mb.sensitive = true
176 end
177
178 def view_element(filename, closures)
179     if entry2type(filename) == 'video'
180         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'")
181         msg 2, cmd
182         system(cmd)
183         return
184     end
185
186     w = Gtk::Window.new.set_title(filename)
187
188     msg 3, "filename: #{filename}"
189     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
190     #- typically this file won't exist in case of videos; try with the largest thumbnail around
191     if !File.exists?(dest_img)
192         if entry2type(filename) == 'video'
193             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
194             if not alternatives.empty?
195                 dest_img = alternatives[-1]
196             end
197         else
198             push_mousecursor_wait
199             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
200             pop_mousecursor
201             if !File.exists?(dest_img)
202                 msg 2, _("Could not generate fullscreen thumbnail!")
203                 return
204                 end
205         end
206     end
207     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)))
208     evt.signal_connect('button-press-event') { |this, event|
209         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
210             menu = Gtk::Menu.new
211             menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
212             delete_item.signal_connect('activate') {
213                 w.destroy
214                 closures[:delete].call
215             }
216             menu.show_all
217             menu.popup(nil, nil, event.button, event.time)
218         end
219     }
220     tooltips = Gtk::Tooltips.new
221     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
222
223     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
224     b.signal_connect('clicked') { w.destroy }
225
226     vb = Gtk::VBox.new
227     vb.pack_start(evt, false, false)
228     vb.pack_end(bottom, false, false)
229
230     w.add(vb)
231     w.signal_connect('delete-event') { w.destroy }
232     w.window_position = Gtk::Window::POS_CENTER
233     w.show_all
234 end
235
236 def create_editzone(scrolledwindow, pagenum, image)
237     frame = Gtk::Frame.new
238     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
239     frame.set_shadow_type(Gtk::SHADOW_IN)
240     textview.signal_connect('key-press-event') { |w, event|
241         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
242         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
243             scrolledwindow.signal_emit('key-press-event', event)
244         end
245         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
246            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
247             if event.keyval == Gdk::Keyval::GDK_Up
248                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
249                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
250                 else
251                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
252                 end
253             else
254                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
255                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
256                 else
257                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
258                 end
259             end
260         end
261         false  #- propagate
262     }
263     textview.signal_connect('focus-in-event') { |w, event|
264         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
265         false  #- propagate
266     }
267
268     candidate_undo_text = nil
269     textview.signal_connect('focus-in-event') { |w, event|
270         candidate_undo_text = textview.buffer.text
271         false  #- propagate
272     }
273     textview.signal_connect('key-release-event') { |w, event|
274         if candidate_undo_text && candidate_undo_text != textview.buffer.text
275             $modified = true
276             save_undo(_("text edit"),
277                       Proc.new { |text|
278                           save_text = textview.buffer.text
279                           textview.buffer.text = text
280                           textview.grab_focus
281                           $notebook.set_page(pagenum)
282                           Proc.new {
283                               textview.buffer.text = save_text
284                               textview.grab_focus
285                               $notebook.set_page(pagenum)
286                           }
287                       }, candidate_undo_text)
288             candidate_undo_text = nil
289         end
290
291         if ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
292             #- autoscroll if cursor or image is not visible
293             ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
294             ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
295             current_miny_visible = scrolledwindow.vadjustment.value
296             current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
297             if ypos_top < current_miny_visible
298                 newval = scrolledwindow.vadjustment.value -
299                          ((current_miny_visible - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
300                 if newval < scrolledwindow.vadjustment.lower
301                     newval = scrolledwindow.vadjustment.lower
302                 end
303                 scrolledwindow.vadjustment.value = newval
304             elsif ypos_bottom > current_maxy_visible
305                 newval = scrolledwindow.vadjustment.value +
306                          ((ypos_bottom - current_maxy_visible - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
307                 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
308                     newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
309                 end
310                 scrolledwindow.vadjustment.value = newval
311             end
312         end
313         false  #- propagate
314     }
315
316     return [ frame, textview ]
317 end
318
319 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
320
321     if !$modified_pixbufs[thumbnail_img]
322         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
323     elsif !$modified_pixbufs[thumbnail_img][:orig]
324         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
325     end
326
327     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
328
329     #- rotate
330     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
331         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
332         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
333         if pixbuf.height > desired_y
334             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
335         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
336             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
337         end
338     end
339
340     #- fix white balance
341     if $modified_pixbufs[thumbnail_img][:whitebalance]
342         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
343     end
344
345     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
346 end
347
348 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
349     $modified = true
350
351     #- update rotate attribute
352     xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
353
354     $modified_pixbufs[thumbnail_img] ||= {}
355     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
356     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
357
358     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
359 end
360
361 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
362     $modified = true
363
364     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
365
366     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
367               Proc.new { |angle|
368                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
369                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
370                   Proc.new {
371                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
372                       $notebook.set_page(0)
373                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
374                   }
375               }, -angle)
376 end
377
378 def color_swap(xmldir, attributes_prefix)
379     $modified = true
380     if xmldir.attributes["#{attributes_prefix}color-swap"]
381         xmldir.delete_attribute("#{attributes_prefix}color-swap")
382     else
383         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
384     end
385 end
386
387 def enhance(xmldir, attributes_prefix)
388     $modified = true
389     if xmldir.attributes["#{attributes_prefix}enhance"]
390         xmldir.delete_attribute("#{attributes_prefix}enhance")
391     else
392         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
393     end
394 end
395
396 def change_frame_offset(xmldir, attributes_prefix, value)
397     $modified = true
398     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
399 end
400
401 def ask_new_frame_offset(xmldir, attributes_prefix)
402     value = xmldir.attributes["#{attributes_prefix}frame-offset"]
403
404     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
405                              $main_window,
406                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
407                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
408                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
409
410     lbl = Gtk::Label.new
411     lbl.markup = utf8(
412 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
413 from. There are approximately 25 frames per second in a video.
414 "))
415     dialog.vbox.add(lbl)
416     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
417     entry.signal_connect('key-press-event') { |w, event|
418         if event.keyval == Gdk::Keyval::GDK_Return
419             dialog.response(Gtk::Dialog::RESPONSE_OK)
420             true
421         elsif event.keyval == Gdk::Keyval::GDK_Escape
422             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
423             true
424         else
425             false  #- propagate if needed
426         end
427     }
428     
429     dialog.window_position = Gtk::Window::POS_MOUSE
430     dialog.show_all
431
432     dialog.run { |response|
433         newval = entry.text
434         dialog.destroy
435         if response == Gtk::Dialog::RESPONSE_OK
436             $modified = true
437             msg 3, "changing frame offset top #{newval}"
438             return { :old => value, :new => newval }
439         else
440             return nil
441         end
442     }
443 end
444
445 def change_whitebalance(xmlelem, attributes_prefix, value)
446     $modified = true
447     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
448 end
449
450 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
451
452     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
453     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
454         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
455         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
456         destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
457         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
458                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
459         $modified_pixbufs[thumbnail_img] ||= {}
460         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
461         system("rm -f '#{destfile}'")
462         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
463         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
464         if entry2type(orig) == 'video'
465             #- cleanup temp for videos
466             system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
467         end
468     end
469
470     $modified_pixbufs[thumbnail_img] ||= {}
471     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
472
473     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
474 end
475
476 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
477     #- init $modified_pixbufs correctly
478 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
479
480     value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
481
482     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
483                              $main_window,
484                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
485                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
486                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
487
488     lbl = Gtk::Label.new
489     lbl.markup = utf8(
490 _("You can fix the <b>white balance</b> of the image, if your image is too blue
491 or too yellow because your camera didn't detect the light correctly. Drag the
492 slider below the image to the left for more blue, to the right for more yellow.
493 "))
494     dialog.vbox.add(lbl)
495     dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
496     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
497     
498     dialog.window_position = Gtk::Window::POS_MOUSE
499     dialog.show_all
500
501     lastval = nil
502     thread = nil
503     timeout = Gtk.timeout_add(100) {
504         if hs.value != lastval
505             lastval = hs.value
506             if thread
507                 thread.kill
508             end
509             thread = Thread.new {
510                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
511             }
512         end
513         true
514     }
515
516     dialog.run { |response|
517         Gtk.timeout_remove(timeout)
518         if response == Gtk::Dialog::RESPONSE_OK
519             $modified = true
520             newval = hs.value.to_s
521             msg 3, "changing white balance to #{newval}"
522             dialog.destroy
523             return { :old => value, :new => newval }
524         else
525             $modified_pixbufs[thumbnail_img] ||= {}
526             $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
527             dialog.destroy
528             return nil
529         end
530     }
531 end
532
533 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
534     system("rm -f '#{destfile}'")
535     #- type can be 'element' or 'subdir'
536     if type == 'element'
537         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
538     else
539         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
540     end
541 end
542
543 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
544     Thread.new {
545         push_mousecursor_wait
546         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
547         img.set(destfile)
548         $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
549         if entry2type(origfile) == 'video'
550             #- cleanup temp for videos
551             system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
552         end
553         pop_mousecursor
554     }
555 end
556
557 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, possible_moves, closures)
558     menu = Gtk::Menu.new
559     if optionals.include?('change_image')
560         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
561         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
562         changeimg.signal_connect('activate') { closures[:change].call }
563         menu.append(            Gtk::SeparatorMenuItem.new)
564     end
565     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
566     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
567     r90.signal_connect('activate') { closures[:rotate].call(90) }
568     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
569     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
570     r270.signal_connect('activate') { closures[:rotate].call(-90) }
571     menu.append(               Gtk::SeparatorMenuItem.new)
572     if !possible_moves[:forbid_left]
573         menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
574         moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
575         moveleft.signal_connect('activate') { closures[:move].call('left') }
576         if !possible_moves[:can_left]
577             moveleft.sensitive = false
578         end
579     end
580     if !possible_moves[:forbid_right]
581         menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
582         moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
583         moveright.signal_connect('activate') { closures[:move].call('right') }
584         if !possible_moves[:can_right]
585             moveright.sensitive = false
586         end
587     end
588     menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
589     moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
590     moveup.signal_connect('activate') { closures[:move].call('up') }
591     if !possible_moves[:can_up]
592         moveup.sensitive = false
593     end
594     menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
595     movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
596     movedown.signal_connect('activate') { closures[:move].call('down') }
597     if !possible_moves[:can_down]
598         movedown.sensitive = false
599     end
600     if type == 'video'
601         menu.append(               Gtk::SeparatorMenuItem.new)
602         menu.append(  color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
603         color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
604         color_swap.signal_connect('activate') { closures[:color_swap].call }
605         menu.append(        flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
606         flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
607         flip.signal_connect('activate') { closures[:rotate].call(180) }
608         menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
609         frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
610         frame_offset.signal_connect('activate') { closures[:frame_offset].call }
611     end
612     menu.append(               Gtk::SeparatorMenuItem.new)
613     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
614     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
615     whitebalance.signal_connect('activate') { closures[:whitebalance].call }
616     menu.append(enhance      = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
617                                                                                                               _("Enhance constrast"))))
618     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
619     enhance.signal_connect('activate') { closures[:enhance].call }
620     if optionals.include?('delete')
621         menu.append(               Gtk::SeparatorMenuItem.new)
622         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
623         delete_item.signal_connect('activate') { closures[:delete].call }
624     end
625     menu.show_all
626     menu.popup(nil, nil, event.button, event.time)
627 end
628
629 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
630
631     img = nil
632     frame1 = Gtk::Frame.new
633
634     my_gen_real_thumbnail = proc {
635         gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
636     }
637
638     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
639     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
640         frame1.add(img = Gtk::Image.new)
641         my_gen_real_thumbnail.call
642     else
643         frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
644     end
645     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
646
647     tooltips = Gtk::Tooltips.new
648     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
649     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
650
651     frame2, textview = create_editzone($autotable_sw, 1, img)
652     textview.buffer.text = utf8(caption)
653     textview.set_justification(Gtk::Justification::CENTER)
654
655     vbox = Gtk::VBox.new(false, 5)
656     vbox.pack_start(evtbox, false, false)
657     vbox.pack_start(frame2, false, false)
658     autotable.append(vbox, filename)
659
660     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
661     $vbox2textview[vbox] = textview
662
663     #- to be able to find widgets by name
664     $name2widgets[filename] = { :textview => textview }
665
666     cleanup_all_thumbnails = Proc.new {
667         #- remove out of sync images
668         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
669         for sizeobj in $images_size
670             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
671         end
672
673     }
674
675     rotate_and_cleanup = Proc.new { |angle|
676         rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
677         cleanup_all_thumbnails.call
678     }
679
680     move = Proc.new { |direction|
681         do_method = "move_#{direction}"
682         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
683         perform = Proc.new {
684             done = autotable.method(do_method).call(vbox)
685             textview.grab_focus  #- because if moving, focus is stolen
686             done
687         }
688         if perform.call
689             save_undo(_("move %s") % direction,
690                       Proc.new {
691                           autotable.method(undo_method).call(vbox)
692                           $notebook.set_page(1)
693                           Proc.new {
694                               autotable.method(do_method).call(vbox)
695                               $notebook.set_page(1)
696                           }
697                       })
698         end
699     }
700
701     color_swap_and_cleanup = Proc.new {
702         perform_color_swap_and_cleanup = Proc.new {
703             color_swap($xmldir.elements["[@filename='#{filename}']"], '')
704             my_gen_real_thumbnail.call
705         }
706
707         cleanup_all_thumbnails.call
708         perform_color_swap_and_cleanup.call
709
710         save_undo(_("color swap"),
711                   Proc.new {
712                       perform_color_swap_and_cleanup.call
713                       $notebook.set_page(1)
714                       Proc.new {
715                           perform_color_swap_and_cleanup.call
716                           $notebook.set_page(1)
717                       }
718                   })
719     }
720
721     change_frame_offset_and_cleanup = Proc.new {
722         if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
723             perform_change_frame_offset_and_cleanup = Proc.new { |val|
724                 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
725                 my_gen_real_thumbnail.call
726             }
727             perform_change_frame_offset_and_cleanup.call(values[:new])
728
729             save_undo(_("specify frame offset"),
730                       Proc.new {
731                           perform_change_frame_offset_and_cleanup.call(values[:old])
732                           $notebook.set_page(1)
733                           Proc.new {
734                               perform_change_frame_offset_and_cleanup.call(values[:new])
735                               $notebook.set_page(1)
736                           }
737                       })
738         end
739     }
740
741     whitebalance_and_cleanup = Proc.new {
742         if values = ask_whitebalance(from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img,
743                                      $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
744             perform_change_whitebalance_and_cleanup = Proc.new { |val|
745                 change_whitebalance($xmldir.elements["[@filename='#{filename}']"], '', val)
746                 recalc_whitebalance(val, from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img,
747                                     $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
748                 cleanup_all_thumbnails.call
749             }
750             perform_change_whitebalance_and_cleanup.call(values[:new])
751
752             save_undo(_("fix white balance"),
753                       Proc.new {
754                           perform_change_whitebalance_and_cleanup.call(values[:old])
755                           $notebook.set_page(1)
756                           Proc.new {
757                               perform_change_whitebalance_and_cleanup.call(values[:new])
758                               $notebook.set_page(1)
759                           }
760                       })
761         end
762     }
763
764     enhance_and_cleanup = Proc.new {
765         perform_enhance_and_cleanup = Proc.new {
766             enhance($xmldir.elements["[@filename='#{filename}']"], '')
767             my_gen_real_thumbnail.call
768         }
769
770         cleanup_all_thumbnails.call
771         perform_enhance_and_cleanup.call
772
773         save_undo(_("enhance"),
774                   Proc.new {
775                       perform_enhance_and_cleanup.call
776                       $notebook.set_page(1)
777                       Proc.new {
778                           perform_enhance_and_cleanup.call
779                           $notebook.set_page(1)
780                       }
781                   })
782     }
783
784     delete = Proc.new {
785         if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
786             $modified = true
787             after = nil
788             perform_delete = Proc.new {
789                 after = autotable.get_next_widget(vbox)
790                 if !after
791                     after = autotable.get_previous_widget(vbox)
792                 end
793                 autotable.remove(vbox)
794                 if after
795                     $vbox2textview[after].grab_focus
796                 end
797             }
798             
799             perform_delete.call
800
801             if !after
802                 if $xmldir.elements['dir']
803                     $xmldir.delete_attribute('thumbnails-caption')
804                     $xmldir.delete_attribute('thumbnails-captionfile')
805                 else
806                     $xmldir.remove
807                 end
808                 save_changes('forced')
809                 populate_subalbums_treeview
810             else
811                 save_undo(_("delete"),
812                           Proc.new { |pos|
813                               autotable.reinsert(pos, vbox, filename)
814                               $notebook.set_page(1)
815                               Proc.new {
816                                   perform_delete.call
817                                   $notebook.set_page(1)
818                               }
819                           }, autotable.get_current_number(vbox))
820             end
821         end
822     }
823
824     textview.signal_connect('key-press-event') { |w, event|
825         propagate = true
826         if event.state != 0
827             x, y = autotable.get_current_pos(vbox)
828             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
829             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
830             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
831             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
832                 if control_pressed
833                     $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
834                 end
835                 if shift_pressed
836                     move.call('up')
837                 end
838             end
839             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
840                 if control_pressed
841                     $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
842                 end
843                 if shift_pressed
844                     move.call('down')
845                 end
846             end
847             if event.keyval == Gdk::Keyval::GDK_Left
848                 if x > 0
849                     if control_pressed
850                         $vbox2textview[previous].grab_focus
851                     end
852                     if shift_pressed
853                         move.call('left')
854                     end
855                 end
856                 if alt_pressed
857                     rotate_and_cleanup.call(-90)
858                 end
859             end
860             if event.keyval == Gdk::Keyval::GDK_Right
861                 next_ = autotable.get_next_widget(vbox)
862                 if next_ && autotable.get_current_pos(next_)[0] > x
863                     if control_pressed
864                         $vbox2textview[next_].grab_focus
865                     end
866                     if shift_pressed
867                         move.call('right')
868                     end
869                 end
870                 if alt_pressed
871                     rotate_and_cleanup.call(90)
872                 end
873             end
874             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
875                 delete.call
876             end
877             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
878                 view_element(filename, { :delete => delete })
879                 propagate = false
880             end
881         end
882         !propagate  #- propagate if needed
883     }
884
885     evtbox.signal_connect('button-press-event') { |w, event|
886         retval = true
887         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
888             shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
889             if $r90.active?
890                 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
891             elsif $r270.active?
892                 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
893             elsif $enhance.active?
894                 enhance_and_cleanup.call
895             elsif $delete.active?
896                 delete.call
897             else
898                 textview.grab_focus
899             end
900         end
901         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
902             x, y = autotable.get_current_pos(vbox)
903             next_ = autotable.get_next_widget(vbox)
904             popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
905                                  { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
906                                    :can_up => y > 0, :can_down => y < autotable.get_max_y },
907                                  { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
908                                    :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup })
909         end
910         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
911             view_element(filename, { :delete => delete })
912         else
913             retval = false  #- propagate
914         end
915         retval
916     }
917
918     vbox.signal_connect('button-press-event') { |w, event|
919         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
920             $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
921         end
922         false
923     }
924     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
925         if $gesture_press && $gesture_press[:filename] == filename
926             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
927                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
928                 msg 3, "gesture rotate: #{angle}"
929                 rotate_and_cleanup.call(angle)
930             end
931         end
932         $gesture_press = nil
933     }
934
935     #- handle reordering with drag and drop
936     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
937     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
938     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
939         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
940     }
941     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
942         ctxt.targets.each { |target|
943             if target.name == 'reorder-elements'
944                 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
945                 if from != to
946                     $modified = true
947                     autotable.move(from, to)
948                     save_undo(_("reorder"),
949                               Proc.new { |from, to|
950                                   if to > from
951                                       autotable.move(to - 1, from)
952                                   else
953                                       autotable.move(to, from + 1)
954                                   end
955                                   $notebook.set_page(1)
956                                   Proc.new {
957                                       autotable.move(from, to)
958                                       $notebook.set_page(1)
959                                   }
960                               }, from, to)
961                 end
962             end
963         }
964     }
965
966     vbox.show_all
967 end
968
969 def create_auto_table
970
971     $autotable = Gtk::AutoTable.new(5)
972
973     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
974     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
975     $autotable_sw.add_with_viewport($autotable)
976 end
977
978 def create_subalbums_page
979
980     subalbums_hb = Gtk::HBox.new
981     $subalbums_vb = Gtk::VBox.new(false, 5)
982     subalbums_hb.pack_start($subalbums_vb, false, false)
983     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
984     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
985     $subalbums_sw.add_with_viewport(subalbums_hb)
986 end
987
988 def save_current_file
989     save_changes
990
991     if $filename
992         ios = File.open($filename, "w")
993         $xmldoc.write(ios, 0)
994         ios.close
995         $modified = false
996     end
997 end
998
999 #- ret: true => ok  false => cancel
1000 def ask_save_modifications(msg1, msg2, *options)
1001     ret = true
1002     options = options.size > 0 ? options[0] : {}
1003     if $modified
1004         if options[:disallow_cancel]
1005             dialog = Gtk::Dialog.new(msg1,
1006                                      $main_window,
1007                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1008                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1009                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1010         else
1011             dialog = Gtk::Dialog.new(msg1,
1012                                      $main_window,
1013                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1014                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1015                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1016                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1017         end
1018         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1019         dialog.vbox.add(Gtk::Label.new(msg2))
1020         dialog.window_position = Gtk::Window::POS_CENTER
1021         dialog.show_all
1022         
1023         dialog.run { |response|
1024             dialog.destroy
1025             if response == Gtk::Dialog::RESPONSE_YES
1026                 save_current_file
1027             elsif response == Gtk::Dialog::RESPONSE_CANCEL
1028                 ret = false
1029             end
1030         }
1031     end
1032     return ret
1033 end
1034
1035 def try_quit(*options)
1036     if ask_save_modifications(utf8(_("Save before quitting?")),
1037                               utf8(_("Do you want to save your changes before quitting?")),
1038                               *options)
1039         Gtk.main_quit
1040     end
1041 end
1042
1043 def show_popup(parent, msg, *options)
1044     dialog = Gtk::Dialog.new
1045     dialog.title = utf8(_("Booh message"))
1046     lbl = Gtk::Label.new
1047     lbl.markup = msg
1048     if options[0] && options[0][:centered]
1049         lbl.set_justify(Gtk::Justification::CENTER)
1050     end
1051     dialog.vbox.add(lbl)
1052     if options[0] && options[0][:okcancel]
1053         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1054     end
1055     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1056
1057     dialog.transient_for = parent
1058     dialog.set_default_size(200, 120)
1059     if options[0] && options[0][:pos_centered]
1060         dialog.window_position = Gtk::Window::POS_CENTER
1061     else
1062         dialog.window_position = Gtk::Window::POS_MOUSE
1063     end
1064     dialog.show_all
1065
1066     dialog.run { |response|
1067         dialog.destroy
1068         if options[0] && options[0][:okcancel]
1069             return response == Gtk::Dialog::RESPONSE_OK
1070         end
1071     }
1072 end
1073
1074 def backend_wait_message(parent, msg, infopipe_path, mode)
1075     w = Gtk::Window.new
1076     w.set_transient_for(parent)
1077     w.modal = true
1078
1079     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1080     vb.pack_start(Gtk::Label.new(msg), false, false)
1081
1082     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1083     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1084     if mode != 'one dir scan'
1085         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1086     end
1087     if mode == 'web-album'
1088         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1089         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1090     end
1091     vb.pack_start(Gtk::HSeparator.new, false, false)
1092
1093     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1094     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1095     vb.pack_end(bottom, false, false)
1096
1097     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1098     refresh_thread = Thread.new {
1099         directories_counter = 0
1100         while line = infopipe.gets
1101             if line =~ /^directories: (\d+), sizes: (\d+)/
1102                 directories = $1.to_f + 1
1103                 sizes = $2.to_f
1104             elsif line =~ /^walking: (.+), (\d+) elements$/
1105                 elements = $2.to_f + 1
1106                 if mode == 'web-album'
1107                     elements += sizes
1108                 end
1109                 element_counter = 0
1110                 pb1_1.fraction = 0
1111                 if mode != 'one dir scan'
1112                     newtext = utf8(full_src_dir_to_rel($1))
1113                     newtext = '/' if newtext == ''
1114                     pb1_2.text = newtext
1115                     directories_counter += 1
1116                     pb1_2.fraction = directories_counter / directories
1117                 end
1118             elsif line =~ /^processing element$/
1119                 element_counter += 1
1120                 pb1_1.fraction = element_counter / elements
1121             elsif line =~ /^processing size$/
1122                 element_counter += 1
1123                 pb1_1.fraction = element_counter / elements
1124             elsif line =~ /^finished processing sizes$/
1125                 pb1_1.fraction = 1
1126             elsif line =~ /^creating index.html$/
1127                 pb1_2.text = utf8(_("finished"))
1128                 pb1_1.fraction = pb1_2.fraction = 1
1129                 directories_counter = 0
1130             elsif line =~ /^index.html: (.+)/
1131                 newtext = utf8(full_src_dir_to_rel($1))
1132                 newtext = '/' if newtext == ''
1133                 pb2.text = newtext
1134                 directories_counter += 1
1135                 pb2.fraction = directories_counter / directories
1136             end
1137         end
1138     }
1139
1140     w.add(vb)
1141     w.signal_connect('delete-event') { w.destroy }
1142     w.signal_connect('destroy') {
1143         Thread.kill(refresh_thread)
1144         if infopipe_path
1145             infopipe.close
1146             system("rm -f #{infopipe_path}")
1147         end
1148     }
1149     w.window_position = Gtk::Window::POS_CENTER
1150     w.show_all
1151
1152     return [ b, w ]
1153 end
1154
1155 def call_backend(cmd, waitmsg, mode, params)
1156     pipe = Tempfile.new("boohpipe")
1157     pipe.close!
1158     system("mkfifo #{pipe.path}")
1159     cmd += " --info-pipe #{pipe.path}"
1160     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1161     pid = nil
1162     Thread.new {
1163         msg 2, cmd
1164         if pid = fork
1165             id, exitstatus = Process.waitpid2(pid)
1166             w8.destroy
1167             if exitstatus == 0
1168                 if params[:successmsg]
1169                     show_popup($main_window, params[:successmsg])
1170                 end
1171                 if params[:closure_after]
1172                     params[:closure_after].call
1173                 end
1174             elsif exitstatus == 15
1175                 #- say nothing, user aborted
1176             else
1177                 if params[:failuremsg]
1178                     show_popup($main_window, params[:failuremsg])
1179                 end
1180             end
1181         else
1182             exec(cmd)
1183         end
1184     }
1185     button.signal_connect('clicked') {
1186         Process.kill('SIGTERM', pid)
1187     }
1188 end
1189
1190 def save_changes(*forced)
1191     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1192         return
1193     end
1194
1195     $xmldir.delete_attribute('already-generated')
1196
1197     if $xmldir.elements['dir']
1198         $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
1199         $xmldir.elements.each('dir') { |element|
1200             path = element.attributes['path']
1201             if element.attributes['subdirs-caption']
1202                 element.add_attribute('subdirs-caption',     $subalbums_edits[path][:editzone].buffer.text)
1203                 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1204             else
1205                 element.add_attribute('thumbnails-caption',     $subalbums_edits[path][:editzone].buffer.text)
1206                 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1207             end
1208         }
1209         if $xmldir.attributes['thumbnails-caption']
1210             path = $xmldir.attributes['path']
1211             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1212         end
1213     end
1214
1215     #- remove and reinsert elements to reflect new ordering
1216     save_attributes = {}
1217     save_types = {}
1218     cpt = 0
1219     $xmldir.elements.each { |element|
1220         if element.name == 'image' || element.name == 'video'
1221             save_types[element.attributes['filename']] = element.name
1222             save_attributes[element.attributes['filename']] = element.attributes
1223             element.remove
1224             cpt += 1
1225         end
1226     }
1227     $autotable.current_order.each { |path|
1228         chld = $xmldir.add_element save_types[path], save_attributes[path]
1229         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1230     }
1231 end
1232
1233 def change_dir
1234     $autotable.clear
1235     $vbox2textview = {}
1236     $name2widgets = {}
1237     UndoHandler.cleanup
1238     $undo_tb.sensitive = $undo_mb.sensitive = false
1239     $redo_tb.sensitive = $redo_mb.sensitive = false
1240
1241     if !$current_path
1242         return
1243     end
1244
1245     $subalbums_vb.children.each { |chld|
1246         $subalbums_vb.remove(chld)
1247     }
1248     $subalbums = Gtk::Table.new(0, 0, true)
1249     current_y_sub_albums = 0
1250
1251     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1252     $subalbums_edits = {}
1253     subalbums_counter = 0
1254     subalbums_edits_bypos = {}
1255
1256     add_subalbum = Proc.new { |xmldir, counter|
1257         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1258         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1259         if xmldir == $xmldir
1260             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1261             caption = xmldir.attributes['thumbnails-caption']
1262             captionfile, dummy = find_subalbum_caption_info(xmldir)
1263             infotype = 'thumbnails'
1264         else
1265             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1266             captionfile, caption = find_subalbum_caption_info(xmldir)
1267             infotype = find_subalbum_info_type(xmldir)
1268         end
1269         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1270         hbox = Gtk::HBox.new
1271         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1272         f = Gtk::Frame.new
1273         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1274
1275         img = nil
1276         my_gen_real_thumbnail = proc {
1277             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1278         }
1279
1280         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1281             f.add(img = Gtk::Image.new)
1282             my_gen_real_thumbnail.call
1283         else
1284             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1285         end
1286         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1287         $subalbums.attach(hbox,
1288                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1289
1290         frame, textview = create_editzone($subalbums_sw, 0, img)
1291         textview.buffer.text = caption
1292         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1293                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1294
1295         change_image = Proc.new {
1296             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1297                                             nil,
1298                                             Gtk::FileChooser::ACTION_OPEN,
1299                                             nil,
1300                                             [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1301             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1302             fc.transient_for = $main_window
1303             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))
1304             f.add(preview_img = Gtk::Image.new)
1305             preview.show_all
1306             fc.signal_connect('update-preview') { |w|
1307                 begin
1308                     if fc.preview_filename
1309                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1310                         fc.preview_widget_active = true
1311                     end
1312                 rescue Gdk::PixbufError
1313                     fc.preview_widget_active = false
1314                 end
1315             }
1316             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1317                 $modified = true
1318                 old_file = captionfile
1319                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1320                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1321                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1322                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1323
1324                 new_file = fc.filename
1325                 msg 3, "new captionfile is: #{fc.filename}"
1326                 perform_changefile = Proc.new {
1327                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1328                     $modified_pixbufs.delete(thumbnail_file)
1329                     xmldir.delete_attribute("#{infotype}-rotate")
1330                     xmldir.delete_attribute("#{infotype}-color-swap")
1331                     xmldir.delete_attribute("#{infotype}-enhance")
1332                     xmldir.delete_attribute("#{infotype}-frame-offset")
1333                     my_gen_real_thumbnail.call
1334                 }
1335                 perform_changefile.call
1336
1337                 save_undo(_("change caption file for sub-album"),
1338                           Proc.new {
1339                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1340                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1341                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1342                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1343                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1344                               my_gen_real_thumbnail.call
1345                               $notebook.set_page(0)
1346                               Proc.new {
1347                                   perform_changefile.call
1348                                   $notebook.set_page(0)
1349                               }
1350                           })
1351             end
1352             fc.destroy
1353         }
1354
1355         rotate_and_cleanup = Proc.new { |angle|
1356             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1357             system("rm -f '#{thumbnail_file}'")
1358         }
1359
1360         move = Proc.new { |direction|
1361             save_changes
1362             if direction == 'up'
1363                 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1364                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1365                 subalbums_edits_bypos[oldpos - 1][:position] += 1
1366             else
1367                 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1368                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1369                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1370             end
1371
1372             elems = []
1373             $xmldir.elements.each('dir') { |element|
1374                 elems << [ element.attributes['path'], element.remove ]
1375             }
1376             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1377                   each { |e| $xmldir.add_element(e[1]) }
1378             change_dir
1379         }
1380
1381         color_swap_and_cleanup = Proc.new {
1382             perform_color_swap_and_cleanup = Proc.new {
1383                 color_swap(xmldir, "#{infotype}-")
1384                 my_gen_real_thumbnail.call
1385             }
1386             perform_color_swap_and_cleanup.call
1387
1388             save_undo(_("color swap"),
1389                       Proc.new {
1390                           perform_color_swap_and_cleanup.call
1391                           $notebook.set_page(0)
1392                           Proc.new {
1393                               perform_color_swap_and_cleanup.call
1394                               $notebook.set_page(0)
1395                           }
1396                       })
1397         }
1398
1399         change_frame_offset_and_cleanup = Proc.new {
1400             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1401                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1402                     change_frame_offset(xmldir, "#{infotype}-", val)
1403                     my_gen_real_thumbnail.call
1404                 }
1405                 perform_change_frame_offset_and_cleanup.call(values[:new])
1406
1407                 save_undo(_("specify frame offset"),
1408                           Proc.new {
1409                               perform_change_frame_offset_and_cleanup.call(values[:old])
1410                               $notebook.set_page(0)
1411                               Proc.new {
1412                                   perform_change_frame_offset_and_cleanup.call(values[:new])
1413                                   $notebook.set_page(0)
1414                               }
1415                           })
1416             end
1417         }
1418
1419         whitebalance_and_cleanup = Proc.new {
1420             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1421                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1422                 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1423                     change_whitebalance(xmldir, "#{infotype}-", val)
1424                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1425                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1426                     system("rm -f '#{thumbnail_file}'")
1427                 }
1428                 perform_change_whitebalance_and_cleanup.call(values[:new])
1429                 
1430                 save_undo(_("fix white balance"),
1431                           Proc.new {
1432                               perform_change_whitebalance_and_cleanup.call(values[:old])
1433                               $notebook.set_page(0)
1434                               Proc.new {
1435                                   perform_change_whitebalance_and_cleanup.call(values[:new])
1436                                   $notebook.set_page(0)
1437                               }
1438                           })
1439             end
1440         }
1441
1442         enhance_and_cleanup = Proc.new {
1443             perform_enhance_and_cleanup = Proc.new {
1444                 enhance(xmldir, "#{infotype}-")
1445                 my_gen_real_thumbnail.call
1446             }
1447             
1448             perform_enhance_and_cleanup.call
1449             
1450             save_undo(_("enhance"),
1451                       Proc.new {
1452                           perform_enhance_and_cleanup.call
1453                           $notebook.set_page(0)
1454                           Proc.new {
1455                               perform_enhance_and_cleanup.call
1456                               $notebook.set_page(0)
1457                           }
1458                       })
1459         }
1460
1461         evtbox.signal_connect('button-press-event') { |w, event|
1462             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1463                 if $r90.active?
1464                     rotate_and_cleanup.call(90)
1465                 elsif $r270.active?
1466                     rotate_and_cleanup.call(-90)
1467                 elsif $enhance.active?
1468                     enhance_and_cleanup.call
1469                 end
1470             end
1471             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1472                 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1473                                      { :forbid_left => true, :forbid_right => true,
1474                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
1475                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1476                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1477             end
1478             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1479                 change_image.call
1480                 true   #- handled
1481             end
1482         }
1483         evtbox.signal_connect('button-press-event') { |w, event|
1484             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1485             false
1486         }
1487
1488         evtbox.signal_connect('button-release-event') { |w, event|
1489             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1490                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1491                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1492                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1493                     msg 3, "gesture rotate: #{angle}"
1494                     rotate_and_cleanup.call(angle)
1495                 end
1496             end
1497             $gesture_press = nil
1498         }
1499                 
1500         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
1501         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
1502         current_y_sub_albums += 1
1503     }
1504
1505     if $xmldir.elements['dir']
1506         #- title edition
1507         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1508         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1509         $subalbums_title.set_justification(Gtk::Justification::CENTER)
1510         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1511         #- this album image/caption
1512         if $xmldir.attributes['thumbnails-caption']
1513             add_subalbum.call($xmldir, 0)
1514         end
1515     end
1516     $xmldir.elements.each { |element|
1517         if element.name == 'image' || element.name == 'video'
1518             #- element (image or video) of this album
1519             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1520             msg 3, "dest_img: #{dest_img}"
1521             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1522         end
1523         if element.name == 'dir'
1524             #- sub-album image/caption
1525             add_subalbum.call(element, subalbums_counter += 1)
1526         end
1527     }
1528     $subalbums_vb.add($subalbums)
1529     $subalbums_vb.show_all
1530
1531     if !$xmldir.elements['image'] && !$xmldir.elements['video']
1532         $notebook.get_tab_label($autotable_sw).sensitive = false
1533         $notebook.set_page(0)
1534     else
1535         $notebook.get_tab_label($autotable_sw).sensitive = true
1536     end
1537
1538     if !$xmldir.elements['dir']
1539         $notebook.get_tab_label($subalbums_sw).sensitive = false
1540         $notebook.set_page(1)
1541     else
1542         $notebook.get_tab_label($subalbums_sw).sensitive = true
1543     end
1544 end
1545
1546 def pixbuf_or_nil(filename)
1547     begin
1548         return Gdk::Pixbuf.new(filename)
1549     rescue
1550         return nil
1551     end
1552 end
1553
1554 def theme_choose(current)
1555     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1556                              $main_window,
1557                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1558                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1559                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1560
1561     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1562     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1563     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1564     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1565     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1566     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1567     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1568     treeview.signal_connect('button-press-event') { |w, event|
1569         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1570             dialog.response(Gtk::Dialog::RESPONSE_OK)
1571         end
1572     }
1573
1574     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1575
1576     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1577         dir.chomp!
1578         iter = model.append
1579         iter[0] = File.basename(dir)
1580         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1581         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1582         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1583         if File.basename(dir) == current
1584             treeview.selection.select_iter(iter)
1585         end
1586     }
1587
1588     dialog.set_default_size(700, 400)
1589     dialog.vbox.show_all
1590     dialog.run { |response|
1591         iter = treeview.selection.selected
1592         dialog.destroy
1593         if response == Gtk::Dialog::RESPONSE_OK && iter
1594             return model.get_value(iter, 0)
1595         end
1596     }
1597     return nil
1598 end
1599
1600 def populate_subalbums_treeview
1601     $albums_ts.clear
1602     $autotable.clear
1603     $subalbums_vb.children.each { |chld|
1604         $subalbums_vb.remove(chld)
1605     }
1606
1607     source = $xmldoc.root.attributes['source']
1608     msg 3, "source: #{source}"
1609
1610     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1611     if !xmldir
1612         msg 1, _("Corrupted booh file...")
1613         return
1614     end
1615
1616     append_dir_elem = Proc.new { |parent_iter, location|
1617         child_iter = $albums_ts.append(parent_iter)
1618         child_iter[0] = File.basename(location)
1619         child_iter[1] = location
1620         msg 3, "puttin location: #{location}"
1621         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1622             append_dir_elem.call(child_iter, elem.attributes['path'])
1623         }
1624     }
1625     append_dir_elem.call(nil, source)
1626
1627     $albums_tv.expand_all
1628     $albums_tv.selection.select_iter($albums_ts.iter_first)
1629 end
1630
1631 def open_file(filename)
1632
1633     $filename = nil
1634     $modified = false
1635     $current_path = nil   #- invalidate
1636     $modified_pixbufs = {}
1637     $albums_ts.clear
1638     $autotable.clear
1639     $subalbums_vb.children.each { |chld|
1640         $subalbums_vb.remove(chld)
1641     }
1642
1643     if !File.exists?(filename)
1644         return utf8(_("File not found."))
1645     end
1646
1647     begin
1648         $xmldoc = REXML::Document.new File.new(filename)
1649     rescue Exception
1650         $xmldoc = nil
1651     end
1652
1653     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1654         if entry2type(filename).nil?
1655             return utf8(_("Not a booh file!"))
1656         else
1657             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."))
1658         end
1659     end
1660
1661     if !source = $xmldoc.root.attributes['source']
1662         return utf8(_("Corrupted booh file..."))
1663     end
1664
1665     if !dest = $xmldoc.root.attributes['destination']
1666         return utf8(_("Corrupted booh file..."))
1667     end
1668
1669     if !theme = $xmldoc.root.attributes['theme']
1670         return utf8(_("Corrupted booh file..."))
1671     end
1672
1673     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1674
1675     $filename = filename
1676     select_theme(theme, limit_sizes)
1677     $default_size['thumbnails'] =~ /(.*)x(.*)/
1678     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1679     $albums_thumbnail_size =~ /(.*)x(.*)/
1680     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1681
1682     populate_subalbums_treeview
1683
1684     $config['last-opens'] ||= []
1685     if $config['last-opens'][-1] != utf8(filename)
1686         $config['last-opens'] << utf8(filename)
1687     end
1688     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1689     return nil
1690 end
1691
1692 def open_file_popup
1693     if !ask_save_modifications(utf8(_("Save this album?")),
1694                                utf8(_("Do you want to save the changes to this album?")),
1695                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
1696         return
1697     end
1698     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1699                                     nil,
1700                                     Gtk::FileChooser::ACTION_OPEN,
1701                                     nil,
1702                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1703     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1704     fc.set_current_folder(File.expand_path("~/.booh"))
1705     fc.transient_for = $main_window
1706     ok = false
1707     while !ok
1708         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1709             push_mousecursor_wait(fc)
1710             msg = open_file(fc.filename)
1711             pop_mousecursor(fc)
1712             if msg
1713                 show_popup(fc, msg)
1714                 ok = false
1715             else
1716                 ok = true
1717             end
1718         else
1719             ok = true
1720         end
1721     end
1722     fc.destroy
1723 end
1724
1725 def additional_booh_options
1726     options = ''
1727     if $config['mproc']
1728         options += "--mproc #{$config['mproc'].to_i} "
1729     end
1730     return options
1731 end
1732
1733 def new_album
1734     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1735                              $main_window,
1736                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1737                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1738                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1739     
1740     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1741     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1742                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1743     tbl.attach(src = Gtk::Entry.new,
1744                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1745     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1746                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1747     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1748                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1749     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1750                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1751     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1752                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1753     tbl.attach(dest = Gtk::Entry.new,
1754                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1755     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1756                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1757     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1758                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1759     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1760                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1761     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1762                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1763
1764     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1765     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1766                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1767     tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1768                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1769     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1770                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1771     tbl.attach(sizes = Gtk::HBox.new,
1772                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1773
1774     src_nb_calculated_for = ''
1775     src_nb_thread = nil
1776     process_src_nb = Proc.new {
1777         if src.text != src_nb_calculated_for
1778             src_nb_calculated_for = src.text
1779             if src_nb_thread
1780                 Thread.kill(src_nb_thread)
1781                 src_nb_thread = nil
1782             end
1783             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1784                 if File.readable?(from_utf8(src_nb_calculated_for))
1785                     src_nb_thread = Thread.new {
1786                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1787                         total = { 'image' => 0, 'video' => 0, nil => 0 }
1788                         `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1789                             if File.basename(dir) =~ /^\./
1790                                 next
1791                             else
1792                                 begin
1793                                     Dir.entries(dir.chomp).each { |file|
1794                                         total[entry2type(file)] += 1
1795                                     }
1796                                 rescue Errno::EACCES, Errno::ENOENT
1797                                 end
1798                             end
1799                         }
1800                         src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1801                         src_nb_thread = nil
1802                     }
1803                 else
1804                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1805                 end
1806             else
1807                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1808             end
1809         end
1810         true
1811     }
1812     timeout_src_nb = Gtk.timeout_add(100) {
1813         process_src_nb.call
1814     }
1815
1816     src_browse.signal_connect('clicked') {
1817         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1818                                         nil,
1819                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
1820                                         nil,
1821                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1822         fc.transient_for = $main_window
1823         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1824             src.text = utf8(fc.filename)
1825             process_src_nb.call
1826             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1827         end
1828         fc.destroy
1829     }
1830
1831     dest_browse.signal_connect('clicked') {
1832         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1833                                         nil,
1834                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
1835                                         nil,
1836                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1837         fc.transient_for = $main_window
1838         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1839             dest.text = utf8(fc.filename)
1840         end
1841         fc.destroy
1842     }
1843
1844     conf_browse.signal_connect('clicked') {
1845         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1846                                         nil,
1847                                         Gtk::FileChooser::ACTION_SAVE,
1848                                         nil,
1849                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1850         fc.transient_for = $main_window
1851         fc.add_shortcut_folder(File.expand_path("~/.booh"))
1852         fc.set_current_folder(File.expand_path("~/.booh"))
1853         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1854             conf.text = utf8(fc.filename)
1855         end
1856         fc.destroy
1857     }
1858
1859     theme_sizes = []
1860     recreate_theme_config = proc {
1861         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1862         theme_sizes = []
1863         select_theme(theme_button.label, 'all')
1864         $images_size.each { |s|
1865             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1866             if !s['optional']
1867                 cb.active = true
1868             end
1869             tooltips = Gtk::Tooltips.new
1870             tooltips.set_tip(cb, utf8(s['description']), nil)
1871             theme_sizes << { :widget => cb, :value => s['name'] }
1872         }
1873         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1874         tooltips = Gtk::Tooltips.new
1875         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1876         theme_sizes << { :widget => cb, :value => 'original' }
1877         sizes.show_all
1878     }
1879     recreate_theme_config.call
1880
1881     theme_button.signal_connect('clicked') {
1882         if newtheme = theme_choose(theme_button.label)
1883             theme_button.label = newtheme
1884             recreate_theme_config.call
1885         end
1886     }
1887
1888     dialog.vbox.add(frame1)
1889     dialog.vbox.add(frame2)
1890     dialog.window_position = Gtk::Window::POS_MOUSE
1891     dialog.show_all
1892
1893     keepon = true
1894     ok = true
1895     while keepon
1896         dialog.run { |response|
1897             if response == Gtk::Dialog::RESPONSE_OK
1898                 srcdir = from_utf8(src.text)
1899                 destdir = from_utf8(dest.text)
1900                 if !File.directory?(srcdir)
1901                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1902                     src.grab_focus
1903                 elsif conf.text == ''
1904                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1905                     conf.grab_focus
1906                 elsif destdir != make_dest_filename(destdir)
1907                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1908                     dest.grab_focus
1909                 elsif File.directory?(destdir)
1910                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1911                     dest.grab_focus
1912                 elsif File.exists?(destdir)
1913                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1914                     dest.grab_focus
1915                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1916                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1917                 else
1918                     system("mkdir '#{destdir}'")
1919                     if !File.directory?(destdir)
1920                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1921                         dest.grab_focus
1922                     else
1923                         keepon = false
1924                     end
1925                 end
1926             else
1927                 keepon = ok = false
1928             end
1929         }
1930     end
1931     srcdir = from_utf8(src.text)
1932     destdir = from_utf8(dest.text)
1933     configskel = File.expand_path(from_utf8(conf.text))
1934     theme = theme_button.label
1935     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1936     dialog.destroy
1937     if src_nb_thread
1938         Thread.kill(src_nb_thread)
1939     end
1940     Gtk.timeout_remove(timeout_src_nb)
1941
1942     if ok
1943         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1944                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1945                      utf8(_("Please wait while scanning source directory...")),
1946                      'full scan',
1947                      { :closure_after => proc { open_file(configskel) } })
1948     end
1949 end
1950
1951 def properties
1952     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1953                              $main_window,
1954                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1955                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1956                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1957     
1958     source = $xmldoc.root.attributes['source']
1959     dest = $xmldoc.root.attributes['destination']
1960     theme = $xmldoc.root.attributes['theme']
1961     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1962     if limit_sizes
1963         limit_sizes = limit_sizes.split(/,/)
1964     end
1965
1966     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1967     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1968                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1969     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1970                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1971     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1972                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1973     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1974                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1975     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1976                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1977     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1978                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1979
1980     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1981     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1982                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1983     tbl.attach(theme_button = Gtk::Button.new(theme),
1984                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1985     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1986                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1987     tbl.attach(sizes = Gtk::HBox.new,
1988                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1989
1990     theme_sizes = []
1991     recreate_theme_config = proc {
1992         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1993         theme_sizes = []
1994         select_theme(theme_button.label, 'all')
1995         $images_size.each { |s|
1996             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1997             if limit_sizes
1998                 if limit_sizes.include?(s['name'])
1999                     cb.active = true
2000                 end
2001             else
2002                 if !s['optional']
2003                     cb.active = true
2004                 end
2005             end
2006             tooltips = Gtk::Tooltips.new
2007             tooltips.set_tip(cb, utf8(s['description']), nil)
2008             theme_sizes << { :widget => cb, :value => s['name'] }
2009         }
2010         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2011         tooltips = Gtk::Tooltips.new
2012         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2013         if limit_sizes && limit_sizes.include?('original')
2014             cb.active = true
2015         end
2016         theme_sizes << { :widget => cb, :value => 'original' }
2017         sizes.show_all
2018     }
2019     recreate_theme_config.call
2020
2021     theme_button.signal_connect('clicked') {
2022         if newtheme = theme_choose(theme_button.label)
2023             limit_sizes = nil
2024             theme_button.label = newtheme
2025             recreate_theme_config.call
2026         end
2027     }
2028
2029     dialog.vbox.add(frame1)
2030     dialog.vbox.add(frame2)
2031     dialog.window_position = Gtk::Window::POS_MOUSE
2032     dialog.show_all
2033
2034     keepon = true
2035     ok = true
2036     while keepon
2037         dialog.run { |response|
2038             if response == Gtk::Dialog::RESPONSE_OK
2039                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2040                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2041                 else
2042                     keepon = false
2043                 end
2044             else
2045                 keepon = ok = false
2046             end
2047         }
2048     end
2049     save_theme = theme_button.label
2050     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2051     dialog.destroy
2052
2053     if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
2054         save_current_file
2055         call_backend("booh-backend --use-config '#{$filename}' --for-gui " +
2056                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
2057                      utf8(_("Please wait while scanning source directory...")),
2058                      'full scan',
2059                      { :closure_after => proc { open_file($filename) } })
2060     end
2061 end
2062
2063 def merge_current
2064     save_current_file
2065
2066     sel = $albums_tv.selection.selected_rows
2067
2068     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2069                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2070                  utf8(_("Please wait while scanning source directory...")),
2071                  'one dir scan',
2072                  { :closure_after => proc {
2073                          open_file($filename)
2074                          $albums_tv.selection.select_path(sel[0])
2075                      } })
2076 end
2077
2078 def merge
2079     save_current_file
2080
2081     theme = $xmldoc.root.attributes['theme']
2082     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2083     if limit_sizes
2084         limit_sizes = "--sizes #{limit_sizes}"
2085     end
2086     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2087                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2088                  utf8(_("Please wait while scanning source directory...")),
2089                  'full scan',
2090                  { :closure_after => proc { open_file($filename) } })
2091 end
2092
2093 def merge_subalbums
2094     save_current_file
2095
2096     call_backend("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
2097                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2098                  utf8(_("Please wait while scanning source directory...")),
2099                  'full scan',
2100                  { :closure_after => proc { open_file($filename) } })
2101 end
2102
2103 def save_as_do
2104     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2105                                     nil,
2106                                     Gtk::FileChooser::ACTION_SAVE,
2107                                     nil,
2108                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2109     fc.transient_for = $main_window
2110     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2111     fc.set_current_folder(File.expand_path("~/.booh"))
2112     fc.filename = $filename
2113     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2114         $filename = fc.filename
2115         save_current_file
2116     end
2117     fc.destroy
2118 end
2119
2120 def preferences
2121     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2122                              $main_window,
2123                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2124                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2125                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2126
2127     dialog.vbox.add(notebook = Gtk::Notebook.new)
2128     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2129     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2130                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2131     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2132                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2133     tooltips = Gtk::Tooltips.new
2134     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
2135     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2136                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2137     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)),
2138                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2139     smp_check.signal_connect('toggled') {
2140         if smp_check.active?
2141             smp_hbox.sensitive = true
2142         else
2143             smp_hbox.sensitive = false
2144         end
2145     }
2146     if $config['mproc']
2147         smp_check.active = true
2148         smp_spin.value = $config['mproc'].to_i
2149     end
2150
2151     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2152     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2153                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2154     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2155                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2156
2157     dialog.vbox.show_all
2158     dialog.run { |response|
2159         if response == Gtk::Dialog::RESPONSE_OK
2160             $config['video-viewer'] = video_viewer_entry.text
2161             if smp_check.active?
2162                 $config['mproc'] = smp_spin.value.to_i
2163             else
2164                 $config.delete('mproc')
2165             end
2166
2167             $config['convert-enhance'] = enhance_entry.text
2168         end
2169     }
2170     dialog.destroy
2171 end
2172
2173 def create_menu_and_toolbar
2174
2175     #- menu
2176     mb = Gtk::MenuBar.new
2177
2178     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
2179     filesubmenu = Gtk::Menu.new
2180     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
2181     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
2182     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2183     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
2184     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
2185     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2186     tooltips = Gtk::Tooltips.new
2187     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
2188     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2189     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
2190     filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums"))).set_sensitive(false))
2191     $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2192     tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory (but don't touch existing subalbums)")), nil)
2193     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
2194     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2195     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
2196     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2197     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
2198     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
2199     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
2200     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2201     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
2202     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2203     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2204     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2205     filemenu.set_submenu(filesubmenu)
2206     mb.append(filemenu)
2207
2208     new.signal_connect('activate') { new_album }
2209     open.signal_connect('activate') { open_file_popup }
2210     $save.signal_connect('activate') { save_current_file }
2211     $save_as.signal_connect('activate') { save_as_do }
2212     $merge_current.signal_connect('activate') { merge_current }
2213     $merge.signal_connect('activate') { merge }
2214     $merge_subalbums.signal_connect('activate') { merge_subalbums }
2215     $generate.signal_connect('activate') {
2216         save_current_file
2217         call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2218                      utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2219                      'web-album',
2220                      { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2221                        :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
2222                        :closure_after => proc {
2223                              $xmldoc.elements.each('//dir') { |elem|
2224                                  elem.add_attribute('already-generated', 'true')
2225                              }
2226                              UndoHandler.cleanup   #- prevent save_changes to mark current dir as not already generated
2227                              $undo_tb.sensitive = $undo_mb.sensitive = false
2228                              $redo_tb.sensitive = $redo_mb.sensitive = false
2229                              save_current_file
2230                          }})
2231     }
2232     $properties.signal_connect('activate') { properties }
2233
2234     quit.signal_connect('activate') { try_quit }
2235
2236     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2237     editsubmenu = Gtk::Menu.new
2238     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2239     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2240     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2241     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2242     editmenu.set_submenu(editsubmenu)
2243     mb.append(editmenu)
2244
2245     prefs.signal_connect('activate') { preferences }
2246
2247     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2248     helpsubmenu = Gtk::Menu.new
2249     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2250     helpmenu.set_submenu(helpsubmenu)
2251     mb.append(helpmenu)
2252
2253     about.signal_connect('activate') {
2254             show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2255
2256 <i>``The Web-Album of choice for discriminating Linux users''</i>
2257
2258 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
2259     }
2260
2261
2262     #- toolbar
2263     tb = Gtk::Toolbar.new
2264
2265     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2266     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
2267     open.menu = Gtk::Menu.new
2268     open.signal_connect('clicked') { open_file_popup }
2269     open.signal_connect('show-menu') {
2270         lastopens = Gtk::Menu.new
2271         j = 0
2272         if $config['last-opens']
2273             $config['last-opens'].reverse.each { |e|
2274                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2275                 item.signal_connect('activate') {
2276                     if ask_save_modifications(utf8(_("Save this album?")),
2277                                               utf8(_("Do you want to save the changes to this album?")),
2278                                               { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2279                         push_mousecursor_wait
2280                         msg = open_file(from_utf8(e))
2281                         pop_mousecursor
2282                         if msg
2283                             show_popup($main_window, msg)
2284                         end
2285                     end
2286                 }
2287                 j += 1
2288             }
2289             lastopens.show_all
2290         end
2291         open.menu = lastopens
2292     }
2293
2294     tb.insert(-1, Gtk::SeparatorToolItem.new)
2295
2296     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2297     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2298     $r90.label = utf8(_("Rotate"))
2299     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2300     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2301     $r270.label = utf8(_("Rotate"))
2302     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2303     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2304     $enhance.label = utf8(_("Enhance"))
2305     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2306     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
2307     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2308     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2309     nothing.label = utf8(_("None"))
2310
2311     tb.insert(-1, Gtk::SeparatorToolItem.new)
2312
2313     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2314     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2315
2316     perform_undo = Proc.new {
2317         $redo_tb.sensitive = $redo_mb.sensitive = true
2318         if not more_undoes = UndoHandler.undo($statusbar)
2319             $undo_tb.sensitive = $undo_mb.sensitive = false
2320         end
2321     }
2322     perform_redo = Proc.new {
2323         $undo_tb.sensitive = $undo_mb.sensitive = true
2324         if not more_redoes = UndoHandler.redo($statusbar)
2325             $redo_tb.sensitive = $redo_mb.sensitive = false
2326         end
2327     }
2328
2329     $undo_tb.signal_connect('clicked')  { perform_undo.call }
2330     $undo_mb.signal_connect('activate') { perform_undo.call }
2331     $redo_tb.signal_connect('clicked')  { perform_redo.call }
2332     $redo_mb.signal_connect('activate') { perform_redo.call }
2333
2334     one_click_explain_try = Proc.new {
2335         if !$config['one-click-explained']
2336             show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2337
2338 You have just clicked on a One-Click tool. When such a tool is activated
2339 (<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
2340 on a thumbnail will immediately apply the desired action.
2341
2342 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2343 ")))
2344             $config['one-click-explained'] = true
2345         end
2346     }
2347
2348     $r90.signal_connect('toggled') {
2349         if $r90.active?
2350             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2351             one_click_explain_try.call
2352             $r270.active = false
2353             $enhance.active = false
2354             $delete.active = false
2355             nothing.sensitive = true
2356         else
2357             if !$r270.active? && !$enhance.active? && !$delete.active?
2358                 set_mousecursor_normal
2359                 nothing.sensitive = false
2360             else
2361                 nothing.sensitive = true
2362             end
2363         end
2364     }
2365     $r270.signal_connect('toggled') {
2366         if $r270.active?
2367             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2368             one_click_explain_try.call
2369             $r90.active = false
2370             $enhance.active = false
2371             $delete.active = false
2372             nothing.sensitive = true
2373         else
2374             if !$r90.active? && !$enhance.active? && !$delete.active?
2375                 set_mousecursor_normal
2376                 nothing.sensitive = false
2377             else
2378                 nothing.sensitive = true
2379             end
2380         end
2381     }
2382     $enhance.signal_connect('toggled') {
2383         if $enhance.active?
2384             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2385             one_click_explain_try.call
2386             $r90.active = false
2387             $r270.active = false
2388             $delete.active = false
2389             nothing.sensitive = true
2390         else
2391             if !$r90.active? && !$r270.active? && !$delete.active?
2392                 set_mousecursor_normal
2393                 nothing.sensitive = false
2394             else
2395                 nothing.sensitive = true
2396             end
2397         end
2398     }
2399     $delete.signal_connect('toggled') {
2400         if $delete.active?
2401             set_mousecursor(Gdk::Cursor::PIRATE)
2402             one_click_explain_try.call
2403             $r90.active = false
2404             $r270.active = false
2405             $enhance.active = false
2406             nothing.sensitive = true
2407         else
2408             if !$r90.active? && !$r270.active? && !$enhance.active?
2409                 set_mousecursor_normal
2410                 nothing.sensitive = false
2411             else
2412                 nothing.sensitive = true
2413             end
2414         end
2415     }
2416     nothing.signal_connect('clicked') {
2417         $r90.active = $r270.active = $enhance.active = $delete.active = false
2418         set_mousecursor_normal
2419     }
2420
2421     return [ mb, tb ]
2422 end
2423
2424 def create_main_window
2425
2426     mb, tb = create_menu_and_toolbar
2427
2428 #    open_file('/home/gc/booh/foo')
2429
2430     $albums_tv = Gtk::TreeView.new
2431     $albums_tv.set_size_request(120, -1)
2432     renderer = Gtk::CellRendererText.new
2433     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2434     $albums_tv.append_column(column)
2435     $albums_tv.set_headers_visible(false)
2436     $albums_tv.selection.signal_connect('changed') { |w|
2437         push_mousecursor_wait
2438         save_changes
2439         iter = w.selected
2440         if !iter
2441             msg 3, "no selection"
2442         else
2443             $current_path = $albums_ts.get_value(iter, 1)
2444             change_dir
2445         end
2446         pop_mousecursor
2447     }
2448     $albums_ts = Gtk::TreeStore.new(String, String)
2449     $albums_tv.set_model($albums_ts)
2450     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2451
2452     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2453     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2454     albums_sw.add_with_viewport($albums_tv)
2455
2456     $notebook = Gtk::Notebook.new
2457     create_subalbums_page
2458     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2459     create_auto_table
2460     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2461     $notebook.show_all
2462     $notebook.signal_connect('switch-page') { |w, page, num|
2463         if num == 0
2464             $delete.active = false
2465             $delete.sensitive = false
2466         else
2467             $delete.sensitive = true
2468         end
2469     }
2470
2471     paned = Gtk::HPaned.new
2472     paned.pack1(albums_sw, false, false)
2473     paned.pack2($notebook, true, true)
2474
2475     main_vbox = Gtk::VBox.new(false, 0)
2476     main_vbox.pack_start(mb, false, false)
2477     main_vbox.pack_start(tb, false, false)
2478     main_vbox.pack_start(paned, true, true)
2479     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2480
2481     $main_window = Gtk::Window.new
2482     $main_window.add(main_vbox)
2483     $main_window.signal_connect('delete-event') {
2484         try_quit({ :disallow_cancel => true })
2485     }
2486
2487     #- read/save size and position of window
2488     if $config['pos-x'] && $config['pos-y']
2489         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2490     else
2491         $main_window.window_position = Gtk::Window::POS_CENTER
2492     end
2493     msg 3, "size: #{$config['width']}x#{$config['height']}"
2494     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2495     $main_window.signal_connect('configure-event') {
2496         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2497         x, y = $main_window.window.root_origin
2498         width, height = $main_window.window.size
2499         $config['pos-x'] = x
2500         $config['pos-y'] = y
2501         $config['width'] = width
2502         $config['height'] = height
2503         false
2504     }
2505
2506     $statusbar.push(0, utf8(_("Ready.")))
2507     $main_window.show_all
2508 end
2509
2510 Thread.abort_on_exception = true
2511
2512 handle_options
2513 read_config
2514
2515 Gtk.init
2516 create_main_window
2517 if ARGV[0]
2518     open_file(ARGV[0])
2519 end
2520 Gtk.main
2521
2522 write_config