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