# # tkcombobox.rb : auto scrollbox & combobox # # by Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp) # require 'tk' module Tk module RbWidget class AutoScrollListbox < TkListbox end class Combobox < TkEntry end end end class Tk::RbWidget::AutoScrollListbox include TkComposite @@up_bmp = TkBitmapImage.new(:data=><<0) @path = @lbox.path TkPack.propagate(@lbox, false) @scr = TkScrollbar.new(@frame, :width=>10) @lbox.yscrollcommand(proc{|*args| @scr.set(*args); _config_proc}) @scr.command(proc{|*args| @lbox.yview(*args); _config_proc}) @up_arrow = TkLabel.new(@lbox, :image=>@@up_bmp, :relief=>:raised, :borderwidth=>1) @down_arrow = TkLabel.new(@lbox, :image=>@@down_bmp, :relief=>:raised, :borderwidth=>1) _init_binding @lbox.pack(:side=>:left, :fill=>:both, :expand=>:true) delegate('DEFAULT', @lbox) delegate('background', @frame, @scr) delegate('activebackground', @scr) delegate('troughcolor', @scr) delegate('repeatdelay', @scr) delegate('repeatinterval', @scr) delegate('relief', @frame) delegate('borderwidth', @frame) delegate_alias('arrowrelief', 'relief', @up_arrow, @down_arrow) delegate_alias('arrowborderwidth', 'borderwidth', @up_arrow, @down_arrow) scrollbar(keys.delete('scrollbar')){false} configure keys unless keys.empty? end def _show_up_arrow unless @up_arrow.winfo_mapped? @up_arrow.pack(:side=>:top, :fill=>:x) end end def _show_down_arrow unless @down_arrow.winfo_mapped? @down_arrow.pack(:side=>:bottom, :fill=>:x) end end def _set_sel(idx) @lbox.activate(idx) @lbox.selection_clear(0, 'end') @lbox.selection_set(idx) end def _check_sel(cidx, tidx = nil, bidx = nil) _set_sel(cidx) unless tidx tidx = @lbox.nearest(0) tidx += 1 if tidx > 0 end unless bidx bidx = @lbox.nearest(10000) bidx -= 1 if bidx < @lbox.index('end') - 1 end if cidx > bidx _set_sel(bidx) end if cidx < tidx _set_sel(tidx) end end def _up_proc cidx = @lbox.curselection[0] idx = @lbox.nearest(0) if idx >= 0 @lbox.see(idx - 1) _set_sel(idx) @up_arrow.pack_forget if idx == 1 @up_timer.stop if idx == 0 _show_down_arrow if @lbox.bbox('end') == [] end if cidx && cidx > 0 && (idx == 0 || cidx == @lbox.nearest(10000)) _set_sel(cidx - 1) end end def _down_proc cidx = @lbox.curselection[0] eidx = @lbox.index('end') - 1 idx = @lbox.nearest(10000) if idx <= eidx @lbox.see(idx + 1) _set_sel(cidx + 1) if cidx < eidx @down_arrow.pack_forget if idx + 1 == eidx @down_timer.stop if idx == eidx _show_up_arrow if @lbox.bbox(0) == [] end if cidx && cidx < eidx && (eidx == idx || cidx == @lbox.nearest(0)) _set_sel(cidx + 1) end end def _key_UP_proc cidx = @lbox.curselection[0] _set_sel(cidx = @lbox.index('activate')) unless cidx cidx -= 1 if cidx == 0 @up_arrow.pack_forget elsif cidx == @lbox.nearest(0) @lbox.see(cidx - 1) end end def _key_DOWN_proc cidx = @lbox.curselection[0] _set_sel(cidx = @lbox.index('activate')) unless cidx cidx += 1 if cidx == @lbox.index('end') - 1 @down_arrow.pack_forget elsif cidx == @lbox.nearest(10000) @lbox.see(cidx + 1) end end def _config_proc if @lbox.size == 0 @up_arrow.pack_forget @down_arrow.pack_forget return end tidx = @lbox.nearest(0) bidx = @lbox.nearest(10000) if tidx > 0 _show_up_arrow tidx += 1 else @up_arrow.pack_forget unless @up_timer.running? end if bidx < @lbox.index('end') - 1 _show_down_arrow bidx -= 1 else @down_arrow.pack_forget unless @down_timer.running? end cidx = @lbox.curselection[0] _check_sel(cidx, tidx, bidx) if cidx end def _init_binding @up_timer = TkAfter.new(@interval, -1, proc{_up_proc}) @down_timer = TkAfter.new(@interval, -1, proc{_down_proc}) @up_timer.set_start_proc(@initwait, proc{}) @down_timer.set_start_proc(@initwait, proc{}) @up_arrow.bind('Enter', proc{@up_timer.start}) @up_arrow.bind('Leave', proc{@up_timer.stop if @up_arrow.winfo_mapped?}) @down_arrow.bind('Enter', proc{@down_timer.start}) @down_arrow.bind('Leave', proc{@down_timer.stop if @down_arrow.winfo_mapped?}) @lbox.bind('Configure', proc{_config_proc}) @lbox.bind('Enter', proc{|y| _set_sel(@lbox.nearest(y))}, '%y') @lbox.bind('Motion', proc{|y| @up_timer.stop if @up_timer.running? @down_timer.stop if @down_timer.running? _check_sel(@lbox.nearest(y)) }, '%y') @lbox.bind('Up', proc{_key_UP_proc}) @lbox.bind('Down', proc{_key_DOWN_proc}) end ############################ public ############################ def scrollbar(mode) if mode @scr.pack(:side=>:right, :fill=>:y) else @scr.pack_forget end end end ################################################ class Tk::RbWidget::Combobox < TkEntry include TkComposite @@down_btn_bmp = TkBitmapImage.new(:data=><< 0 @lst.see(0) @lst.activate(0) @lst.selection_set(0) end @top.grab begin @wait_var.tkwait if (idx = @wait_var.to_i) >= 0 # @ent.value = @lst.get(idx) _set_entry_value(@lst.get(idx)) end @top.withdraw @btn.relief(:raised) @btn.image(@@down_btn_bmp) rescue ensure begin @top.grab(:release) @ent.focus rescue end end end private :_button_proc def _init_bindings @btn.bind('1', proc{_button_proc(true)}) @btn.bind('3', proc{_button_proc(false)}) @lst.bind('1', proc{|y| @wait_var.value = @lst.nearest(y)}, '%y') @lst.bind('Return', proc{@wait_var.value = @lst.curselection[0]}) cancel = TkVirtualEvent.new('2', '3', 'Escape') @lst.bind(cancel, proc{@wait_var.value = -1}) end private :_init_bindings def _set_entry_value(val) @ent.textvariable.value = val end private :_set_entry_value #---------------------------------------------------- def _state_control(value = None) if value == None # get @ent.state else # set @ent.state(value.to_s) case value = @ent.state # regulate 'state' string when 'normal', 'readonly' @btn.state 'normal' when 'disabled' @btn.state 'disabled' else # unknown : do nothing end end end private :_state_control def __methodcall_optkeys # { key=>method, ... } {'state' => :_state_control} end private :__methodcall_optkeys #---------------------------------------------------- def _textvariable_control(var = None) if var == None # get ((var = @ent.textvariable) === @default_var)? nil: var else # set @var = var tk_send('configure', '-textvariable', (@var)? var: @default_var) end end private :_textvariable_control #---------------------------------------------------- def initialize_composite(keys={}) keys = _symbolkey2str(keys) @btn = TkLabel.new(@frame, :relief=>:raised, :borderwidth=>2, :image=>@@down_btn_bmp).pack(:side=>:right, :ipadx=>2, :fill=>:y) @ent = TkEntry.new(@frame).pack(:side=>:left) @path = @ent.path @top = TkToplevel.new(@btn, :borderwidth=>1, :relief=>:raised) { withdraw transient overrideredirect(true) } startwait = keys.delete('startwait'){300} interval = keys.delete('interval'){150} @lst = Tk::RbWidget::AutoScrollListbox.new(@top, :scrollbar=>true, :startwait=>startwait, :interval=>interval) @lst.pack(:fill=>:both, :expand=>true) @ent_list = [] @wait_var = TkVariable.new @var = @default_var = TkVariable.new @ent.textvariable @default_var _init_bindings option_methods('textvariable' => :_textvariable_control) delegate('DEFAULT', @ent) delegate('height', @lst) delegate('relief', @frame) delegate('borderwidth', @frame) delegate('arrowrelief', @lst) delegate('arrowborderwidth', @lst) delegate('state', false) if mode = keys.delete('scrollbar') scrollbar(mode) end configure keys unless keys.empty? end private :initialize_composite def scrollbar(mode) @lst.scrollbar(mode) end def _reset_width len = @ent.width @lst.get(0, 'end').each{|l| len = l.length if l.length > len} @lst.width(len + 1) end private :_reset_width def add(ent) ent = ent.to_s unless @ent_list.index(ent) @ent_list << ent @lst.insert('end', ent) end _reset_width self end def remove(ent) ent = ent.to_s @ent_list.delete(ent) if idx = @lst.get(0, 'end').index(ent) @lst.delete(idx) end _reset_width self end def values(ary = nil) if ary @lst.delete(0, 'end') @ent_list.clear ary.each{|ent| add(ent)} _reset_width self else @lst.get(0, 'end') end end def see(idx) @lst.see(@lst.index(idx) - 1) end def list_index(idx) @lst.index(idx) end end ################################################ # test ################################################ if __FILE__ == $0 # e0 = Tk::RbWidget::Combobox.new.pack # e0.values(%w(aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu)) v = TkVariable.new e = Tk::RbWidget::Combobox.new(:height=>7, :scrollbar=>true, :textvariable=>v, :arrowrelief=>:flat, :arrowborderwidth=>0, :startwait=>400, :interval=>200).pack e.values(%w(aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu)) #e.see(e.list_index('end') - 2) e.value = 'cc' TkFrame.new{|f| fnt = TkFont.new('Helvetica 10') TkLabel.new(f, :font=>fnt, :text=>'TkCombobox value :').pack(:side=>:left) TkLabel.new(f, :font=>fnt, :textvariable=>v).pack(:side=>:left) }.pack TkFrame.new(:relief=>:raised, :borderwidth=>2, :height=>3).pack(:fill=>:x, :expand=>true, :padx=>5, :pady=>3) l = Tk::RbWidget::AutoScrollListbox.new(nil, :relief=>:groove, :borderwidth=>4,:height=>7, :width=>20).pack(:fill=>:both, :expand=>true) (0..20).each{|i| l.insert('end', "line #{i}")} TkFrame.new(:relief=>:ridge, :borderwidth=>3){ TkButton.new(self, :text=>'ON', :command=>proc{l.scrollbar(true)}).pack(:side=>:left) TkButton.new(self, :text=>'OFF', :command=>proc{l.scrollbar(false)}).pack(:side=>:right) pack(:fill=>:x) } Tk.mainloop end