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