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