allow to remove a thumbnail from its fullsize view
[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     if $filename
991         ios = File.open($filename, "w")
992         $xmldoc.write(ios, 0)
993         ios.close
994         $modified = false
995     end
996 end
997
998 def try_quit
999     if $modified
1000         dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
1001                                  $main_window,
1002                                  Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1003                                  [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1004                                  [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1005                                  [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1006         dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
1007         dialog.window_position = Gtk::Window::POS_CENTER
1008         dialog.show_all
1009         
1010         dialog.run { |response|
1011             dialog.destroy
1012             if response == Gtk::Dialog::RESPONSE_CANCEL
1013                 return
1014             elsif response == Gtk::Dialog::RESPONSE_YES
1015                 save_current_file
1016             end
1017         }
1018     end
1019
1020     Gtk.main_quit
1021 end
1022
1023 def show_popup(parent, msg, *options)
1024     dialog = Gtk::Dialog.new
1025     dialog.title = utf8(_("Booh message"))
1026     lbl = Gtk::Label.new
1027     lbl.markup = msg
1028     if options[0] && options[0][:centered]
1029         lbl.set_justify(Gtk::Justification::CENTER)
1030     end
1031     dialog.vbox.add(lbl)
1032     if options[0] && options[0][:okcancel]
1033         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1034     end
1035     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1036
1037     dialog.transient_for = parent
1038     dialog.set_default_size(200, 120)
1039     if options[0] && options[0][:pos_centered]
1040         dialog.window_position = Gtk::Window::POS_CENTER
1041     else
1042         dialog.window_position = Gtk::Window::POS_MOUSE
1043     end
1044     dialog.show_all
1045
1046     dialog.run { |response|
1047         dialog.destroy
1048         if options[0] && options[0][:okcancel]
1049             return response == Gtk::Dialog::RESPONSE_OK
1050         end
1051     }
1052 end
1053
1054 def backend_wait_message(parent, msg, infopipe_path, mode)
1055     w = Gtk::Window.new
1056     w.set_transient_for(parent)
1057     w.modal = true
1058
1059     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1060     vb.pack_start(Gtk::Label.new(msg), false, false)
1061
1062     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1063     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1064     if mode != 'one dir scan'
1065         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1066     end
1067     if mode == 'web-album'
1068         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1069         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1070     end
1071     vb.pack_start(Gtk::HSeparator.new, false, false)
1072
1073     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1074     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1075     vb.pack_end(bottom, false, false)
1076
1077     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1078     refresh_thread = Thread.new {
1079         directories_counter = 0
1080         while line = infopipe.gets
1081             if line =~ /^directories: (\d+), sizes: (\d+)/
1082                 directories = $1.to_f + 1
1083                 sizes = $2.to_f
1084             elsif line =~ /^walking: (.+), (\d+) elements$/
1085                 elements = $2.to_f + 1
1086                 if mode == 'web-album'
1087                     elements += sizes
1088                 end
1089                 element_counter = 0
1090                 pb1_1.fraction = 0
1091                 if mode != 'one dir scan'
1092                     newtext = utf8(full_src_dir_to_rel($1))
1093                     newtext = '/' if newtext == ''
1094                     pb1_2.text = newtext
1095                     directories_counter += 1
1096                     pb1_2.fraction = directories_counter / directories
1097                 end
1098             elsif line =~ /^processing element$/
1099                 element_counter += 1
1100                 pb1_1.fraction = element_counter / elements
1101             elsif line =~ /^processing size$/
1102                 element_counter += 1
1103                 pb1_1.fraction = element_counter / elements
1104             elsif line =~ /^finished processing sizes$/
1105                 pb1_1.fraction = 1
1106             elsif line =~ /^creating index.html$/
1107                 pb1_2.text = utf8(_("finished"))
1108                 pb1_2.fraction = 1
1109                 directories_counter = 0
1110             elsif line =~ /^index.html: (.+)/
1111                 newtext = utf8(full_src_dir_to_rel($1))
1112                 newtext = '/' if newtext == ''
1113                 pb2.text = newtext
1114                 directories_counter += 1
1115                 pb2.fraction = directories_counter / directories
1116             end
1117         end
1118     }
1119
1120     w.add(vb)
1121     w.signal_connect('delete-event') { w.destroy }
1122     w.signal_connect('destroy') {
1123         Thread.kill(refresh_thread)
1124         if infopipe_path
1125             infopipe.close
1126             system("rm -f #{infopipe_path}")
1127         end
1128     }
1129     w.window_position = Gtk::Window::POS_CENTER
1130     w.show_all
1131
1132     return [ b, w ]
1133 end
1134
1135 def call_backend(cmd, waitmsg, mode, params)
1136     pipe = Tempfile.new("boohpipe")
1137     pipe.close!
1138     system("mkfifo #{pipe.path}")
1139     cmd += " --info-pipe #{pipe.path}"
1140     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1141     pid = nil
1142     Thread.new {
1143         msg 2, cmd
1144         if pid = fork
1145             id, exitstatus = Process.waitpid2(pid)
1146             w8.destroy
1147             if exitstatus == 0
1148                 if params[:successmsg]
1149                     show_popup($main_window, params[:successmsg])
1150                 end
1151                 if params[:closure_after]
1152                     params[:closure_after].call
1153                 end
1154             elsif exitstatus == 15
1155                 #- say nothing, user aborted
1156             else
1157                 if params[:failuremsg]
1158                     show_popup($main_window, params[:failuremsg])
1159                 end
1160             end
1161         else
1162             exec(cmd)
1163         end
1164     }
1165     button.signal_connect('clicked') {
1166         Process.kill('SIGTERM', pid)
1167     }
1168 end
1169
1170 def save_changes(*forced)
1171     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1172         return
1173     end
1174
1175     if $xmldir.elements['dir']
1176         $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
1177         $xmldir.elements.each('dir') { |element|
1178             path = element.attributes['path']
1179             if element.attributes['subdirs-caption']
1180                 element.add_attribute('subdirs-caption',     $subalbums_edits[path][:editzone].buffer.text)
1181                 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1182             else
1183                 element.add_attribute('thumbnails-caption',     $subalbums_edits[path][:editzone].buffer.text)
1184                 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1185             end
1186         }
1187         if $xmldir.attributes['thumbnails-caption']
1188             path = $xmldir.attributes['path']
1189             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1190         end
1191     end
1192
1193     #- remove and reinsert elements to reflect new ordering
1194     save_attributes = {}
1195     save_types = {}
1196     cpt = 0
1197     $xmldir.elements.each { |element|
1198         if element.name == 'image' || element.name == 'video'
1199             save_types[element.attributes['filename']] = element.name
1200             save_attributes[element.attributes['filename']] = element.attributes
1201             element.remove
1202             cpt += 1
1203         end
1204     }
1205     $autotable.current_order.each { |path|
1206         chld = $xmldir.add_element save_types[path], save_attributes[path]
1207         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1208     }
1209 end
1210
1211 def change_dir
1212     $autotable.clear
1213     $vbox2textview = {}
1214     $name2widgets = {}
1215     UndoHandler.cleanup
1216     $undo_tb.sensitive = $undo_mb.sensitive = false
1217     $redo_tb.sensitive = $redo_mb.sensitive = false
1218
1219     if !$current_path
1220         return
1221     end
1222
1223     $subalbums_vb.children.each { |chld|
1224         $subalbums_vb.remove(chld)
1225     }
1226     $subalbums = Gtk::Table.new(0, 0, true)
1227     current_y_sub_albums = 0
1228
1229     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1230     $subalbums_edits = {}
1231     subalbums_counter = 0
1232     subalbums_edits_bypos = {}
1233
1234     add_subalbum = Proc.new { |xmldir, counter|
1235         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1236         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1237         if xmldir == $xmldir
1238             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1239             caption = xmldir.attributes['thumbnails-caption']
1240             captionfile, dummy = find_subalbum_caption_info(xmldir)
1241             infotype = 'thumbnails'
1242         else
1243             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1244             captionfile, caption = find_subalbum_caption_info(xmldir)
1245             infotype = find_subalbum_info_type(xmldir)
1246         end
1247         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1248         hbox = Gtk::HBox.new
1249         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1250         f = Gtk::Frame.new
1251         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1252
1253         img = nil
1254         my_gen_real_thumbnail = proc {
1255             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1256         }
1257
1258         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1259             f.add(img = Gtk::Image.new)
1260             my_gen_real_thumbnail.call
1261         else
1262             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1263         end
1264         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1265         $subalbums.attach(hbox,
1266                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1267
1268         frame, textview = create_editzone($subalbums_sw, 0, img)
1269         textview.buffer.text = caption
1270         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1271                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1272
1273         change_image = Proc.new {
1274             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1275                                             nil,
1276                                             Gtk::FileChooser::ACTION_OPEN,
1277                                             nil,
1278                                             [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1279             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1280             fc.transient_for = $main_window
1281             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))
1282             f.add(preview_img = Gtk::Image.new)
1283             preview.show_all
1284             fc.signal_connect('update-preview') { |w|
1285                 begin
1286                     if fc.preview_filename
1287                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1288                         fc.preview_widget_active = true
1289                     end
1290                 rescue Gdk::PixbufError
1291                     fc.preview_widget_active = false
1292                 end
1293             }
1294             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1295                 $modified = true
1296                 old_file = captionfile
1297                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1298                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1299                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1300                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1301
1302                 new_file = fc.filename
1303                 msg 3, "new captionfile is: #{fc.filename}"
1304                 perform_changefile = Proc.new {
1305                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1306                     $modified_pixbufs.delete(thumbnail_file)
1307                     xmldir.delete_attribute("#{infotype}-rotate")
1308                     xmldir.delete_attribute("#{infotype}-color-swap")
1309                     xmldir.delete_attribute("#{infotype}-enhance")
1310                     xmldir.delete_attribute("#{infotype}-frame-offset")
1311                     my_gen_real_thumbnail.call
1312                 }
1313                 perform_changefile.call
1314
1315                 save_undo(_("change caption file for sub-album"),
1316                           Proc.new {
1317                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1318                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1319                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1320                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1321                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1322                               my_gen_real_thumbnail.call
1323                               $notebook.set_page(0)
1324                               Proc.new {
1325                                   perform_changefile.call
1326                                   $notebook.set_page(0)
1327                               }
1328                           })
1329             end
1330             fc.destroy
1331         }
1332
1333         rotate_and_cleanup = Proc.new { |angle|
1334             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1335             system("rm -f '#{thumbnail_file}'")
1336         }
1337
1338         move = Proc.new { |direction|
1339             save_changes
1340             if direction == 'up'
1341                 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1342                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1343                 subalbums_edits_bypos[oldpos - 1][:position] += 1
1344             else
1345                 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1346                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1347                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1348             end
1349
1350             elems = []
1351             $xmldir.elements.each('dir') { |element|
1352                 elems << [ element.attributes['path'], element.remove ]
1353             }
1354             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1355                   each { |e| $xmldir.add_element(e[1]) }
1356             change_dir
1357         }
1358
1359         color_swap_and_cleanup = Proc.new {
1360             perform_color_swap_and_cleanup = Proc.new {
1361                 color_swap(xmldir, "#{infotype}-")
1362                 my_gen_real_thumbnail.call
1363             }
1364             perform_color_swap_and_cleanup.call
1365
1366             save_undo(_("color swap"),
1367                       Proc.new {
1368                           perform_color_swap_and_cleanup.call
1369                           $notebook.set_page(0)
1370                           Proc.new {
1371                               perform_color_swap_and_cleanup.call
1372                               $notebook.set_page(0)
1373                           }
1374                       })
1375         }
1376
1377         change_frame_offset_and_cleanup = Proc.new {
1378             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1379                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1380                     change_frame_offset(xmldir, "#{infotype}-", val)
1381                     my_gen_real_thumbnail.call
1382                 }
1383                 perform_change_frame_offset_and_cleanup.call(values[:new])
1384
1385                 save_undo(_("specify frame offset"),
1386                           Proc.new {
1387                               perform_change_frame_offset_and_cleanup.call(values[:old])
1388                               $notebook.set_page(0)
1389                               Proc.new {
1390                                   perform_change_frame_offset_and_cleanup.call(values[:new])
1391                                   $notebook.set_page(0)
1392                               }
1393                           })
1394             end
1395         }
1396
1397         whitebalance_and_cleanup = Proc.new {
1398             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1399                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1400                 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1401                     change_whitebalance(xmldir, "#{infotype}-", val)
1402                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1403                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1404                     system("rm -f '#{thumbnail_file}'")
1405                 }
1406                 perform_change_whitebalance_and_cleanup.call(values[:new])
1407                 
1408                 save_undo(_("fix white balance"),
1409                           Proc.new {
1410                               perform_change_whitebalance_and_cleanup.call(values[:old])
1411                               $notebook.set_page(0)
1412                               Proc.new {
1413                                   perform_change_whitebalance_and_cleanup.call(values[:new])
1414                                   $notebook.set_page(0)
1415                               }
1416                           })
1417             end
1418         }
1419
1420         enhance_and_cleanup = Proc.new {
1421             perform_enhance_and_cleanup = Proc.new {
1422                 enhance(xmldir, "#{infotype}-")
1423                 my_gen_real_thumbnail.call
1424             }
1425             
1426             perform_enhance_and_cleanup.call
1427             
1428             save_undo(_("enhance"),
1429                       Proc.new {
1430                           perform_enhance_and_cleanup.call
1431                           $notebook.set_page(0)
1432                           Proc.new {
1433                               perform_enhance_and_cleanup.call
1434                               $notebook.set_page(0)
1435                           }
1436                       })
1437         }
1438
1439         evtbox.signal_connect('button-press-event') { |w, event|
1440             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1441                 if $r90.active?
1442                     rotate_and_cleanup.call(90)
1443                 elsif $r270.active?
1444                     rotate_and_cleanup.call(-90)
1445                 elsif $enhance.active?
1446                     enhance_and_cleanup.call
1447                 end
1448             end
1449             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1450                 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1451                                      { :forbid_left => true, :forbid_right => true,
1452                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
1453                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1454                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1455             end
1456             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1457                 change_image.call
1458                 true   #- handled
1459             end
1460         }
1461         evtbox.signal_connect('button-press-event') { |w, event|
1462             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1463             false
1464         }
1465
1466         evtbox.signal_connect('button-release-event') { |w, event|
1467             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1468                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1469                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1470                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1471                     msg 3, "gesture rotate: #{angle}"
1472                     rotate_and_cleanup.call(angle)
1473                 end
1474             end
1475             $gesture_press = nil
1476         }
1477                 
1478         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
1479         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
1480         current_y_sub_albums += 1
1481     }
1482
1483     if $xmldir.elements['dir']
1484         #- title edition
1485         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1486         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1487         $subalbums_title.set_justification(Gtk::Justification::CENTER)
1488         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1489         #- this album image/caption
1490         if $xmldir.attributes['thumbnails-caption']
1491             add_subalbum.call($xmldir, 0)
1492         end
1493     end
1494     $xmldir.elements.each { |element|
1495         if element.name == 'image' || element.name == 'video'
1496             #- element (image or video) of this album
1497             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1498             msg 3, "dest_img: #{dest_img}"
1499             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1500         end
1501         if element.name == 'dir'
1502             #- sub-album image/caption
1503             add_subalbum.call(element, subalbums_counter += 1)
1504         end
1505     }
1506     $subalbums_vb.add($subalbums)
1507     $subalbums_vb.show_all
1508
1509     if !$xmldir.elements['image'] && !$xmldir.elements['video']
1510         $notebook.get_tab_label($autotable_sw).sensitive = false
1511         $notebook.set_page(0)
1512     else
1513         $notebook.get_tab_label($autotable_sw).sensitive = true
1514     end
1515
1516     if !$xmldir.elements['dir']
1517         $notebook.get_tab_label($subalbums_sw).sensitive = false
1518         $notebook.set_page(1)
1519     else
1520         $notebook.get_tab_label($subalbums_sw).sensitive = true
1521     end
1522 end
1523
1524 def pixbuf_or_nil(filename)
1525     begin
1526         return Gdk::Pixbuf.new(filename)
1527     rescue
1528         return nil
1529     end
1530 end
1531
1532 def theme_choose(current)
1533     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1534                              $main_window,
1535                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1536                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1537                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1538
1539     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1540     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1541     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1542     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1543     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1544     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1545     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1546     treeview.signal_connect('button-press-event') { |w, event|
1547         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1548             dialog.response(Gtk::Dialog::RESPONSE_OK)
1549         end
1550     }
1551
1552     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1553
1554     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1555         dir.chomp!
1556         iter = model.append
1557         iter[0] = File.basename(dir)
1558         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1559         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1560         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1561         if File.basename(dir) == current
1562             treeview.selection.select_iter(iter)
1563         end
1564     }
1565
1566     dialog.set_default_size(700, 400)
1567     dialog.vbox.show_all
1568     dialog.run { |response|
1569         iter = treeview.selection.selected
1570         dialog.destroy
1571         if response == Gtk::Dialog::RESPONSE_OK && iter
1572             return model.get_value(iter, 0)
1573         end
1574     }
1575     return nil
1576 end
1577
1578 def populate_subalbums_treeview
1579     $albums_ts.clear
1580     $autotable.clear
1581     $subalbums_vb.children.each { |chld|
1582         $subalbums_vb.remove(chld)
1583     }
1584
1585     source = $xmldoc.root.attributes['source']
1586     msg 3, "source: #{source}"
1587
1588     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1589     if !xmldir
1590         msg 1, _("Corrupted booh file...")
1591         return
1592     end
1593
1594     append_dir_elem = Proc.new { |parent_iter, location|
1595         child_iter = $albums_ts.append(parent_iter)
1596         child_iter[0] = File.basename(location)
1597         child_iter[1] = location
1598         msg 3, "puttin location: #{location}"
1599         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1600             append_dir_elem.call(child_iter, elem.attributes['path'])
1601         }
1602     }
1603     append_dir_elem.call(nil, source)
1604
1605     $albums_tv.expand_all
1606     $albums_tv.selection.select_iter($albums_ts.iter_first)
1607 end
1608
1609 def open_file(filename)
1610
1611     $filename = nil
1612     $modified = false
1613     $current_path = nil   #- invalidate
1614     $modified_pixbufs = {}
1615     $albums_ts.clear
1616     $autotable.clear
1617     $subalbums_vb.children.each { |chld|
1618         $subalbums_vb.remove(chld)
1619     }
1620
1621     if !File.exists?(filename)
1622         return utf8(_("File not found."))
1623     end
1624
1625     begin
1626         $xmldoc = REXML::Document.new File.new(filename)
1627     rescue Exception
1628         $xmldoc = nil
1629     end
1630
1631     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1632         if entry2type(filename).nil?
1633             return utf8(_("Not a booh file!"))
1634         else
1635             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."))
1636         end
1637     end
1638
1639     if !source = $xmldoc.root.attributes['source']
1640         return utf8(_("Corrupted booh file..."))
1641     end
1642
1643     if !dest = $xmldoc.root.attributes['destination']
1644         return utf8(_("Corrupted booh file..."))
1645     end
1646
1647     if !theme = $xmldoc.root.attributes['theme']
1648         return utf8(_("Corrupted booh file..."))
1649     end
1650
1651     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1652
1653     $filename = filename
1654     select_theme(theme, limit_sizes)
1655     $default_size['thumbnails'] =~ /(.*)x(.*)/
1656     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1657     $albums_thumbnail_size =~ /(.*)x(.*)/
1658     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1659
1660     populate_subalbums_treeview
1661
1662     $config['last-opens'] ||= []
1663     if $config['last-opens'][-1] != utf8(filename)
1664         $config['last-opens'] << utf8(filename)
1665     end
1666     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1667     return nil
1668 end
1669
1670 def open_file_popup
1671     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1672                                     nil,
1673                                     Gtk::FileChooser::ACTION_OPEN,
1674                                     nil,
1675                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1676     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1677     fc.set_current_folder(File.expand_path("~/.booh"))
1678     fc.transient_for = $main_window
1679     ok = false
1680     while !ok
1681         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1682             push_mousecursor_wait(fc)
1683             msg = open_file(fc.filename)
1684             pop_mousecursor(fc)
1685             if msg
1686                 show_popup(fc, msg)
1687                 ok = false
1688             else
1689                 ok = true
1690             end
1691         else
1692             ok = true
1693         end
1694     end
1695     fc.destroy
1696 end
1697
1698 def additional_booh_options
1699     options = ''
1700     if $config['mproc']
1701         options += "--mproc #{$config['mproc'].to_i} "
1702     end
1703     return options
1704 end
1705
1706 def new_album
1707     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1708                              $main_window,
1709                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1710                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1711                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1712     
1713     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1714     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1715                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1716     tbl.attach(src = Gtk::Entry.new,
1717                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1718     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1719                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1720     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1721                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1722     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1723                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1724     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1725                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1726     tbl.attach(dest = Gtk::Entry.new,
1727                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1728     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1729                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1730     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1731                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1732     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1733                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1734     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1735                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1736
1737     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1738     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1739                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1740     tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1741                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1742     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1743                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1744     tbl.attach(sizes = Gtk::HBox.new,
1745                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1746
1747     src_nb_calculated_for = ''
1748     src_nb_thread = nil
1749     process_src_nb = Proc.new {
1750         if src.text != src_nb_calculated_for
1751             src_nb_calculated_for = src.text
1752             if src_nb_thread
1753                 Thread.kill(src_nb_thread)
1754                 src_nb_thread = nil
1755             end
1756             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1757                 if File.readable?(from_utf8(src_nb_calculated_for))
1758                     src_nb_thread = Thread.new {
1759                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1760                         total = { 'image' => 0, 'video' => 0, nil => 0 }
1761                         `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1762                             if File.basename(dir) =~ /^\./
1763                                 next
1764                             else
1765                                 begin
1766                                     Dir.entries(dir.chomp).each { |file|
1767                                         total[entry2type(file)] += 1
1768                                     }
1769                                 rescue Errno::EACCES, Errno::ENOENT
1770                                 end
1771                             end
1772                         }
1773                         src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1774                         src_nb_thread = nil
1775                     }
1776                 else
1777                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1778                 end
1779             else
1780                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1781             end
1782         end
1783         true
1784     }
1785     timeout_src_nb = Gtk.timeout_add(100) {
1786         process_src_nb.call
1787     }
1788
1789     src_browse.signal_connect('clicked') {
1790         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1791                                         nil,
1792                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
1793                                         nil,
1794                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1795         fc.transient_for = $main_window
1796         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1797             src.text = utf8(fc.filename)
1798             process_src_nb.call
1799             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1800         end
1801         fc.destroy
1802     }
1803
1804     dest_browse.signal_connect('clicked') {
1805         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1806                                         nil,
1807                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
1808                                         nil,
1809                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1810         fc.transient_for = $main_window
1811         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1812             dest.text = utf8(fc.filename)
1813         end
1814         fc.destroy
1815     }
1816
1817     conf_browse.signal_connect('clicked') {
1818         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1819                                         nil,
1820                                         Gtk::FileChooser::ACTION_SAVE,
1821                                         nil,
1822                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1823         fc.transient_for = $main_window
1824         fc.add_shortcut_folder(File.expand_path("~/.booh"))
1825         fc.set_current_folder(File.expand_path("~/.booh"))
1826         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1827             conf.text = utf8(fc.filename)
1828         end
1829         fc.destroy
1830     }
1831
1832     theme_sizes = []
1833     recreate_theme_config = proc {
1834         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1835         theme_sizes = []
1836         select_theme(theme_button.label, 'all')
1837         $images_size.each { |s|
1838             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1839             if !s['optional']
1840                 cb.active = true
1841             end
1842             tooltips = Gtk::Tooltips.new
1843             tooltips.set_tip(cb, utf8(s['description']), nil)
1844             theme_sizes << { :widget => cb, :value => s['name'] }
1845         }
1846         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1847         tooltips = Gtk::Tooltips.new
1848         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1849         theme_sizes << { :widget => cb, :value => 'original' }
1850         sizes.show_all
1851     }
1852     recreate_theme_config.call
1853
1854     theme_button.signal_connect('clicked') {
1855         if newtheme = theme_choose(theme_button.label)
1856             theme_button.label = newtheme
1857             recreate_theme_config.call
1858         end
1859     }
1860
1861     dialog.vbox.add(frame1)
1862     dialog.vbox.add(frame2)
1863     dialog.window_position = Gtk::Window::POS_MOUSE
1864     dialog.show_all
1865
1866     keepon = true
1867     ok = true
1868     while keepon
1869         dialog.run { |response|
1870             if response == Gtk::Dialog::RESPONSE_OK
1871                 srcdir = from_utf8(src.text)
1872                 destdir = from_utf8(dest.text)
1873                 if !File.directory?(srcdir)
1874                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1875                     src.grab_focus
1876                 elsif conf.text == ''
1877                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1878                     conf.grab_focus
1879                 elsif destdir != make_dest_filename(destdir)
1880                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1881                     dest.grab_focus
1882                 elsif File.directory?(destdir)
1883                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1884                     dest.grab_focus
1885                 elsif File.exists?(destdir)
1886                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1887                     dest.grab_focus
1888                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1889                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1890                 else
1891                     system("mkdir '#{destdir}'")
1892                     if !File.directory?(destdir)
1893                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1894                         dest.grab_focus
1895                     else
1896                         keepon = false
1897                     end
1898                 end
1899             else
1900                 keepon = ok = false
1901             end
1902         }
1903     end
1904     srcdir = from_utf8(src.text)
1905     destdir = from_utf8(dest.text)
1906     configskel = File.expand_path(from_utf8(conf.text))
1907     theme = theme_button.label
1908     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1909     dialog.destroy
1910     if src_nb_thread
1911         Thread.kill(src_nb_thread)
1912     end
1913     Gtk.timeout_remove(timeout_src_nb)
1914
1915     if ok
1916         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1917                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1918                      utf8(_("Please wait while scanning source directory...")),
1919                      'full scan',
1920                      { :closure_after => proc { open_file(configskel) } })
1921     end
1922 end
1923
1924 def properties
1925     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1926                              $main_window,
1927                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1928                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1929                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1930     
1931     source = $xmldoc.root.attributes['source']
1932     dest = $xmldoc.root.attributes['destination']
1933     theme = $xmldoc.root.attributes['theme']
1934     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1935     if limit_sizes
1936         limit_sizes = limit_sizes.split(/,/)
1937     end
1938
1939     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1940     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1941                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1942     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1943                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1944     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1945                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1946     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1947                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1948     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1949                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1950     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1951                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1952
1953     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1954     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1955                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1956     tbl.attach(theme_button = Gtk::Button.new(theme),
1957                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1958     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1959                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1960     tbl.attach(sizes = Gtk::HBox.new,
1961                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1962
1963     theme_sizes = []
1964     recreate_theme_config = proc {
1965         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1966         theme_sizes = []
1967         select_theme(theme_button.label, 'all')
1968         $images_size.each { |s|
1969             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1970             if limit_sizes
1971                 if limit_sizes.include?(s['name'])
1972                     cb.active = true
1973                 end
1974             else
1975                 if !s['optional']
1976                     cb.active = true
1977                 end
1978             end
1979             tooltips = Gtk::Tooltips.new
1980             tooltips.set_tip(cb, utf8(s['description']), nil)
1981             theme_sizes << { :widget => cb, :value => s['name'] }
1982         }
1983         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1984         tooltips = Gtk::Tooltips.new
1985         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1986         if limit_sizes && limit_sizes.include?('original')
1987             cb.active = true
1988         end
1989         theme_sizes << { :widget => cb, :value => 'original' }
1990         sizes.show_all
1991     }
1992     recreate_theme_config.call
1993
1994     theme_button.signal_connect('clicked') {
1995         if newtheme = theme_choose(theme_button.label)
1996             limit_sizes = nil
1997             theme_button.label = newtheme
1998             recreate_theme_config.call
1999         end
2000     }
2001
2002     dialog.vbox.add(frame1)
2003     dialog.vbox.add(frame2)
2004     dialog.window_position = Gtk::Window::POS_MOUSE
2005     dialog.show_all
2006
2007     keepon = true
2008     ok = true
2009     while keepon
2010         dialog.run { |response|
2011             if response == Gtk::Dialog::RESPONSE_OK
2012                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2013                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2014                 else
2015                     keepon = false
2016                 end
2017             else
2018                 keepon = ok = false
2019             end
2020         }
2021     end
2022     save_theme = theme_button.label
2023     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2024     dialog.destroy
2025
2026     if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
2027         save_current_file
2028         call_backend("booh-backend --use-config '#{$filename}' --for-gui " +
2029                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
2030                      utf8(_("Please wait while scanning source directory...")),
2031                      'full scan',
2032                      { :closure_after => proc { open_file($filename) } })
2033     end
2034 end
2035
2036 def merge_current
2037     save_current_file
2038
2039     sel = $albums_tv.selection.selected_rows
2040
2041     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2042                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2043                  utf8(_("Please wait while scanning source directory...")),
2044                  'one dir scan',
2045                  { :closure_after => proc {
2046                          open_file($filename)
2047                          $albums_tv.selection.select_path(sel[0])
2048                      } })
2049 end
2050
2051 def merge
2052     save_current_file
2053
2054     theme = $xmldoc.root.attributes['theme']
2055     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2056     if limit_sizes
2057         limit_sizes = "--sizes #{limit_sizes}"
2058     end
2059     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2060                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2061                  utf8(_("Please wait while scanning source directory...")),
2062                  'full scan',
2063                  { :closure_after => proc { open_file($filename) } })
2064 end
2065
2066 def merge_subalbums
2067     save_current_file
2068
2069     call_backend("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
2070                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2071                  utf8(_("Please wait while scanning source directory...")),
2072                  'full scan',
2073                  { :closure_after => proc { open_file($filename) } })
2074 end
2075
2076 def save_as_do
2077     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2078                                     nil,
2079                                     Gtk::FileChooser::ACTION_SAVE,
2080                                     nil,
2081                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2082     fc.transient_for = $main_window
2083     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2084     fc.set_current_folder(File.expand_path("~/.booh"))
2085     fc.filename = $filename
2086     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2087         $filename = fc.filename
2088         save_current_file
2089     end
2090     fc.destroy
2091 end
2092
2093 def preferences
2094     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2095                              $main_window,
2096                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2097                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2098                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2099
2100     dialog.vbox.add(notebook = Gtk::Notebook.new)
2101     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2102     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2103                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2104     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2105                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2106     tooltips = Gtk::Tooltips.new
2107     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
2108     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2109                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2110     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)),
2111                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2112     smp_check.signal_connect('toggled') {
2113         if smp_check.active?
2114             smp_hbox.sensitive = true
2115         else
2116             smp_hbox.sensitive = false
2117         end
2118     }
2119     if $config['mproc']
2120         smp_check.active = true
2121         smp_spin.value = $config['mproc'].to_i
2122     end
2123
2124     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2125     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2126                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2127     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2128                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2129
2130     dialog.vbox.show_all
2131     dialog.run { |response|
2132         if response == Gtk::Dialog::RESPONSE_OK
2133             $config['video-viewer'] = video_viewer_entry.text
2134             if smp_check.active?
2135                 $config['mproc'] = smp_spin.value.to_i
2136             else
2137                 $config.delete('mproc')
2138             end
2139
2140             $config['convert-enhance'] = enhance_entry.text
2141         end
2142     }
2143     dialog.destroy
2144 end
2145
2146 def create_menu_and_toolbar
2147
2148     #- menu
2149     mb = Gtk::MenuBar.new
2150
2151     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
2152     filesubmenu = Gtk::Menu.new
2153     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
2154     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
2155     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2156     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
2157     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
2158     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2159     tooltips = Gtk::Tooltips.new
2160     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
2161     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2162     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
2163     filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums"))).set_sensitive(false))
2164     $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2165     tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory (but don't touch existing subalbums)")), nil)
2166     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
2167     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2168     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
2169     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2170     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
2171     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
2172     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
2173     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2174     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
2175     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2176     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2177     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2178     filemenu.set_submenu(filesubmenu)
2179     mb.append(filemenu)
2180
2181     new.signal_connect('activate') { new_album }
2182     open.signal_connect('activate') { open_file_popup }
2183     $save.signal_connect('activate') { save_current_file }
2184     $save_as.signal_connect('activate') { save_as_do }
2185     $merge_current.signal_connect('activate') { merge_current }
2186     $merge.signal_connect('activate') { merge }
2187     $merge_subalbums.signal_connect('activate') { merge_subalbums }
2188     $generate.signal_connect('activate') {
2189         save_current_file
2190         call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2191                      utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2192                      'web-album',
2193                      { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2194                        :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")) })
2195     }
2196     $properties.signal_connect('activate') { properties }
2197
2198     quit.signal_connect('activate') { try_quit }
2199
2200     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2201     editsubmenu = Gtk::Menu.new
2202     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2203     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2204     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2205     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2206     editmenu.set_submenu(editsubmenu)
2207     mb.append(editmenu)
2208
2209     prefs.signal_connect('activate') { preferences }
2210
2211     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2212     helpsubmenu = Gtk::Menu.new
2213     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2214     helpmenu.set_submenu(helpsubmenu)
2215     mb.append(helpmenu)
2216
2217     about.signal_connect('activate') {
2218             show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2219
2220 <i>``The Web-Album of choice for discriminating Linux users''</i>
2221
2222 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
2223     }
2224
2225
2226     #- toolbar
2227     tb = Gtk::Toolbar.new
2228
2229     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2230     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
2231     open.menu = Gtk::Menu.new
2232     open.signal_connect('clicked') { open_file_popup }
2233     open.signal_connect('show-menu') {
2234         lastopens = Gtk::Menu.new
2235         j = 0
2236         if $config['last-opens']
2237             $config['last-opens'].reverse.each { |e|
2238                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2239                 item.signal_connect('activate') {
2240                     push_mousecursor_wait
2241                     msg = open_file(from_utf8(e))
2242                     pop_mousecursor
2243                     if msg
2244                         show_popup($main_window, msg)
2245                     end
2246                 }
2247                 j += 1
2248             }
2249             lastopens.show_all
2250         end
2251         open.menu = lastopens
2252     }
2253
2254     tb.insert(-1, Gtk::SeparatorToolItem.new)
2255
2256     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2257     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2258     $r90.label = utf8(_("Rotate"))
2259     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2260     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2261     $r270.label = utf8(_("Rotate"))
2262     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2263     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2264     $enhance.label = utf8(_("Enhance"))
2265     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2266     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
2267     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2268     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2269     nothing.label = utf8(_("None"))
2270
2271     tb.insert(-1, Gtk::SeparatorToolItem.new)
2272
2273     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2274     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2275
2276     perform_undo = Proc.new {
2277         $redo_tb.sensitive = $redo_mb.sensitive = true
2278         if not more_undoes = UndoHandler.undo($statusbar)
2279             $undo_tb.sensitive = $undo_mb.sensitive = false
2280         end
2281     }
2282     perform_redo = Proc.new {
2283         $undo_tb.sensitive = $undo_mb.sensitive = true
2284         if not more_redoes = UndoHandler.redo($statusbar)
2285             $redo_tb.sensitive = $redo_mb.sensitive = false
2286         end
2287     }
2288
2289     $undo_tb.signal_connect('clicked')  { perform_undo.call }
2290     $undo_mb.signal_connect('activate') { perform_undo.call }
2291     $redo_tb.signal_connect('clicked')  { perform_redo.call }
2292     $redo_mb.signal_connect('activate') { perform_redo.call }
2293
2294     one_click_explain_try = Proc.new {
2295         if !$config['one-click-explained']
2296             show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2297
2298 You have just clicked on a One-Click tool. When such a tool is activated
2299 (<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
2300 on a thumbnail will immediately apply the desired action.
2301
2302 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2303 ")))
2304             $config['one-click-explained'] = true
2305         end
2306     }
2307
2308     $r90.signal_connect('toggled') {
2309         if $r90.active?
2310             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2311             one_click_explain_try.call
2312             $r270.active = false
2313             $enhance.active = false
2314             $delete.active = false
2315             nothing.sensitive = true
2316         else
2317             if !$r270.active? && !$enhance.active? && !$delete.active?
2318                 set_mousecursor_normal
2319                 nothing.sensitive = false
2320             else
2321                 nothing.sensitive = true
2322             end
2323         end
2324     }
2325     $r270.signal_connect('toggled') {
2326         if $r270.active?
2327             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2328             one_click_explain_try.call
2329             $r90.active = false
2330             $enhance.active = false
2331             $delete.active = false
2332             nothing.sensitive = true
2333         else
2334             if !$r90.active? && !$enhance.active? && !$delete.active?
2335                 set_mousecursor_normal
2336                 nothing.sensitive = false
2337             else
2338                 nothing.sensitive = true
2339             end
2340         end
2341     }
2342     $enhance.signal_connect('toggled') {
2343         if $enhance.active?
2344             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2345             one_click_explain_try.call
2346             $r90.active = false
2347             $r270.active = false
2348             $delete.active = false
2349             nothing.sensitive = true
2350         else
2351             if !$r90.active? && !$r270.active? && !$delete.active?
2352                 set_mousecursor_normal
2353                 nothing.sensitive = false
2354             else
2355                 nothing.sensitive = true
2356             end
2357         end
2358     }
2359     $delete.signal_connect('toggled') {
2360         if $delete.active?
2361             set_mousecursor(Gdk::Cursor::PIRATE)
2362             one_click_explain_try.call
2363             $r90.active = false
2364             $r270.active = false
2365             $enhance.active = false
2366             nothing.sensitive = true
2367         else
2368             if !$r90.active? && !$r270.active? && !$enhance.active?
2369                 set_mousecursor_normal
2370                 nothing.sensitive = false
2371             else
2372                 nothing.sensitive = true
2373             end
2374         end
2375     }
2376     nothing.signal_connect('clicked') {
2377         $r90.active = $r270.active = $enhance.active = $delete.active = false
2378         set_mousecursor_normal
2379     }
2380
2381     return [ mb, tb ]
2382 end
2383
2384 def create_main_window
2385
2386     mb, tb = create_menu_and_toolbar
2387
2388 #    open_file('/home/gc/booh/foo')
2389
2390     $albums_tv = Gtk::TreeView.new
2391     $albums_tv.set_size_request(120, -1)
2392     renderer = Gtk::CellRendererText.new
2393     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2394     $albums_tv.append_column(column)
2395     $albums_tv.set_headers_visible(false)
2396     $albums_tv.selection.signal_connect('changed') { |w|
2397         push_mousecursor_wait
2398         save_changes
2399         iter = w.selected
2400         if !iter
2401             msg 3, "no selection"
2402         else
2403             $current_path = $albums_ts.get_value(iter, 1)
2404             change_dir
2405         end
2406         pop_mousecursor
2407     }
2408     $albums_ts = Gtk::TreeStore.new(String, String)
2409     $albums_tv.set_model($albums_ts)
2410     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2411
2412     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2413     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2414     albums_sw.add_with_viewport($albums_tv)
2415
2416     $notebook = Gtk::Notebook.new
2417     create_subalbums_page
2418     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2419     create_auto_table
2420     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2421     $notebook.show_all
2422     $notebook.signal_connect('switch-page') { |w, page, num|
2423         if num == 0
2424             $delete.active = false
2425             $delete.sensitive = false
2426         else
2427             $delete.sensitive = true
2428         end
2429     }
2430
2431     paned = Gtk::HPaned.new
2432     paned.pack1(albums_sw, false, false)
2433     paned.pack2($notebook, true, true)
2434
2435     main_vbox = Gtk::VBox.new(false, 0)
2436     main_vbox.pack_start(mb, false, false)
2437     main_vbox.pack_start(tb, false, false)
2438     main_vbox.pack_start(paned, true, true)
2439     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2440
2441     $main_window = Gtk::Window.new
2442     $main_window.add(main_vbox)
2443     $main_window.signal_connect('delete-event') {
2444         try_quit
2445     }
2446
2447     #- read/save size and position of window
2448     if $config['pos-x'] && $config['pos-y']
2449         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2450     else
2451         $main_window.window_position = Gtk::Window::POS_CENTER
2452     end
2453     msg 3, "size: #{$config['width']}x#{$config['height']}"
2454     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2455     $main_window.signal_connect('configure-event') {
2456         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2457         x, y = $main_window.window.root_origin
2458         width, height = $main_window.window.size
2459         $config['pos-x'] = x
2460         $config['pos-y'] = y
2461         $config['width'] = width
2462         $config['height'] = height
2463         false
2464     }
2465
2466     $statusbar.push(0, utf8(_("Ready.")))
2467     $main_window.show_all
2468 end
2469
2470 Thread.abort_on_exception = true
2471 handle_options
2472 read_config
2473
2474 Gtk.init
2475 create_main_window
2476 if ARGV[0]
2477     open_file(ARGV[0])
2478 end
2479 Gtk.main
2480
2481 write_config