*** empty log message ***
[booh] / bin / booh-classifier
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
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         puts "args: #{ARGV}"
70     rescue
71         puts $!
72         usage
73         exit(1)
74     end
75 end
76
77 def memfree
78     meminfo = IO.readlines('/proc/meminfo').join
79     meminfo =~ /MemFree:.*?(\d+)/ or return -1
80     memfree = $1
81     meminfo =~ /Buffers:.*?(\d+)/ and buffers = $1
82     meminfo =~ /Cached:.*?(\d+)/ and cached = $1
83     return memfree.to_i + buffers.to_i + cached.to_i
84 end
85
86 def set_cache_memory_use_figure
87     if $config['cache-memory-use'] =~ /memfree_(\d+)/
88         $config['cache-memory-use-figure'] = memfree*$1.to_f/100
89     else
90         $config['cache-memory-use-figure'] = $config['cache-memory-use'].to_i
91     end
92     msg 2, "Set cache memory use figure: #{$config['cache-memory-use-figure']} kB"
93 end
94
95 def read_config
96     $config = {}
97     $config_file = File.expand_path('~/.booh-classifier-rc')
98     if File.readable?($config_file)
99         $xmldoc = REXML::Document.new(File.new($config_file))
100         $xmldoc.root.elements.each { |element|
101             txt = element.get_text
102             if txt
103                 if txt.value =~ /~~~/
104                     $config[element.name] = txt.value.split(/~~~/)
105                 else
106                     $config[element.name] = txt.value
107                 end
108             elsif element.elements.size == 0
109                 $config[element.name] = ''
110             else
111                 $config[element.name] = {}
112                 element.each { |chld|
113                     txt = chld.get_text
114                     $config[element.name][chld.name] = txt ? txt.value : nil
115                 }
116             end
117         }
118     end
119     $config['video-viewer'] ||= '/usr/bin/mplayer %f'
120     $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
121     $config['preload-distance'] ||= '5'
122     $config['cache-memory-use'] ||= 'memfree_50%'
123     $config['rotate-set-exif'] ||= 'true'
124     set_cache_memory_use_figure
125 end
126
127 def check_config
128     missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
129     if missing != []
130         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
131     end
132
133     viewer_binary = $config['video-viewer'].split.first
134     if viewer_binary && ! File.executable?(viewer_binary)
135         show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
136 You should fix this in Edit/Preferences so that you can view videos.
137
138 Problem was: '%s' is not an executable file.
139 Hint: don't forget to specify the full path to the executable,
140 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
141     end
142     browser_binary = $config['browser'].split.first
143     if browser_binary && ! File.executable?(browser_binary)
144         show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
145 You should fix this in Edit/Preferences so that you can open URLs.
146
147 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
148     end
149 end
150
151 def write_config
152     ios = File.open($config_file, "w")
153     $xmldoc = Document.new "<booh-classifier-rc version='#{$VERSION}'/>"
154     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
155     $config.each_pair { |key, value|
156         elem = $xmldoc.root.add_element key
157         if value.is_a? Hash
158             $config[key].each_pair { |subkey, subvalue|
159                 subelem = elem.add_element subkey
160                 subelem.add_text subvalue.to_s
161             }
162         elsif value.is_a? Array
163             elem.add_text value.join('~~~')
164         else
165             if !value
166                 elem.remove
167             else
168                 elem.add_text value.to_s
169             end
170         end
171     }
172     $xmldoc.write(ios, 0)
173     ios.close
174 end
175
176 def save_undo(name, closure, *params)
177     UndoHandler.save_undo(name, closure, [ *params ])
178     $undo_mb.sensitive = true
179     $redo_mb.sensitive = false
180 end
181
182 def get_mem
183     IO.readlines('/proc/self/status').join =~ /VmRSS.*?(\d+)\s*kB/
184     puts $1
185     return $1.to_i
186 end
187
188 def show_mem(*txt)
189     txt.length > 0 and print txt[0]
190     puts get_mem
191 end
192
193 def min(v1, v2)
194     return v1 < v2 ? v1 : v2
195 end
196 def max(v1, v2)
197     return v1 > v2 ? v1 : v2
198 end
199
200 class Gdk::Color
201     def darker
202         color = dup
203         color.red = max(color.red - 10000, 0)
204         color.green = max(color.green - 10000, 0)
205         color.blue = max(color.blue - 10000, 0)
206         return color
207     end
208     def lighter
209         color = dup
210         color.red = min(color.red + 10000, 65535)
211         color.green = min(color.green + 10000, 65535)
212         color.blue = min(color.blue + 10000, 65535)
213         return color
214     end
215 end
216
217 $colors = [ Gdk::Color.new(0, 65535, 0),
218             Gdk::Color.new(0, 0, 65535),
219             Gdk::Color.new(65535, 65535, 0),
220             Gdk::Color.new(0, 65535, 65535),
221             Gdk::Color.new(65535, 0, 65535) ]
222
223 class Tag
224     attr_accessor :color, :name
225     def initialize(name)
226         @name = name
227     end
228 end
229 $tags = {}
230
231 class Entry
232     @@thumbnails_height = 64
233     @@max_height = nil
234     def Entry.thumbnails_height
235         return @@thumbnails_height
236     end
237
238     attr_accessor :path, :type, :angle, :button, :removed, :tagged
239
240     def initialize(path, type)
241         @path = path
242         @type = type
243         if @@max_height.nil?
244             @@max_height = $main_window.root_window.size[1]
245         end
246         @protect_cleanup = Mutex.new
247     end
248
249     def pixbuf_full
250         @protect_cleanup.synchronize {
251             if @pixbuf_full.nil?
252                 puts ">>> pixbuf_full #{path}"
253                 load_into_pixbuf_full
254             end
255             return @pixbuf_full
256         }
257     end
258     def free_pixbuf_full
259         @protect_cleanup.synchronize {
260             if @pixbuf_full.nil?
261                 return false
262             else
263                 puts ">>> free_pixbuf_full #{path}"
264                 @pixbuf_full = nil
265                 return true
266             end
267         }
268     end
269     def pixbuf_main(width, height)
270         @protect_cleanup.synchronize {
271             if @pixbuf_main.nil? || width != @width || height != @height
272                 puts ">>> pixbuf_main #{path}"
273                 @width = width
274                 @height = height
275                 load_into_pixbuf_full  #- make sure it is loaded
276                 if @pixbuf_full.width.to_f / @pixbuf_full.height > width.to_f / height
277                     resized_height = @pixbuf_full.height * (width.to_f/@pixbuf_full.width)
278                     if @pixbuf_full.width > width || @pixbuf_full.height > resized_height
279                         @pixbuf_main = @pixbuf_full.scale(width, resized_height, Gdk::Pixbuf::INTERP_BILINEAR)
280                     else
281                         @pixbuf_main = @pixbuf_full
282                     end
283                 else
284                     resized_width = @pixbuf_full.width * (height.to_f/@pixbuf_full.height)
285                     if @pixbuf_full.width > resized_width || @pixbuf_full.height > height
286                         @pixbuf_main = @pixbuf_full.scale(resized_width, height, Gdk::Pixbuf::INTERP_BILINEAR)
287                     else
288                         @pixbuf_main = @pixbuf_full
289                     end
290                 end
291             end
292             return @pixbuf_main
293         }
294     end
295     def free_pixbuf_main
296         @protect_cleanup.synchronize {
297             if @pixbuf_main.nil?
298                 return false
299             else
300                 puts ">>> free_pixbuf_main #{path}"
301                 @pixbuf_main = nil
302                 return true
303             end
304         }
305     end
306     def pixbuf_thumbnail
307         return @protect_cleanup.synchronize {
308             if @pixbuf_thumbnail.nil?
309                 puts ">>> pixbuf_thumbnail #{path}"
310                 load_into_pixbuf_full  #- make sure it is loaded
311                 @pixbuf_thumbnail = @pixbuf_full.scale(@pixbuf_full.width * (@@thumbnails_height.to_f/@pixbuf_full.height), @@thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
312             end
313             return @pixbuf_thumbnail
314         }
315     end
316     def free_pixbuf_thumbnail
317         @protect_cleanup.synchronize {
318             if @pixbuf_thumbnail.nil?
319                 return false
320             else
321                 puts ">>> free_pixbuf_thumbnail #{path}"
322                 @pixbuf_thumbnail = nil
323                 return true
324             end
325         }
326     end
327
328     def show_bg
329         if removed
330             red = Gdk::Color.new(65535, 0, 0)
331             button.modify_bg(Gtk::StateType::NORMAL, red)
332             button.modify_bg(Gtk::StateType::PRELIGHT, red.lighter)
333             button.modify_bg(Gtk::StateType::ACTIVE, red)
334         elsif tagged
335             button.modify_bg(Gtk::StateType::NORMAL, tagged.color)
336             button.modify_bg(Gtk::StateType::PRELIGHT, tagged.color.lighter)
337             button.modify_bg(Gtk::StateType::ACTIVE, tagged.color)
338         else
339             # TODO need to add proper undo support :/
340             white = Gdk::Color.new(55535, 55535, 55535)
341             button.modify_bg(Gtk::StateType::NORMAL, white)
342             button.modify_bg(Gtk::StateType::PRELIGHT, white.lighter)
343             button.modify_bg(Gtk::StateType::ACTIVE, white)
344         end
345     end
346
347     private
348     def load_into_pixbuf_full
349         if @pixbuf_full.nil?
350             puts ">>> load_into_pixbuf_full #{path}"
351             begin
352                 @pixbuf_full = Gdk::Pixbuf.new(@path)
353             rescue Gdk::PixbufError
354                 puts "Cannot load #{@path}: #{$!}"
355                 return
356             end
357             if @pixbuf_full
358                 if @angle.nil?
359                     @angle = guess_rotate(path)
360                 end
361                 if @angle != 0
362                     puts ">>> load_into_pixbuf_full #{path} => rotate #{@angle}"
363                     @pixbuf_full = rotate_pixbuf(@pixbuf_full, @angle)
364                 end
365                 if @pixbuf_full.height > @@max_height
366                     #- save a lot of memory, don't store in actual full size
367                     @pixbuf_full = @pixbuf_full.scale(@pixbuf_full.width * (@@max_height.to_f/@pixbuf_full.height), @@max_height, Gdk::Pixbuf::INTERP_BILINEAR)
368                 end
369             end
370         end
371     end
372
373     def to_s
374         @path
375     end
376 end
377
378 class MainView < Gtk::DrawingArea
379
380     def initialize
381         super()
382         signal_connect('expose-event') { draw }
383         signal_connect('configure-event') { update_shown; GC.start }
384         signal_connect('realize') {
385             @preloader = Thread.new {
386                 #- background preloading
387                 while true
388                     puts "background main preloading triggered..."
389                     if ! @index.nil?
390                         w, h = window.size
391                         for j in 1 .. $config['preload-distance'].to_i
392                             i = @index + j
393                             if i < $allentries.size
394                                 $allentries[i].pixbuf_main(w, h)
395                             end
396                             i = @index - j
397                             if i >= 0
398                                 $allentries[i].pixbuf_main(w, h)
399                             end
400                             GC.start
401                             mem = get_mem
402                             if mem > $config['cache-memory-use-figure']
403                                 puts "too much RSS, stopping main preloading"
404                                 break
405                             end
406                         end
407                         check_memory_free_cache_if_needed
408                     end
409                     Thread.stop
410                 end
411             }
412             @preloader.priority = -2
413         }
414     end
415
416     def set_shown_entry(index)
417         if index == @index || index < 0 || index >= $allentries.size
418             return
419         end
420         old_index = @index
421         @index = index
422         if index.nil?
423             @entry = nil
424         else
425             @entry = $allentries[index]
426         end
427         if ! @entry.button
428             #- not loaded yet
429             @index = old_index
430             return
431         end
432         redraw        
433         @preloader.run
434         @entry.button.grab_focus
435     end
436
437
438     def get_shown_entry
439         return @index
440     end
441
442     def redraw
443         update_shown
444         w, h = window.size
445         window.begin_paint(Gdk::Rectangle.new(0, 0, w, h))
446         window.clear
447         draw
448         window.end_paint
449     end
450
451     def update_shown
452         if @entry
453             width, height = window.size 
454             @pixbuf = @entry.pixbuf_main(width, height)
455             if @pixbuf.width == width
456                 @xpos = 0
457                 @ypos = (height-@pixbuf.height)/2
458             else
459                 @xpos = (width-@pixbuf.width)/2
460                 @ypos = 0
461             end
462             @preloader.run
463         else
464             @pixbuf = nil
465         end
466     end
467
468     def draw
469         if @pixbuf
470             window.draw_pixbuf(nil, @pixbuf, 0, 0, @xpos, @ypos, -1, -1, Gdk::RGB::DITHER_NONE, -1, -1)
471         end
472     end
473 end
474
475 def check_memory_free_cache_if_needed
476     GC.start
477     mem = get_mem
478     i = $mainview.get_shown_entry
479     puts "mem: #{mem} index: #{i}"
480     return if i.nil?
481     ($allentries.size - 1).downto(1) { |j|
482         if mem < $config['cache-memory-use-figure'] * 2 / 3
483             break
484         end
485         index = i + j
486         puts "too much RSS, freeing full size of #{i+j} and #{i-j}..."
487         freedsomething = false
488         if i + j < $allentries.size
489             freedsomething |= $allentries[i+j].free_pixbuf_full
490         end
491         if i - j > 0
492             freedsomething |= $allentries[i-j].free_pixbuf_full
493         end
494         if freedsomething
495             GC.start
496             mem = get_mem
497             puts "\tmem now: #{mem}"
498         end
499     }
500 end
501
502 def autoscroll_if_needed(button)
503     xpos_left = button.allocation.x
504     xpos_right = button.allocation.x + button.allocation.width
505     hadj = $imagesline_sw.hadjustment
506     current_minx_visible = hadj.value
507     current_maxx_visible = hadj.value + hadj.page_size
508     if xpos_left < current_minx_visible
509         #- autoscroll left
510         newval = hadj.value - (current_minx_visible - xpos_left)
511         hadj.value = newval
512         button.queue_draw  #- TOREMOVE: the visual focus is displayed incorrectly
513     elsif xpos_right > current_maxx_visible
514         #- autoscroll right
515         newval = hadj.value + (xpos_right - current_maxx_visible)
516         if newval > hadj.upper - hadj.page_size
517             newval = hadj.upper - hadj.page_size
518         end
519         hadj.value = newval
520         button.queue_draw  #- TOREMOVE: the visual focus is displayed incorrectly
521     end
522 end
523
524 def show_popup(parent, msg, *options)
525     dialog = Gtk::Dialog.new
526     if options[0]
527         options = options[0]
528     else
529         options = {}
530     end
531     if options[:title]
532         dialog.title = options[:title]
533     else
534         dialog.title = utf8(_("Booh message"))
535     end
536     lbl = Gtk::Label.new
537     if options[:nomarkup]
538         lbl.text = msg
539     else
540         lbl.markup = msg
541     end
542     if options[:centered]
543         lbl.set_justify(Gtk::Justification::CENTER)
544     end
545     if options[:selectable]
546         lbl.selectable = true
547     end
548     if options[:topwidget]
549         dialog.vbox.add(options[0][:topwidget])
550     end
551     if options[:scrolled]
552         sw = Gtk::ScrolledWindow.new(nil, nil)
553         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
554         sw.add_with_viewport(lbl)
555         dialog.vbox.add(sw)
556         dialog.set_default_size(500, 600)
557     else
558         dialog.vbox.add(lbl)
559         dialog.set_default_size(200, 120)
560     end
561     if options[:bottomwidget]
562         dialog.vbox.add(options[:bottomwidget])
563     end
564     if options[:okcancel]
565         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
566     end
567     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
568
569     if options[:pos_centered]
570         dialog.window_position = Gtk::Window::POS_CENTER
571     else
572         dialog.window_position = Gtk::Window::POS_MOUSE
573     end
574
575     if options[:linkurl]
576         linkbut = Gtk::Button.new('')
577         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
578         linkbut.signal_connect('clicked') {
579             open_url(options[0][:linkurl] + '/index.html')
580             dialog.response(Gtk::Dialog::RESPONSE_OK)
581             set_mousecursor_normal
582         }
583         linkbut.relief = Gtk::RELIEF_NONE
584         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
585         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
586         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
587     end
588
589     dialog.show_all
590
591     if options[:stuff_connector]
592         options[:stuff_connector].call({ :dialog => dialog })
593     end
594                                         
595     if !options[:not_transient]
596         dialog.transient_for = parent
597         dialog.run { |response|
598             if options[:data_getter]
599                 options[:data_getter].call
600             end
601             dialog.destroy
602             if options[:okcancel]
603                 return response == Gtk::Dialog::RESPONSE_OK
604             end
605         }
606     else
607         dialog.signal_connect('response') { dialog.destroy }
608     end
609 end
610
611 def thumbnail_keypressed(entry, i, event)
612     if event.state & Gdk::Window::MOD1_MASK != 0
613         #- ALT pressed: Alt-Left and Alft-Right rotate
614         if event.keyval == Gdk::Keyval::GDK_Left || event.keyval == Gdk::Keyval::GDK_Right
615             if event.keyval == Gdk::Keyval::GDK_Left
616                 entry.angle = (entry.angle - 90) % 360
617             else
618                 entry.angle = (entry.angle + 90) % 360
619             end
620             entry.free_pixbuf_full
621             entry.free_pixbuf_main
622             entry.free_pixbuf_thumbnail
623             $mainview.redraw
624             entry.button.set_image(img = Gtk::Image.new(entry.pixbuf_thumbnail))
625         end
626
627     elsif event.state & Gdk::Window::CONTROL_MASK != 0
628         #- CONTROL pressed: Ctrl-z and Ctrl-r for undo/redo
629         if event.keyval == Gdk::Keyval::GDK_z
630             perform_undo
631         end
632         if event.keyval == Gdk::Keyval::GDK_r
633             perform_redo
634         end
635
636     else
637         removed_before = entry.removed
638         tag_before = entry.tagged
639
640         if event.keyval == Gdk::Keyval::GDK_Delete
641             entry.removed = true
642             entry.tagged = nil
643             entry.show_bg
644             $mainview.set_shown_entry(i + 1)
645
646             save_undo(_("set for removal"),
647                       proc {
648                           entry.removed = removed_before
649                           entry.tagged = tag_before
650                           entry.show_bg
651                           $mainview.set_shown_entry(i)
652                           proc {
653                               entry.removed = true
654                               entry.tagged = nil
655                               entry.show_bg
656                               $mainview.set_shown_entry(i)
657                           }
658                       })
659
660         elsif event.keyval == Gdk::Keyval::GDK_space
661             entry.removed = false
662             entry.tagged = nil
663             entry.show_bg
664             $mainview.set_shown_entry(i + 1)
665
666             save_undo(_("remove tag"),
667                       proc {
668                           entry.removed = removed_before
669                           entry.tagged = tag_before
670                           entry.show_bg
671                           $mainview.set_shown_entry(i)
672                           proc {
673                               entry.removed = false
674                               entry.tagged = nil
675                               entry.show_bg
676                               $mainview.set_shown_entry(i)
677                           }
678                       })
679
680         else
681             char = [ Gdk::Keyval.to_unicode(event.keyval) ].pack("C*")
682             if char =~ /^[a-zA-z0-9]$/
683                 tag = $tags[char]
684                 
685                 if tag.nil?
686                     vb = Gtk::VBox.new(false, 0)
687                     vb.pack_start(entry = Gtk::Entry.new.set_text(char), false, false)
688                     vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(bt = Gtk::Button.new(utf8(_("  Change color  ")))))
689                     text = nil
690                     color = nil
691                     bt.signal_connect('clicked') {
692                         color = $colors.shift
693                         if color.nil?
694                             color = Gdk::Color.new(16384 + rand(49151), 16384 + rand(49151), 16384 + rand(49151))
695                         end
696                         bt.modify_bg(Gtk::StateType::NORMAL, color)
697                         bt.modify_bg(Gtk::StateType::PRELIGHT, color)
698                         bt.modify_bg(Gtk::StateType::ACTIVE, color.darker)
699                     }
700                     bt.clicked
701                     entry.signal_connect('changed') {  #- cannot add a new tag with first letter of an existing tag
702                         while $tags.has_key?(entry.text[0,1])
703                             entry.text = entry.text.sub(/./, '')
704                         end
705                     }
706                     if show_popup($main_window,
707                                   utf8(_("You typed the text character '%s', which is not associated with a tag.\nType in the full name of the tag below to create a new one.")) % char,
708                                   { :okcancel => true, :bottomwidget => vb, :data_getter => proc { text = entry.text },
709                                     :stuff_connector => proc { |stuff| entry.select_region(0, 0)
710                                                                        entry.position = -1
711                                                                        entry.signal_connect('activate') { stuff[:dialog].response(Gtk::Dialog::RESPONSE_OK) } } } )
712                         if text.length > 0
713                             char = text[0,1]  #- in case it changed
714                             tag = Tag.new(text)
715                             tag.color = color
716                             $tags[char] = tag
717                             label = Gtk::Label.new.set_markup('<b>(' + char + ')</b>' + text[1..-1]).set_justify(Gtk::Justification::CENTER)
718                             $tags_vbox.pack_start(evt = Gtk::EventBox.new.add(label).modify_bg(Gtk::StateType::NORMAL, tag.color).show_all)
719                         end
720                     end
721
722                 else
723                     entry.removed = false
724                     entry.tagged = tag
725                     entry.show_bg
726                     $mainview.set_shown_entry(i + 1)
727
728                     save_undo(_("set tag"),
729                               proc {
730                                   entry.removed = removed_before
731                                   entry.tagged = tag_before
732                                   entry.show_bg
733                                   $mainview.set_shown_entry(i)
734                                   proc {
735                                       entry.removed = false
736                                       entry.tagged = tag
737                                       entry.show_bg
738                                       $mainview.set_shown_entry(i)
739                                   }
740                               })
741                 end
742             end
743         end
744     end
745 end
746
747 def sb_msg(msg)
748     $statusbar.pop(0)
749     if msg
750         $statusbar.push(0, utf8(msg))
751     end
752 end
753     
754 def show_entries
755     e = Thread.new {
756         t1 = Time.now
757         show_mem
758         sb_msg(_("Loading images..."))
759         tooltips = Gtk::Tooltips.new
760         counter = 0
761         $allentries.each_with_index { |entry, i|
762             if entry.pixbuf_full
763                 entry.pixbuf_thumbnail
764                 gtk_thread_protect(proc { |i|
765                                        entry = $allentries[i]
766                                        entry.button = Gtk::Button.new.set_image(img = Gtk::Image.new(entry.pixbuf_thumbnail))
767                                        tooltips.set_tip(entry.button, File.basename(entry.path).gsub(/\.[^.]+$/, ''), nil)
768                                        $imagesline.pack_start(entry.button.show_all, false, false)
769                                        entry.button.signal_connect('clicked') { $mainview.set_shown_entry(i) }
770                                        entry.button.signal_connect('focus-in-event') { entry.button.clicked; autoscroll_if_needed(entry.button) }
771                                        entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, i, e) }
772                                        if i == 0
773                                            entry.button.grab_focus
774                                        end
775                                    }, i)
776                 if i % 4 == 0
777                     check_memory_free_cache_if_needed
778                 end
779                 counter += 1
780             end
781         }
782         check_memory_free_cache_if_needed
783         sb_msg(_("%d images loaded.") % counter)
784         puts "time: #{Time.now - t1}"
785     }
786     e.priority = -1
787 end
788
789 def open_dir(path)
790     #- remove visual stuff, so that user will see something is happening
791     reset_tags
792     reset_thumbnails
793     $mainview.set_shown_entry(nil)
794     sb_msg(_("Scanning source directory..."))
795
796     path = File.expand_path(path.sub(%r|/$|, ''))
797     examined_dirs = `find '#{path}' -type d -follow`.sort.collect { |v| v.chomp }
798     #- validate first
799     examined_dirs.each { |dir|
800         if dir =~ /'/
801             return utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir)
802         end
803         Dir.entries(dir).each { |file|
804             if file =~ /['"\[\]]/
805                 return utf8(_("Files can't contain any of the characters ', \", [ or ], sorry: %s") % "#{dir}/#{file}")
806             end
807         }
808     }
809
810     #- scan for populate second
811     examined_dirs.each { |dir|
812         if File.basename(dir) =~ /^\./
813             msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
814             next
815         end
816         Dir.entries(dir).each { |file|
817             type = entry2type(file)
818            if type && $allentries.size < 10
819                 $allentries << Entry.new(File.join(dir, file), type)
820             end
821         }
822     }
823     $workingdir = path
824     show_entries
825     $execute.sensitive = true
826     return nil
827 end
828
829 def open_dir_popup
830     fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory to work with")),
831                                     nil,
832                                     Gtk::FileChooser::ACTION_SELECT_FOLDER,
833                                     nil,
834                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
835     fc.transient_for = $main_window
836     if $workingdir
837         fc.current_folder = $workingdir
838     end
839     ok = false
840     while !ok
841         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
842             msg = open_dir(fc.filename)
843             if msg
844                 show_popup(fc, msg)
845                 ok = false
846             else
847                 ok = true
848             end
849         else
850             ok = true
851         end
852     end
853     fc.destroy
854 end
855
856 def gtk_thread_protect(proc, *params)
857     if Thread.current == Thread.main
858         proc.call(*params)
859     else
860         $protect_gtk_pending_calls.synchronize {
861             $gtk_pending_calls << [ proc, params ]
862         }
863     end
864 end
865
866 def gtk_thread_flush
867     $protect_gtk_pending_calls.synchronize {
868         if $gtk_pending_calls.size > 0
869             elem = $gtk_pending_calls.shift
870             elem[0].call(*elem[1])
871         end
872     }
873 end
874
875 def try_quit(*options)
876     Gtk.main_quit
877 end
878
879 def execute
880     dialog = Gtk::Dialog.new
881     dialog.title = utf8(_("Booh message"))
882
883     vb1 = Gtk::VBox.new(false, 5)
884     label = Gtk::Label.new.set_markup(utf8(_("You're about to <b>execute</b> actions on the marked images.\nPlease confirm below the actions. This operation is not undoable!")))
885     vb1.pack_start(label, false, false)
886
887     table = Gtk::Table.new(0, 0, false)
888     table.set_row_spacings(5)
889     table.set_column_spacings(5)
890     table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Tag name:</b>"))).set_justify(Gtk::Justification::CENTER), 0, 1, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
891     table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Amount of pictures:</b>"))).set_justify(Gtk::Justification::CENTER), 1, 2, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
892     table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Pictures examples:</b>"))).set_justify(Gtk::Justification::CENTER), 2, 3, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
893     table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Action to perform:</b>"))).set_justify(Gtk::Justification::CENTER), 3, 4, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
894     add_row = proc { |row, name, color, truthproc, normal|
895         table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(Gtk::EventBox.new.add(Gtk::Label.new.set_markup(name)).modify_bg(Gtk::StateType::NORMAL, color)),
896                      0, 1, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
897         counter = 0
898         examples = Gtk::HBox.new(false, 5)
899         $allentries.each { |entry|
900             if truthproc.call(entry)
901                 counter += 1
902                 if counter < 4
903                     examples.pack_start(Gtk::Image.new(entry.pixbuf_thumbnail), false, false)
904                 elsif counter == 4
905                     examples.pack_start(Gtk::Label.new.set_markup("<b>...</b>"), false, false)
906                 end
907             end
908         }
909         table.attach(Gtk::Label.new(counter.to_s).set_justify(Gtk::Justification::CENTER), 1, 2, row, row + 1, 0, 0, 5, 5)
910         table.attach(examples, 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
911
912         combostore = Gtk::ListStore.new(Gdk::Pixbuf, String)
913         iter = combostore.append
914         if normal
915             iter[0] = $main_window.render_icon(Gtk::Stock::GO_FORWARD, Gtk::IconSize::MENU)
916             iter[1] = utf8(_("Move to:"))
917         else
918             iter[0] = $main_window.render_icon(Gtk::Stock::DELETE, Gtk::IconSize::MENU)
919             iter[1] = utf8(_("Permanently remove"))
920         end
921         iter = combostore.append
922         iter[0] = $main_window.render_icon(Gtk::Stock::MEDIA_STOP, Gtk::IconSize::MENU)
923         iter[1] = utf8(_("Do nothing"))
924         combo = Gtk::ComboBox.new(combostore)
925         combo.active = 0
926         renderer = Gtk::CellRendererPixbuf.new
927         combo.pack_start(renderer, false)
928         combo.set_attributes(renderer, :pixbuf => 0)
929         renderer = Gtk::CellRendererText.new
930         combo.pack_start(renderer, true)
931         combo.set_attributes(renderer, :text => 1)
932
933         if normal
934             pathbutton = Gtk::Button.new.add(pathlabel = Gtk::Label.new.set_markup(utf8(_("<i>(unset)</i>"))))
935             lastpath = $workingdir
936             pathbutton.signal_connect('clicked') {
937                 fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory where to move the pictures to")),
938                                                 nil,
939                                                 Gtk::FileChooser::ACTION_SELECT_FOLDER,
940                                                 nil,
941                                                 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
942                 fc.transient_for = dialog
943                 fc.current_folder = lastpath
944                 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
945                     pathlabel.text = fc.filename
946                 end
947                 lastpath = fc.filename
948                 fc.destroy
949             }
950             combo.signal_connect('changed') {
951                 pathbutton.sensitive = combo.active == 0
952             }
953             vb = Gtk::VBox.new(false, 5)
954             vb.pack_start(combo, false, false)
955             vb.pack_start(pathbutton, false, false)
956             table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(vb), 3, 4, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
957             { :combo => combo, :pathlabel => pathlabel }
958         else
959             table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(combo), 3, 4, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
960             { :combo => combo }
961         end
962     }
963     stuff = {}
964     stuff['toremove'] = add_row.call(1, utf8(_("<i>to remove</i>")), Gdk::Color.new(65535, 0, 0), proc { |entry| entry.removed }, false)
965     $tags.values.each_with_index { |tag, row| stuff[tag] = add_row.call(row + 2, tag.name, tag.color, proc { |entry| entry.tagged == tag }, true) }
966     vb1.pack_start(sw = Gtk::ScrolledWindow.new(nil, nil).add_with_viewport(table).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC), true, true)
967
968     toremove_amount = $allentries.find_all { |entry| entry.removed }.size
969     check_removal = Gtk::CheckButton.new(utf8(_("I have noticed I am about to permanently remove the %d above mentioned pictures.") % toremove_amount))
970     if toremove_amount > 0
971         vb1.pack_start(check_removal, false, false)
972         stuff['toremove'][:combo].signal_connect('changed') { |widget|
973             check_removal.sensitive = widget.active == 0
974         }
975     end
976
977     dialog.vbox.add(vb1)
978
979     dialog.set_default_size(800, 600)
980     dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
981     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
982     dialog.window_position = Gtk::Window::POS_MOUSE
983     dialog.transient_for = $main_window
984
985     dialog.show_all
986
987     while true
988         dialog.run { |response|        
989             if response == Gtk::Dialog::RESPONSE_OK
990                 if toremove_amount > 0 && ! check_removal.active? && check_removal.sensitive?
991                     show_popup(dialog, utf8(_("You have not confirmed that you noticed the permanent removal of the pictures marked for deletion.")))
992                     break
993                 end
994                 problem = false
995                 tag2entries = {}
996                 $tags.values.each { |tag| tag2entries[tag] = [] }
997                 $allentries.each { |entry| entry.tagged and tag2entries[entry.tagged] << entry }
998                 stuff.keys.each { |key|
999                     if key.is_a?(Tag) && stuff[key][:combo].active == 0
1000                         path = stuff[key][:pathlabel].text
1001                         if path[0] != ?/
1002                             show_popup(dialog, utf8(_("You have not selected a directory where to move %s.") % key.name))
1003                             problem = true
1004                             break
1005                         end
1006                         st = File.stat(path)
1007                         if ! st.directory? || ! st.writable?
1008                             show_popup(dialog, utf8(_("Directory where to move %s is not valid or not writable.") % key.name))
1009                             problem = true
1010                             break
1011                         end
1012                         tag2entries[key].each { |entry|
1013                             begin
1014                                 File.stat(File.join(path, File.basename(entry.path)))
1015                                 show_popup(dialog, utf8(_("Sorry, a file '%s' already exists in '%s'.") % [ File.basename(entry.path), path ]))
1016                                 problem = true
1017                                 break
1018                             rescue
1019                             end
1020                         }
1021                         if problem
1022                             break
1023                         end
1024                     end
1025                 }
1026                 if ! problem
1027                     puts "execute!"
1028                     dialog.destroy
1029                     return
1030                 end
1031
1032             else
1033                 dialog.destroy
1034                 return
1035             end
1036         }
1037     end
1038 end
1039
1040 def preferences
1041     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1042                              $main_window,
1043                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1044                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1045                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1046
1047     tooltips = Gtk::Tooltips.new
1048     
1049     dialog.vbox.add(tbl = Gtk::Table.new(0, 0, false))
1050     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
1051                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1052     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
1053                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1054     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mplayer %f")), nil)
1055
1056     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
1057                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1058     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
1059                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1060     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
1061
1062     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Preloading distance: ")))),
1063                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1064     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(preload_distance = Gtk::SpinButton.new(0, 50, 1).set_value($config['preload-distance'].to_i)),
1065                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1066     tooltips.set_tip(preload_distance, utf8(_("Amount of pictures preloaded left and right to the currently shown")), nil)
1067
1068     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Cache memory use: ")))),
1069                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1070     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(cache_vbox = Gtk::VBox.new(false, 0)),
1071                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1072     cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_memfree_radio = Gtk::RadioButton.new(''), false, false).
1073                                                   pack_start(cache_memfree_spin = Gtk::SpinButton.new(0, 100, 10), false, false).
1074                                                   pack_start(cache_memfree_label = Gtk::Label.new(utf8(_("% of free memory"))), false, false), false, false)
1075     cache_memfree_spin.signal_connect('value-changed') { cache_memfree_radio.active = true }
1076     tooltips.set_tip(cache_memfree_spin, utf8(_("Percentage of free memory (+ buffers/cache) measured at startup")), nil)
1077     cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_specify_radio = Gtk::RadioButton.new(cache_memfree_radio, ''), false, false).
1078                                                   pack_start(cache_specify_spin = Gtk::SpinButton.new(0, 4000, 50), false, false).
1079                                                   pack_start(cache_specify_label = Gtk::Label.new(utf8(_("MB"))).set_sensitive(false), false, false), false, false)
1080     cache_specify_spin.signal_connect('value-changed') { cache_specify_radio.active = true }
1081     cache_memfree_radio.signal_connect('toggled') {
1082         if cache_memfree_radio.active?
1083             cache_memfree_label.sensitive = true
1084             cache_specify_label.sensitive = false
1085         else
1086             cache_specify_label.sensitive = true
1087             cache_memfree_label.sensitive = false
1088         end
1089     }
1090     tooltips.set_tip(cache_specify_spin, utf8(_("Amount of memory in megabytes")), nil)
1091     if $config['cache-memory-use'] =~ /memfree_(\d+)/
1092         cache_memfree_spin.value = $1.to_i
1093     else
1094         cache_specify_spin.value = $config['cache-memory-use'].to_i
1095     end
1096
1097     tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
1098                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
1099     tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
1100     update_exif_orientation_check.active = $config['rotate-set-exif']
1101
1102     dialog.vbox.show_all
1103     dialog.run { |response|
1104         if response == Gtk::Dialog::RESPONSE_OK
1105             $config['video-viewer'] = from_utf8(video_viewer_entry.text)
1106             $config['preload-distance'] = preload_distance.value
1107             $config['rotate-set-exif'] = update_exif_orientation_check.active?
1108             if cache_memfree_radio.active?
1109                 $config['cache-memory-use'] = "memfree_#{cache_memfree_spin.value}%"
1110             else
1111                 $config['cache-memory-use'] = cache_specify_spin.value
1112             end
1113             set_cache_memory_use_figure
1114             p $config
1115         end
1116     }
1117     dialog.destroy
1118 end
1119
1120 def perform_undo
1121     if $undo_mb.sensitive?
1122         $redo_mb.sensitive = true
1123         if not more_undoes = UndoHandler.undo($statusbar)
1124             $undo_mb.sensitive = false
1125         end
1126     end
1127 end
1128
1129 def perform_redo
1130     if $redo_mb.sensitive?
1131         $undo_mb.sensitive = true
1132         if not more_redoes = UndoHandler.redo($statusbar)
1133             $redo_mb.sensitive = false
1134         end
1135     end
1136 end
1137
1138 def create_menubar    
1139     #- menu
1140     mb = Gtk::MenuBar.new
1141
1142     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1143     filesubmenu = Gtk::Menu.new
1144     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1145     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1146     filesubmenu.append($execute  = Gtk::ImageMenuItem.new(Gtk::Stock::EXECUTE).set_sensitive(false))
1147     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1148     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
1149     filemenu.set_submenu(filesubmenu)
1150     mb.append(filemenu)
1151
1152     open.signal_connect('activate') { open_dir_popup }
1153     $execute.signal_connect('activate') { execute }
1154     quit.signal_connect('activate') { try_quit }
1155
1156     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
1157     editsubmenu = Gtk::Menu.new
1158     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
1159     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
1160     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
1161     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
1162     editmenu.set_submenu(editsubmenu)
1163     mb.append(editmenu)
1164
1165     $undo_mb.signal_connect('activate') { perform_undo }
1166     $redo_mb.signal_connect('activate') { perform_redo }
1167     prefs.signal_connect('activate') { preferences }
1168     
1169     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
1170     helpsubmenu = Gtk::Menu.new
1171     helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
1172     tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
1173     helpsubmenu.append(Gtk::SeparatorMenuItem.new)
1174     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
1175     helpmenu.set_submenu(helpsubmenu)
1176     mb.append(helpmenu)
1177
1178     tutos.signal_connect('activate') { open_url('http://booh.org/tutorial.html') }
1179     about.signal_connect('activate') { call_about }
1180
1181
1182     #- no toolbar, to save height
1183
1184     return mb
1185 end
1186
1187 def reset_tags
1188     for child in $tags_vbox.children
1189         $tags_vbox.remove(child)
1190     end
1191     $tags_vbox.pack_start(Gtk::Label.new(utf8(_("Tags list:"))).set_justify(Gtk::Justification::CENTER), false, false).show_all
1192 end
1193
1194 def reset_thumbnails
1195     $allentries = []
1196     for child in $imagesline.children
1197         $imagesline.remove(child)
1198     end
1199 end
1200
1201 def create_main_window
1202
1203     mb = create_menubar
1204
1205     main_vbox = Gtk::VBox.new(false, 0)
1206     main_vbox.pack_start(mb, false, false)
1207     mainview_hbox = Gtk::HBox.new
1208     mainview_hbox.pack_start(Gtk::Alignment.new(0.5, 0, 0, 0).add($tags_vbox = Gtk::VBox.new(false, 5)), false, true)
1209     reset_tags
1210     mainview_hbox.pack_start($mainview = MainView.new, true, true)
1211     main_vbox.pack_start(mainview_hbox, true, true)
1212     $imagesline_sw = Gtk::ScrolledWindow.new(nil, nil)
1213     $imagesline_sw.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_NEVER)
1214     $imagesline_sw.add_with_viewport($imagesline = Gtk::HBox.new(false, 0).show)
1215     main_vbox.pack_start($imagesline_sw, false, false)
1216     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
1217
1218     $imagesline.set_size_request(-1, Entry.thumbnails_height + $imagesline_sw.hscrollbar.size_request[1])
1219
1220     $main_window = Gtk::Window.new
1221     $main_window.add(main_vbox)
1222     $main_window.signal_connect('delete-event') {
1223         try_quit({ :disallow_cancel => true })
1224     }
1225
1226     #- read/save size and position of window
1227     if $config['pos-x'] && $config['pos-y']
1228         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
1229     else
1230         $main_window.window_position = Gtk::Window::POS_CENTER
1231     end
1232     msg 3, "size: #{$config['width']}x#{$config['height']}"
1233     $main_window.set_default_size(($config['width'] || 700).to_i, ($config['height'] || 600).to_i)
1234     $main_window.signal_connect('configure-event') {
1235         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
1236         x, y = $main_window.window.root_origin
1237         width, height = $main_window.window.size
1238         $config['pos-x'] = x
1239         $config['pos-y'] = y
1240         $config['width'] = width
1241         $config['height'] = height
1242         false
1243     }
1244
1245     $protect_gtk_pending_calls = Mutex.new
1246     $gtk_pending_calls = []
1247     Gtk.timeout_add(50) {
1248         gtk_thread_flush
1249         true
1250     }
1251
1252     Gtk.timeout_add(10000) {
1253         show_mem
1254         true
1255     }
1256  
1257     $main_window.show_all
1258 end
1259
1260
1261 Thread.abort_on_exception = true
1262 read_config
1263 Gtk.init
1264
1265
1266 #- Gdk::Pixbuf#rotate memory leak check (in ruby-gnome2 <= 0.16.0)
1267 #pb = Gdk::Pixbuf.new("#{$FPATH}/images/logo.png")
1268 #1.upto(5) { pb = pb.rotate(Gdk::Pixbuf::ROTATE_CLOCKWISE) }
1269 #GC.start
1270 #mem = get_mem
1271 #1.upto(5) { pb = pb.rotate(Gdk::Pixbuf::ROTATE_CLOCKWISE) }
1272 #GC.start
1273 #mem2 = get_mem
1274 #if mem2 != mem
1275 #    puts _("Gdk::Pixbuf#scale memory leak detected (this is normal with unpatched ruby-gnome2 <= 0.16.0). Application would slow down to a crawl, won't proceed.")
1276 #    exit 1
1277 #end
1278
1279
1280 create_main_window
1281 check_config
1282
1283 if ARGV[0]
1284     open_dir(ARGV[0])
1285 end
1286 Gtk.main
1287
1288 write_config