1#
2#  tkcombobox.rb : auto scrollbox & combobox
3#
4#                         by Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)
5#
6require 'tk'
7
8module Tk
9  module RbWidget
10    class AutoScrollListbox < TkListbox
11    end
12    class Combobox < TkEntry
13    end
14  end
15end
16
17class Tk::RbWidget::AutoScrollListbox
18  include TkComposite
19
20  @@up_bmp = TkBitmapImage.new(:data=><<EOD)
21#define up_arrow_width 9
22#define up_arrow_height 9
23static unsigned char up_arrow_bits[] = {
24   0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x38, 0x00, 0x38, 0x00, 0x7c, 0x00,
25   0x7c, 0x00, 0xfe, 0x00, 0x00, 0x00};
26EOD
27
28  @@down_bmp = TkBitmapImage.new(:data=><<EOD)
29#define up_arrow_width 9
30#define up_arrow_height 9
31static unsigned char down_arrow_bits[] = {
32   0x00, 0x00, 0xfe, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x38, 0x00, 0x38, 0x00,
33   0x10, 0x00, 0x10, 0x00, 0x00, 0x00};
34EOD
35
36  ############################
37  private
38  ############################
39  def initialize_composite(keys={})
40    keys = _symbolkey2str(keys)
41
42    @initwait = keys.delete('startwait'){300}
43    @interval = keys.delete('interval'){150}
44    @initwait -= @interval
45    @initwait = 0 if @initwait < 0
46
47    @lbox = TkListbox.new(@frame, :borderwidth=>0)
48    @path = @lbox.path
49    TkPack.propagate(@lbox, false)
50
51    @scr = TkScrollbar.new(@frame, :width=>10)
52
53    @lbox.yscrollcommand(proc{|*args| @scr.set(*args); _config_proc})
54    @scr.command(proc{|*args| @lbox.yview(*args); _config_proc})
55
56    @up_arrow   = TkLabel.new(@lbox, :image=>@@up_bmp,
57                              :relief=>:raised, :borderwidth=>1)
58    @down_arrow = TkLabel.new(@lbox, :image=>@@down_bmp,
59                              :relief=>:raised, :borderwidth=>1)
60
61    _init_binding
62
63    @lbox.pack(:side=>:left, :fill=>:both, :expand=>:true)
64
65    delegate('DEFAULT', @lbox)
66    delegate('background', @frame, @scr)
67    delegate('activebackground', @scr)
68    delegate('troughcolor', @scr)
69    delegate('repeatdelay', @scr)
70    delegate('repeatinterval', @scr)
71    delegate('relief', @frame)
72    delegate('borderwidth', @frame)
73
74    delegate_alias('arrowrelief', 'relief', @up_arrow, @down_arrow)
75    delegate_alias('arrowborderwidth', 'borderwidth', @up_arrow, @down_arrow)
76
77    scrollbar(keys.delete('scrollbar')){false}
78
79    configure keys unless keys.empty?
80  end
81
82  def _show_up_arrow
83    unless @up_arrow.winfo_mapped?
84      @up_arrow.pack(:side=>:top, :fill=>:x)
85    end
86  end
87
88  def _show_down_arrow
89    unless @down_arrow.winfo_mapped?
90      @down_arrow.pack(:side=>:bottom, :fill=>:x)
91    end
92  end
93
94  def _set_sel(idx)
95      @lbox.activate(idx)
96      @lbox.selection_clear(0, 'end')
97      @lbox.selection_set(idx)
98  end
99
100  def _check_sel(cidx, tidx = nil, bidx = nil)
101    _set_sel(cidx)
102    unless tidx
103      tidx = @lbox.nearest(0)
104      tidx += 1 if tidx > 0
105    end
106    unless bidx
107      bidx = @lbox.nearest(10000)
108      bidx -= 1 if bidx < @lbox.index('end') - 1
109    end
110    if cidx > bidx
111      _set_sel(bidx)
112    end
113    if cidx < tidx
114      _set_sel(tidx)
115    end
116  end
117
118  def _up_proc
119    cidx = @lbox.curselection[0]
120    idx = @lbox.nearest(0)
121    if idx >= 0
122      @lbox.see(idx - 1)
123      _set_sel(idx)
124      @up_arrow.pack_forget if idx == 1
125      @up_timer.stop if idx == 0
126      _show_down_arrow if @lbox.bbox('end') == []
127    end
128    if cidx && cidx > 0 && (idx == 0 || cidx == @lbox.nearest(10000))
129      _set_sel(cidx - 1)
130    end
131  end
132
133  def _down_proc
134    cidx = @lbox.curselection[0]
135    eidx = @lbox.index('end') - 1
136    idx = @lbox.nearest(10000)
137    if idx <= eidx
138      @lbox.see(idx + 1)
139      _set_sel(cidx + 1) if cidx < eidx
140      @down_arrow.pack_forget if idx + 1 == eidx
141      @down_timer.stop if idx == eidx
142      _show_up_arrow if @lbox.bbox(0) == []
143    end
144    if cidx && cidx < eidx && (eidx == idx || cidx == @lbox.nearest(0))
145      _set_sel(cidx + 1)
146    end
147  end
148
149  def _key_UP_proc
150    cidx = @lbox.curselection[0]
151    _set_sel(cidx = @lbox.index('activate')) unless cidx
152    cidx -= 1
153    if cidx == 0
154      @up_arrow.pack_forget
155    elsif cidx == @lbox.nearest(0)
156      @lbox.see(cidx - 1)
157    end
158  end
159
160  def _key_DOWN_proc
161    cidx = @lbox.curselection[0]
162    _set_sel(cidx = @lbox.index('activate')) unless cidx
163    cidx += 1
164    if cidx == @lbox.index('end') - 1
165      @down_arrow.pack_forget
166    elsif cidx == @lbox.nearest(10000)
167      @lbox.see(cidx + 1)
168    end
169  end
170
171  def _config_proc
172    if @lbox.size == 0
173      @up_arrow.pack_forget
174      @down_arrow.pack_forget
175      return
176    end
177    tidx = @lbox.nearest(0)
178    bidx = @lbox.nearest(10000)
179    if tidx > 0
180      _show_up_arrow
181      tidx += 1
182    else
183      @up_arrow.pack_forget unless @up_timer.running?
184    end
185    if bidx < @lbox.index('end') - 1
186      _show_down_arrow
187      bidx -= 1
188    else
189      @down_arrow.pack_forget unless @down_timer.running?
190    end
191    cidx = @lbox.curselection[0]
192    _check_sel(cidx, tidx, bidx) if cidx
193  end
194
195  def _init_binding
196    @up_timer = TkAfter.new(@interval, -1, proc{_up_proc})
197    @down_timer = TkAfter.new(@interval, -1, proc{_down_proc})
198
199    @up_timer.set_start_proc(@initwait, proc{})
200    @down_timer.set_start_proc(@initwait, proc{})
201
202    @up_arrow.bind('Enter', proc{@up_timer.start})
203    @up_arrow.bind('Leave', proc{@up_timer.stop if @up_arrow.winfo_mapped?})
204    @down_arrow.bind('Enter', proc{@down_timer.start})
205    @down_arrow.bind('Leave', proc{@down_timer.stop if @down_arrow.winfo_mapped?})
206
207    @lbox.bind('Configure', proc{_config_proc})
208    @lbox.bind('Enter', proc{|y| _set_sel(@lbox.nearest(y))}, '%y')
209    @lbox.bind('Motion', proc{|y|
210                 @up_timer.stop if @up_timer.running?
211                 @down_timer.stop if @down_timer.running?
212                 _check_sel(@lbox.nearest(y))
213               }, '%y')
214
215    @lbox.bind('Up', proc{_key_UP_proc})
216    @lbox.bind('Down', proc{_key_DOWN_proc})
217  end
218
219  ############################
220  public
221  ############################
222  def scrollbar(mode)
223    if mode
224      @scr.pack(:side=>:right, :fill=>:y)
225    else
226      @scr.pack_forget
227    end
228  end
229end
230
231################################################
232
233class Tk::RbWidget::Combobox < TkEntry
234  include TkComposite
235
236  @@down_btn_bmp = TkBitmapImage.new(:data=><<EOD)
237#define down_arrow_width 11
238#define down_arrow_height 11
239static unsigned char down_arrow_bits[] = {
240   0x00, 0x00, 0xfe, 0x03, 0xfc, 0x01, 0xfc, 0x01, 0xf8, 0x00, 0xf8, 0x00,
241   0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00};
242EOD
243
244  @@up_btn_bmp = TkBitmapImage.new(:data=><<EOD)
245#define up_arrow_width 11
246#define up_arrow_height 11
247static unsigned char up_arrow_bits[] = {
248   0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x70, 0x00, 0x70, 0x00, 0xf8, 0x00,
249   0xf8, 0x00, 0xfc, 0x01, 0xfc, 0x01, 0xfe, 0x03, 0x00, 0x00};
250EOD
251
252  def _button_proc(dir = true)
253    return if @ent.state == 'disabled'
254    @btn.relief(:sunken)
255    x = @frame.winfo_rootx
256    y = @frame.winfo_rooty
257    if dir
258      @top.geometry("+#{x}+#{y + @frame.winfo_height}")
259    else
260      @btn.image(@@up_btn_bmp)
261      @top.geometry("+#{x}+#{y - @top.winfo_reqheight}")
262    end
263    @top.deiconify
264    @lst.focus
265
266    if (idx = values.index(@ent.value))
267      @lst.see(idx - 1)
268      @lst.activate(idx)
269      @lst.selection_set(idx)
270    elsif @lst.size > 0
271      @lst.see(0)
272      @lst.activate(0)
273      @lst.selection_set(0)
274    end
275    @top.grab
276
277    begin
278      @wait_var.tkwait
279      if (idx = @wait_var.to_i) >= 0
280        # @ent.value = @lst.get(idx)
281        _set_entry_value(@lst.get(idx))
282      end
283      @top.withdraw
284      @btn.relief(:raised)
285      @btn.image(@@down_btn_bmp)
286    rescue
287    ensure
288      begin
289        @top.grab(:release)
290        @ent.focus
291      rescue
292      end
293    end
294  end
295  private :_button_proc
296
297  def _init_bindings
298    @btn.bind('1', proc{_button_proc(true)})
299    @btn.bind('3', proc{_button_proc(false)})
300
301    @lst.bind('1', proc{|y| @wait_var.value = @lst.nearest(y)}, '%y')
302    @lst.bind('Return', proc{@wait_var.value = @lst.curselection[0]})
303
304    cancel = TkVirtualEvent.new('2', '3', 'Escape')
305    @lst.bind(cancel, proc{@wait_var.value = -1})
306  end
307  private :_init_bindings
308
309  def _set_entry_value(val)
310    @ent.textvariable.value = val
311  end
312  private :_set_entry_value
313
314  #----------------------------------------------------
315
316  def _state_control(value = None)
317    if value == None
318      # get
319      @ent.state
320    else
321      # set
322      @ent.state(value.to_s)
323      case value = @ent.state # regulate 'state' string
324      when 'normal', 'readonly'
325        @btn.state 'normal'
326      when 'disabled'
327        @btn.state 'disabled'
328      else
329        # unknown : do nothing
330      end
331    end
332  end
333  private :_state_control
334
335  def __methodcall_optkeys  # { key=>method, ... }
336    {'state' => :_state_control}
337  end
338  private :__methodcall_optkeys
339
340  #----------------------------------------------------
341
342  def _textvariable_control(var = None)
343    if var == None
344      # get
345      ((var = @ent.textvariable) === @default_var)? nil: var
346    else
347      # set
348      @var = var
349      tk_send('configure', '-textvariable', (@var)? var: @default_var)
350    end
351  end
352  private :_textvariable_control
353
354  #----------------------------------------------------
355
356  def initialize_composite(keys={})
357    keys = _symbolkey2str(keys)
358
359    @btn = TkLabel.new(@frame, :relief=>:raised, :borderwidth=>2,
360                       :image=>@@down_btn_bmp).pack(:side=>:right,
361                                                    :ipadx=>2, :fill=>:y)
362    @ent = TkEntry.new(@frame).pack(:side=>:left)
363    @path = @ent.path
364
365    @top = TkToplevel.new(@btn, :borderwidth=>1, :relief=>:raised) {
366      withdraw
367      transient
368      overrideredirect(true)
369    }
370
371    startwait = keys.delete('startwait'){300}
372    interval = keys.delete('interval'){150}
373    @lst = Tk::RbWidget::AutoScrollListbox.new(@top, :scrollbar=>true,
374                                               :startwait=>startwait,
375                                               :interval=>interval)
376    @lst.pack(:fill=>:both, :expand=>true)
377    @ent_list = []
378
379    @wait_var = TkVariable.new
380    @var = @default_var = TkVariable.new
381
382    @ent.textvariable @default_var
383
384    _init_bindings
385
386    option_methods('textvariable' => :_textvariable_control)
387
388    delegate('DEFAULT', @ent)
389    delegate('height', @lst)
390    delegate('relief', @frame)
391    delegate('borderwidth', @frame)
392
393    delegate('arrowrelief', @lst)
394    delegate('arrowborderwidth', @lst)
395
396    delegate('state', false)
397
398    if mode = keys.delete('scrollbar')
399      scrollbar(mode)
400    end
401
402    configure keys unless keys.empty?
403  end
404  private :initialize_composite
405
406  def scrollbar(mode)
407    @lst.scrollbar(mode)
408  end
409
410  def _reset_width
411    len = @ent.width
412    @lst.get(0, 'end').each{|l| len = l.length if l.length > len}
413    @lst.width(len + 1)
414  end
415  private :_reset_width
416
417  def add(ent)
418    ent = ent.to_s
419    unless @ent_list.index(ent)
420      @ent_list << ent
421      @lst.insert('end', ent)
422    end
423    _reset_width
424    self
425  end
426
427  def remove(ent)
428    ent = ent.to_s
429    @ent_list.delete(ent)
430    if idx = @lst.get(0, 'end').index(ent)
431      @lst.delete(idx)
432    end
433    _reset_width
434    self
435  end
436
437  def values(ary = nil)
438    if ary
439      @lst.delete(0, 'end')
440      @ent_list.clear
441      ary.each{|ent| add(ent)}
442      _reset_width
443      self
444    else
445      @lst.get(0, 'end')
446    end
447  end
448
449  def see(idx)
450    @lst.see(@lst.index(idx) - 1)
451  end
452
453  def list_index(idx)
454    @lst.index(idx)
455  end
456end
457
458
459################################################
460# test
461################################################
462if __FILE__ == $0
463# e0 = Tk::RbWidget::Combobox.new.pack
464# e0.values(%w(aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu))
465
466  v = TkVariable.new
467  e = Tk::RbWidget::Combobox.new(:height=>7, :scrollbar=>true,
468                                 :textvariable=>v,
469                                 :arrowrelief=>:flat, :arrowborderwidth=>0,
470                                 :startwait=>400, :interval=>200).pack
471  e.values(%w(aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu))
472  #e.see(e.list_index('end') - 2)
473  e.value = 'cc'
474  TkFrame.new{|f|
475    fnt = TkFont.new('Helvetica 10')
476    TkLabel.new(f, :font=>fnt, :text=>'TkCombobox value :').pack(:side=>:left)
477    TkLabel.new(f, :font=>fnt, :textvariable=>v).pack(:side=>:left)
478  }.pack
479
480  TkFrame.new(:relief=>:raised, :borderwidth=>2,
481              :height=>3).pack(:fill=>:x, :expand=>true, :padx=>5, :pady=>3)
482
483  l = Tk::RbWidget::AutoScrollListbox.new(nil, :relief=>:groove,
484                                          :borderwidth=>4,:height=>7,
485                                          :width=>20).pack(:fill=>:both,
486                                                           :expand=>true)
487  (0..20).each{|i| l.insert('end', "line #{i}")}
488
489  TkFrame.new(:relief=>:ridge, :borderwidth=>3){
490    TkButton.new(self, :text=>'ON',
491                 :command=>proc{l.scrollbar(true)}).pack(:side=>:left)
492    TkButton.new(self, :text=>'OFF',
493                 :command=>proc{l.scrollbar(false)}).pack(:side=>:right)
494    pack(:fill=>:x)
495  }
496  Tk.mainloop
497end
498