5531b71ff4564d9247f2ec68e900dbb153a42290
[booh] / bin / booh-gui
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
38
39 #- options
40 $options = [
41     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
42
43     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
44 ]
45
46 def usage
47     puts _("Usage: %s [OPTION]...") % File.basename($0)
48     $options.each { |ary|
49         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
50     }
51 end
52
53 def handle_options
54     parser = GetoptLong.new
55     parser.set_options(*$options.collect { |ary| ary[0..2] })
56     begin
57         parser.each_option do |name, arg|
58             case name
59             when '--help'
60                 usage
61                 exit(0)
62
63             when '--verbose-level'
64                 $verbose_level = arg.to_i
65
66             end
67         end
68     rescue
69         puts $!
70         usage
71         exit(1)
72     end
73 end
74
75 def read_config
76     $config = {}
77     $config_file = File.expand_path('~/.booh-gui-rc')
78     if File.readable?($config_file)
79         $xmldoc = REXML::Document.new(File.new($config_file))
80         $xmldoc.root.elements.each { |element|
81             txt = element.get_text
82             $config[element.name] = txt ? txt.value : nil
83         }
84     end
85     if !FileTest.directory?(File.expand_path('~/.booh-gui-files'))
86         system("mkdir ~/.booh-gui-files")
87     end
88 end
89
90 def write_config
91     ios = File.open($config_file, "w")
92     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
93     $xmldoc << XMLDecl.new( XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET )
94     $config.each_pair { |key, value|
95     elem = $xmldoc.root.add_element key
96         elem.add_text value.to_s
97     }
98     $xmldoc.write(ios, 0)
99     ios.close
100 end
101
102 def set_mousecursor(widget, what)
103     if !widget.window
104         widget.realize
105     end
106     widget.window.set_cursor(Gdk::Cursor.new(what))
107 end
108 def set_mousecursor_wait(widget)
109     set_mousecursor(widget, Gdk::Cursor::WATCH)
110     Gtk.main_iteration while Gtk.events_pending?
111 end
112 def set_mousecursor_normal(widget)
113     set_mousecursor(widget, Gdk::Cursor::LEFT_PTR)
114 end
115
116 def current_dest_dir
117     source = $xmldoc.root.attributes['source']
118     dest = $xmldoc.root.attributes['destination']
119     return make_dest_filename(from_utf8($current_path).sub(/^#{Regexp.quote(source)}/, dest))
120 end
121
122 def build_full_dest_filename(filename)
123     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
124 end
125
126 def view_element(filename)
127     if entry2type(filename) == 'video'
128         system("mplayer #{$current_path + '/' + filename}")
129         return
130     end
131
132     w = Gtk::Window.new
133
134     msg 3, "filename: #{filename}"
135     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
136     #- typically this file won't exist in case of videos; try with the largest thumbnail around
137     if !File.exists?(dest_img)
138         if entry2type(filename) == 'video'
139             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
140             if not alternatives.empty?
141                 dest_img = alternatives[-1]
142             end
143         else
144             set_mousecursor_wait($main_window)
145             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
146             set_mousecursor_normal($main_window)
147             if !File.exists?(dest_img)
148                 msg 2, _("Could not generate fullscreen thumbnail!")
149                 return
150                 end
151         end
152     end
153     i = Gtk::Image.new(dest_img)
154     f = Gtk::Frame.new
155     f.add(i)
156     f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
157     hb = Gtk::HBox.new
158     hb.pack_start(Gtk::Label.new, true, true)
159     hb.pack_start(f, false, false)
160     hb.pack_end(Gtk::Label.new, true, true)
161     evt = Gtk::EventBox.new
162     evt.add(hb)
163
164     tooltips = Gtk::Tooltips.new
165     tooltips.set_tip(evt, utf8(File.basename(filename).gsub(/\.jpg/, '')), nil)
166     
167     hb2 = Gtk::HBox.new
168     hb2.pack_start(Gtk::Label.new, true, true)
169     hb2.pack_start(b = Gtk::Button.new(Gtk::Stock::CLOSE), false, false)
170     b.signal_connect('clicked') { w.destroy }
171     hb2.pack_end(Gtk::Label.new, true, true)
172
173     vb = Gtk::VBox.new
174     vb.pack_start(evt, false, false)
175     vb.pack_end(hb2, false, false)
176
177     w.add(vb)
178     w.signal_connect('delete-event') { w.destroy }
179     w.window_position = Gtk::Window::POS_CENTER
180     w.show_all
181 end
182
183 def create_editzone(scrolledwindow)
184     frame = Gtk::Frame.new
185     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
186     frame.set_shadow_type(Gtk::SHADOW_IN)
187     textview.signal_connect('key-press-event') { |w, event|
188         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
189         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
190             scrolledwindow.signal_emit('key-press-event', event)
191         end
192         false  #- propagate
193     }
194     textview.signal_connect('focus-in-event') { |w, event|
195         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
196         false  #- propagate
197     }
198
199     return [ frame, textview ]
200 end
201
202 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
203     #- update rotate attribute
204     xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = ( xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle ) % 360)
205
206     if !$rotated_pixbufs[thumbnail_img]
207         $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
208     else
209         $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ( $rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle ) % 360
210     end
211     msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
212
213     #- rotate shown thumbnail
214     pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
215     msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
216     if pixbuf.height > desired_y
217         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
218                                                                              Gdk::Pixbuf::INTERP_BILINEAR)
219     elsif pixbuf.width < desired_x && pixbuf.height < desired_y
220         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
221                                                                              Gdk::Pixbuf::INTERP_BILINEAR)
222     else
223         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
224     end
225 end
226
227 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
228
229     frame1 = Gtk::Frame.new
230     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
231     if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
232         frame1.add(img = Gtk::Image.new)
233         Thread.new {
234             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => thumbnail_img, 'size' => $default_size['thumbnails'] } ])
235             img.set(thumbnail_img)
236         }
237     else
238         frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
239     end
240     frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
241     hbox1 = Gtk::HBox.new
242     hbox1.pack_start(Gtk::Label.new, true, true)
243     hbox1.pack_start(frame1, false, false)
244     hbox1.pack_end(Gtk::Label.new, true, true)
245     evtbox = Gtk::EventBox.new
246     evtbox.add(hbox1)
247
248     tooltips = Gtk::Tooltips.new
249     tipname = File.basename(thumbnail_img).gsub(/-\d+x\d+\.jpg/, '')
250     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(File.size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
251
252     frame2, textview = create_editzone($autotable_sw)
253     textview.buffer.text = utf8(caption)
254     textview.set_justification(Gtk::Justification::CENTER)
255
256     vbox = Gtk::VBox.new(false, 5)
257     vbox.pack_start(evtbox, false, false)
258     vbox.pack_start(frame2, false, false)
259     autotable.append(vbox, filename)
260
261     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
262     $vbox2textview[vbox] = textview
263
264     #- to be able to find widgets by name
265     $name2widgets[filename] = { :textview => textview }
266
267     rotate_and_cleanup = Proc.new { |angle|
268         rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
269
270         #- remove out of sync images
271         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
272         for sizeobj in $images_size
273             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
274         end
275     }
276
277     delete = Proc.new {
278         after = autotable.get_next_widget(vbox)
279         if !after
280             after = autotable.get_previous_widget(vbox)
281         end
282         autotable.remove(vbox)
283         if after
284             $vbox2textview[after].grab_focus
285         end
286     }
287
288     textview.signal_connect('key-press-event') { |w, event|
289         propagate = true
290         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
291             $autotable_sw.signal_emit('key-press-event', event)
292         end
293         if event.state != 0
294             x, y = autotable.get_current_pos(vbox)
295             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
296             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
297             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
298             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
299                 if control_pressed
300                     $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
301                 end
302                 if shift_pressed
303                     autotable.move_up(vbox)
304                     textview.grab_focus  #- because if moving, focus is stolen
305                 end
306             end
307             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
308                 if control_pressed
309                     $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
310                 end
311                 if shift_pressed
312                     autotable.move_down(vbox)
313                     textview.grab_focus  #- because if moving, focus is stolen
314                 end
315             end
316             if event.keyval == Gdk::Keyval::GDK_Left
317                 previous = autotable.get_previous_widget(vbox)
318                 if previous && autotable.get_current_pos(previous)[0] < x
319                     if control_pressed
320                         $vbox2textview[previous].grab_focus
321                     end
322                     if shift_pressed
323                         autotable.move_left(vbox)
324                         textview.grab_focus  #- because if moving, focus is stolen
325                     end
326                 end
327                 if alt_pressed
328                     rotate_and_cleanup.call(-90)
329                 end
330             end
331             if event.keyval == Gdk::Keyval::GDK_Right
332                 next_ = autotable.get_next_widget(vbox)
333                 if next_ && autotable.get_current_pos(next_)[0] > x
334                     if control_pressed
335                         $vbox2textview[next_].grab_focus
336                     end
337                     if shift_pressed
338                         autotable.move_right(vbox)
339                         textview.grab_focus  #- because if moving, focus is stolen
340                     end
341                 end
342                 if alt_pressed
343                     rotate_and_cleanup.call(90)
344                 end
345             end
346             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
347                 delete.call
348             end
349             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
350                 view_element(filename)
351                 propagate = false
352             end
353         end
354         !propagate  #- propagate if needed
355     }
356
357     evtbox.signal_connect('button-press-event') { |w, event|
358         retval = true
359         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
360             if $delete.active?
361                 delete.call
362             elsif $r90.active?
363                 rotate_and_cleanup.call(90)
364             elsif $r270.active?
365                 rotate_and_cleanup.call(270)
366             else
367                 textview.grab_focus
368             end
369         end
370         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
371             view_element(filename)
372         else
373             retval = false  #- propagate
374         end
375         retval
376     }
377
378     vbox.signal_connect('button-press-event') { |w, event|
379         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
380             $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
381         end
382         false
383     }
384
385     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
386         if $gesture_press && $gesture_press[:filename] == filename
387             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
388                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
389                 msg 3, "gesture rotate: #{angle}"
390                 rotate_and_cleanup.call(90)
391             end
392         end
393         $gesture_press = nil
394     }
395
396     vbox.show_all
397 end
398
399 def create_auto_table
400
401     $autotable = Gtk::AutoTable.new(5)
402
403     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
404     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
405     $autotable_sw.add_with_viewport($autotable)
406 end
407
408 def create_subalbums_page
409
410     subalbums_hb = Gtk::HBox.new
411 #    subalbums_hb.pack_start(Gtk::Label.new, true, true)
412     $subalbums_vb = Gtk::VBox.new(false, 5)
413     subalbums_hb.pack_start($subalbums_vb, false, false)
414     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
415     $subalbums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
416     $subalbums_sw.add_with_viewport(subalbums_hb)
417 end
418
419 def try_quit
420     save_changes
421     if $filename
422         ios = File.open($filename, "w")
423         $xmldoc.write(ios, 0)
424         ios.close
425     end
426     Gtk.main_quit
427 end
428
429 def show_popup(parent, msg)
430     dialog = Gtk::Dialog.new
431     dialog.title = utf8(_("Booh message"))
432     lbl = Gtk::Label.new
433     lbl.markup = msg
434     dialog.vbox.add(lbl)
435     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
436
437     dialog.transient_for = parent
438     dialog.set_default_size(200, 120)
439     dialog.window_position = Gtk::Window::POS_CENTER_ON_PARENT
440     dialog.show_all
441
442     dialog.run
443     dialog.destroy
444 end
445
446 def save_changes
447     if !$current_path
448         return
449     end
450
451     if $xmldir.elements['dir']
452         $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
453         $xmldir.elements.each('dir') { |element|
454             path = element.attributes['path']
455             if element.attributes['subdirs-caption']
456                 element.add_attribute('subdirs-caption',     $subalbums_edits[path][:editzone].buffer.text)
457                 element.add_attribute('subdirs-captionfile', $subalbums_edits[path][:captionfile])
458             else
459                 element.add_attribute('thumbnails-caption',     $subalbums_edits[path][:editzone].buffer.text)
460                 element.add_attribute('thumbnails-captionfile', $subalbums_edits[path][:captionfile])
461             end
462         }
463         if $xmldir.attributes['thumbnails-caption']
464             path = $xmldir.attributes['path']
465             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
466         end
467     end
468
469     #- remove and reinsert elements to reflect new ordering
470     save_attributes = {}
471     save_types = {}
472     $xmldir.elements.each { |element|
473         if element.name == 'image' || element.name == 'video'
474             save_types[element.attributes['filename']] = element.name
475             save_attributes[element.attributes['filename']] = element.attributes
476             element.remove
477         end
478     }
479     $autotable.current_order.each { |path|
480         chld = $xmldir.add_element save_types[path], save_attributes[path]
481         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
482     }
483 end
484
485 def change_dir
486     $autotable.clear
487     $vbox2textview = {}
488     $name2widgets = {}
489
490     $subalbums_vb.children.each { |chld|
491         $subalbums_vb.remove(chld)
492     }
493     $subalbums = Gtk::Table.new(0, 0, true)
494     current_y_sub_albums = 0
495
496     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
497     $subalbums_edits = {}
498     add_subalbum = Proc.new { |xmldir|
499         if xmldir == $xmldir
500             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
501             caption = xmldir.attributes['thumbnails-caption']
502             captionfile, dummy = find_subalbum_caption_info(xmldir)
503         else
504             thumbnail_file = "#{current_dest_dir}/thumbnails-#{from_utf8(File.basename(xmldir.attributes['path']))}.jpg"
505             captionfile, caption = find_subalbum_caption_info(xmldir)
506         end
507         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
508         hbox = Gtk::HBox.new
509         hbox.pack_start(Gtk::Label.new, true, true)
510         hbox.pack_start(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>'), false, false)
511         f = Gtk::Frame.new
512         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
513
514         if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
515             f.add(img = Gtk::Image.new)
516             Thread.new {
517                 infotype = xmldir == $xmldir ? 'thumbnails' : find_subalbum_info_type(xmldir)
518                 gen_thumbnails_subdir(from_utf8(captionfile), xmldir, false,
519                                       [ { 'filename' => thumbnail_file, 'size' => $albums_thumbnail_size } ], infotype)
520                 img.set(thumbnail_file)
521             }
522         else
523             f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
524         end
525         hbox.pack_end(evtbox = Gtk::EventBox.new.add(f), false, false)
526         $subalbums.attach(hbox,
527                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
528
529         rotate_and_cleanup = Proc.new { |angle|
530             rotate(angle, thumbnail_file, img, xmldir, ( xmldir == $xmldir ? 'thumbnails' : find_subalbum_info_type(xmldir) ) + '-',
531                    $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
532             system("rm -f '#{thumbnail_file}'")
533         }
534
535         evtbox.signal_connect('button-press-event') { |w, event|
536             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
537                 if $r90.active?
538                     rotate_and_cleanup.call(90)
539                 end
540                 if $r270.active?
541                     rotate_and_cleanup.call(270)
542                 end
543             end
544             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
545                 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
546                                                 nil,
547                                                 Gtk::FileChooser::ACTION_OPEN,
548                                                 nil,
549                                                 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
550                 fc.set_current_folder(xmldir.attributes['path'])
551                 fc.transient_for = $main_window
552                 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))
553                 f.add(preview_img = Gtk::Image.new)
554                 preview.show_all
555                 fc.signal_connect('update-preview') { |w|
556                     begin
557                         if fc.preview_filename
558                             preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
559                             fc.preview_widget_active = true
560                         end
561                     rescue Gdk::PixbufError
562                         fc.preview_widget_active = false
563                     end
564                 }
565                 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
566                     msg 3, "new captionfile is: #{fc.filename}"
567                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = utf8(fc.filename)
568                     Thread.new(fc.filename) { |filename|
569                         system("rm -f '#{thumbnail_file}'")
570                         $rotated_pixbufs.delete(thumbnail_file)
571                         infotype = xmldir == $xmldir ? 'thumbnails' : find_subalbum_info_type(xmldir)
572                         xmldir.delete_attribute("#{infotype}-rotate")
573                         gen_thumbnails_subdir(filename, xmldir, false,
574                                               [ { 'filename' => thumbnail_file, 'size' => $albums_thumbnail_size } ], infotype)
575                         img.set(thumbnail_file)
576                     }
577                 end
578                 fc.destroy
579                 true   #- handled
580             end
581         }
582         evtbox.signal_connect('button-press-event') { |w, event|
583             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
584             false
585         }
586
587         evtbox.signal_connect('button-release-event') { |w, event|
588             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
589                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
590                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
591                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
592                     msg 3, "gesture rotate: #{angle}"
593                     rotate_and_cleanup.call(angle)
594                 end
595             end
596             $gesture_press = nil
597         }
598         
599         vbox = Gtk::VBox.new
600         vbox.pack_start(Gtk::Label.new, true, true)
601         frame, textview = create_editzone($subalbums_sw)
602         textview.buffer.text = caption
603         vbox.pack_start(frame, false, false)
604         vbox.pack_end(Gtk::Label.new, true, true)
605         $subalbums.attach(vbox,
606                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
607         
608         $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
609         current_y_sub_albums += 1
610     }
611
612     if $xmldir.elements['dir']
613         #- title edition
614         frame, $subalbums_title = create_editzone($subalbums_sw)
615         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
616         $subalbums_title.set_justification(Gtk::Justification::CENTER)
617         hbox = Gtk::HBox.new
618         hbox.add(Gtk::Label.new)
619         hbox.add(frame)
620         hbox.add(Gtk::Label.new)
621         $subalbums_vb.pack_start(hbox, false, false)
622         #- this album image/caption
623         if $xmldir.attributes['thumbnails-caption']
624             add_subalbum.call($xmldir)
625         end
626     end
627     $xmldir.elements.each { |element|
628         if element.name == 'image' || element.name == 'video'
629             #- element (image or video) of this album
630             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
631             msg 3, "dest_img: #{dest_img}"
632             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
633         end
634         if element.name == 'dir'
635             #- sub-album image/caption
636             add_subalbum.call(element)
637         end
638     }
639     $subalbums_vb.add($subalbums)
640     $subalbums_vb.show_all
641
642     if !$xmldir.elements['image'] && !$xmldir.elements['video']
643         $notebook.get_tab_label($autotable_sw).sensitive = false
644         $notebook.set_page(0)
645     else
646         $notebook.get_tab_label($autotable_sw).sensitive = true
647     end
648
649     if !$xmldir.elements['dir']
650         $notebook.get_tab_label($subalbums_sw).sensitive = false
651         $notebook.set_page(1)
652     else
653         $notebook.get_tab_label($subalbums_sw).sensitive = true
654     end
655 end
656
657 def open_file(filename)
658     $filename = nil
659     $current_path = nil   #- invalidate
660     $rotated_pixbufs = {}
661
662     begin
663         $xmldoc = REXML::Document.new File.new(filename)
664     rescue
665         $xmldoc = nil
666     end
667
668     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
669         return utf8(_("Not a booh file!"))
670     end
671
672     if !source = $xmldoc.root.attributes['source']
673         return utf8(_("Corrupted booh file..."))
674     end
675
676     if !dest = $xmldoc.root.attributes['destination']
677         return utf8(_("Corrupted booh file..."))
678     end
679
680     if !theme = $xmldoc.root.attributes['theme']
681         return utf8(_("Corrupted booh file..."))
682     end
683
684     $filename = filename
685     select_theme(theme)
686     $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
687     $default_size['thumbnails'] =~ /(.*)x(.*)/
688     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
689     $albums_thumbnail_size =~ /(.*)x(.*)/
690     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
691
692     msg 3, "source: #{source}"
693
694     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
695     if !xmldir
696         return utf8(_("Corrupted booh file..."))
697     end
698
699     $albums_ts.clear
700
701     append_dir_elem = Proc.new { |parent_iter, location|
702         child_iter = $albums_ts.append(parent_iter)
703         child_iter[0] = File.basename(location)
704         child_iter[1] = location
705         msg 3, "puttin location: #{location}"
706         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
707             append_dir_elem.call(child_iter, elem.attributes['path'])
708         }
709     }
710     append_dir_elem.call(nil, source)
711
712     $albums_tv.expand_all
713     $albums_tv.selection.select_iter($albums_ts.iter_first)
714
715     return nil
716 end
717
718 def open_file_popup
719     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
720                                     nil,
721                                     Gtk::FileChooser::ACTION_OPEN,
722                                     nil,
723                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
724     fc.add_shortcut_folder(File.expand_path("~/.booh-gui-files"))
725     fc.set_current_folder(File.expand_path("~/.booh-gui-files"))
726     fc.transient_for = $main_window
727     ok = false
728     while !ok
729         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
730             set_mousecursor_wait(fc)
731             msg = open_file(fc.filename)
732             set_mousecursor_normal(fc)
733             if msg
734                 show_popup(fc, msg)
735                 ok = false
736             else
737                 ok = true
738             end
739         else
740             ok = true
741         end
742     end
743     fc.destroy
744 end
745
746 def create_menu
747     mb = Gtk::MenuBar.new
748
749     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
750     filesubmenu = Gtk::Menu.new
751     filesubmenu.append(new     = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
752     filesubmenu.append(open    = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
753     filesubmenu.append(          Gtk::SeparatorMenuItem.new)
754     filesubmenu.append(save    = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE))
755     filesubmenu.append(save    = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS))
756     filesubmenu.append(          Gtk::SeparatorMenuItem.new)
757     filesubmenu.append(quit    = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
758     filemenu.set_submenu(filesubmenu)
759     mb.append(filemenu)
760
761     open.signal_connect('activate') { open_file_popup }
762     quit.signal_connect('activate') { try_quit }
763
764     return mb
765 end
766
767 def create_toolbar
768     tb = Gtk::Toolbar.new
769
770     tb.insert(-1, open = Gtk::ToolButton.new(Gtk::Stock::OPEN))
771     open.label = utf8(_("Open"))
772
773     tb.insert(-1, Gtk::SeparatorToolItem.new)
774
775     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
776     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
777     $r90.label = utf8(_("Rotate"))
778     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
779     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
780     $r270.label = utf8(_("Rotate"))
781     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
782     $delete.label = utf8(_("Delete"))
783     tb.insert(-1, nothing = Gtk::ToolButton.new(''))
784     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
785     nothing.label = utf8(_("None"))
786
787     open.signal_connect('clicked') { open_file_popup }
788
789     one_click_explain_try = Proc.new {
790         if !$config['one-click-explained']
791             show_popup($main_window, utf8(_("<b>One-Click tools.</b>
792
793 You have just clicked on a One-Click tool. When such a tool is activated
794 (<span foreground=\"darkblue\">Rotate clockwise</span>, <span foreground=\"darkblue\">Rotate counter-clockwise</span>, or <span foreground=\"darkblue\">Delete</span>), clicking on a
795 thumbnail will immediately apply the desired action.
796
797 Click the <span foreground=\"darkblue\">None</span> icon when you're finished with One-Click tools.
798 ")))
799             $config['one-click-explained'] = true
800         end
801     }
802
803     $r90.signal_connect('toggled') {
804         if $r90.active?
805             set_mousecursor($autotable, Gdk::Cursor::SB_RIGHT_ARROW)
806             set_mousecursor($subalbums, Gdk::Cursor::SB_RIGHT_ARROW)
807             one_click_explain_try.call
808             $r270.active = false
809             $delete.active = false
810         else
811             if !$r270.active? && !$delete.active?
812                 set_mousecursor_normal($autotable)
813                 set_mousecursor_normal($subalbums)
814             end
815         end
816     }
817     $r270.signal_connect('toggled') {
818         if $r270.active?
819             set_mousecursor($autotable, Gdk::Cursor::SB_LEFT_ARROW)
820             set_mousecursor($subalbums, Gdk::Cursor::SB_LEFT_ARROW)
821             one_click_explain_try.call
822             $r90.active = false
823             $delete.active = false
824         else
825             if !$r90.active? && !$delete.active?
826                 set_mousecursor_normal($autotable)
827                 set_mousecursor_normal($subalbums)
828             end
829         end
830     }
831     $delete.signal_connect('toggled') {
832         if $delete.active?
833             set_mousecursor($autotable, Gdk::Cursor::PIRATE)
834             one_click_explain_try.call
835             $r90.active = false
836             $r270.active = false
837         else
838             if !$r90.active? && !$r270.active?
839                 set_mousecursor_normal($autotable)
840                 set_mousecursor_normal($subalbums)
841             end
842         end
843     }
844     nothing.signal_connect('clicked') {
845         $r90.active = $r270.active = $delete.active = false
846         set_mousecursor_normal($autotable)
847         set_mousecursor_normal($subalbums)
848     }
849
850     return tb
851 end
852
853 def create_main_window
854
855     mb = create_menu
856
857     tb = create_toolbar
858
859 #    open_file('/home/gc/booh/foo')
860
861     $albums_tv = Gtk::TreeView.new
862     $albums_tv.set_size_request(120, -1)
863     renderer = Gtk::CellRendererText.new
864     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
865     $albums_tv.append_column(column)
866     $albums_tv.set_headers_visible(false)
867     $albums_tv.selection.signal_connect('changed') { |w|
868         set_mousecursor_wait($main_window)
869         save_changes
870         iter = w.selected
871         if !iter
872             msg 3, "no selection"
873         else
874             $current_path = $albums_ts.get_value(iter, 1)
875             change_dir
876         end
877         set_mousecursor_normal($main_window)
878     }
879     $albums_ts = Gtk::TreeStore.new(String, String)
880     $albums_tv.set_model($albums_ts)
881     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
882
883     $notebook = Gtk::Notebook.new
884     create_subalbums_page
885     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
886     create_auto_table
887     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
888     $notebook.show_all
889     $notebook.signal_connect('switch-page') { |w, page, num|
890         if num == 0
891             $delete.active = false
892             $delete.sensitive = false
893         else
894             $delete.sensitive = true
895         end
896     }
897
898     paned = Gtk::HPaned.new
899     paned.pack1($albums_tv, false, false)
900     paned.pack2($notebook, true, true)
901
902     main_vbox = Gtk::VBox.new(false, 0)
903     main_vbox.pack_start(mb, false, false)
904     main_vbox.pack_start(tb, false, false)
905     main_vbox.pack_start(paned, true, true)
906     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
907
908     $main_window = Gtk::Window.new
909     $main_window.add(main_vbox)
910     $main_window.signal_connect('delete-event') {
911         try_quit
912     }
913
914     #- read/save size and position of window
915     if $config['pos-x'] && $config['pos-y']
916         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
917     else
918         $main_window.window_position = Gtk::Window::POS_CENTER
919     end
920     msg 3, "size: #{$config['width']}x#{$config['height']}"
921     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
922     $main_window.signal_connect('configure-event') {
923         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
924         x, y = $main_window.window.root_origin
925         width, height = $main_window.window.size
926         $config['pos-x'] = x
927         $config['pos-y'] = y
928         $config['width'] = width
929         $config['height'] = height
930         false
931     }
932
933     $statusbar.push(0, utf8(_("Ready.")))
934     $main_window.show_all
935 end
936
937
938 handle_options
939 read_config
940
941 Gtk.init
942 create_main_window
943 if ARGV[0]
944     open_file(ARGV[0])
945 end
946 Gtk.main
947
948 write_config