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