#
#  tkcombobox.rb : TkAutoScrollbox & TkCombobox
# 
#                         by Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)
#
require 'tk'

class TkAutoScrollbox < TkListbox
  include TkComposite

  @@up_bmp = TkBitmapImage.new(:data=><<EOD)
#define up_arrow_width 9
#define up_arrow_height 9
static unsigned char up_arrow_bits[] = {
   0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x38, 0x00, 0x38, 0x00, 0x7c, 0x00,
   0x7c, 0x00, 0xfe, 0x00, 0x00, 0x00};
EOD

  @@down_bmp = TkBitmapImage.new(:data=><<EOD)
#define up_arrow_width 9
#define up_arrow_height 9
static unsigned char down_arrow_bits[] = {
   0x00, 0x00, 0xfe, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x38, 0x00, 0x38, 0x00,
   0x10, 0x00, 0x10, 0x00, 0x00, 0x00};
EOD

  ############################
  private
  ############################
  def initialize_composite(keys={})
    keys = _symbolkey2str(keys)

    @initwait = keys.delete('startwait'){300}
    @interval = keys.delete('interval'){150}
    @initwait -= @interval
    @initwait = 0 if @initwait < 0

    @lbox = TkListbox.new(@frame, :borderwidth=>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 TkCombobox < TkEntry
  include TkComposite

  @@down_btn_bmp = TkBitmapImage.new(:data=><<EOD)
#define down_arrow_width 11
#define down_arrow_height 11
static unsigned char down_arrow_bits[] = {
   0x00, 0x00, 0xfe, 0x03, 0xfc, 0x01, 0xfc, 0x01, 0xf8, 0x00, 0xf8, 0x00,
   0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00};
EOD

  @@up_btn_bmp = TkBitmapImage.new(:data=><<EOD)
#define up_arrow_width 11
#define up_arrow_height 11
static unsigned char up_arrow_bits[] = {
   0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x70, 0x00, 0x70, 0x00, 0xf8, 0x00,
   0xf8, 0x00, 0xfc, 0x01, 0xfc, 0x01, 0xfe, 0x03, 0x00, 0x00};
EOD

  def _button_proc(dir = true)
    @btn.relief(:sunken)
    x = @frame.winfo_rootx
    y = @frame.winfo_rooty
    if dir
      @top.geometry("+#{x}+#{y + @frame.winfo_height}")
    else
      @btn.image(@@up_btn_bmp)
      @top.geometry("+#{x}+#{y - @top.winfo_reqheight}")
    end
    @top.deiconify
    @lst.focus

    if (idx = values.index(@ent.value))
      @lst.see(idx - 1)
      @lst.activate(idx)
      @lst.selection_set(idx)
    elsif @lst.size > 0
      @lst.see(0)
      @lst.activate(0)
      @lst.selection_set(0)
    end
    @top.grab

    begin
      @var.tkwait
      if (idx = @var.to_i) >= 0
        @ent.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| @var.value = @lst.nearest(y)}, '%y')
    @lst.bind('Return', proc{@var.value = @lst.curselection[0]})

    cancel = TkVirtualEvent.new('2', '3', 'Escape')
    @lst.bind(cancel, proc{@var.value = -1})
  end
  private :_init_bindings

  def initialize_composite(keys={})
    keys = _symbolkey2str(keys)

    @btn = TkLabel.new(@frame, :relief=>:raised, :borderwidth=>3, 
                       :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 = TkAutoScrollbox.new(@top, 
                               :startwait=>startwait, 
                               :interval=>interval).pack(:fill=>:both, 
                                                         :expand=>true)
    @ent_list = []

    @var = TkVariable.new

    _init_bindings

    delegate('DEFAULT', @ent)
    delegate('height', @lst)
    delegate('relief', @frame)
    delegate('borderwidth', @frame)

    delegate('arrowrelief', @lst)
    delegate('arrowborderwidth', @lst)

    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
  v = TkVariable.new
  e = TkCombobox.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 = TkAutoScrollbox.new(nil, :relief=>:groove, :borderwidth=>4, 
                          :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
