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