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