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