canvas.rb   [plain text]


#
#               tk/canvas.rb - Tk canvas classes
#                       $Date: 2007-02-13 08:01:19 +0900 (Tue, 13 Feb 2007) $
#                       by Yukihiro Matsumoto <matz@caelum.co.jp>
#
require 'tk'
require 'tk/canvastag'
require 'tk/itemconfig'
require 'tk/scrollable'

module TkCanvasItemConfig
  include TkItemConfigMethod

  def __item_strval_optkeys(id)
    # maybe need to override
    super(id) + [
      'fill', 'activefill', 'disabledfill', 
      'outline', 'activeoutline', 'disabledoutline'
    ]
  end
  private :__item_strval_optkeys

  def __item_methodcall_optkeys(id)
    {'coords'=>'coords'}
  end
  private :__item_methodcall_optkeys

  def __item_val2ruby_optkeys(id)  # { key=>proc, ... }
    super(id).update('window'=>proc{|i, v| window(v)})
  end
  private :__item_val2ruby_optkeys

  def __item_pathname(tagOrId)
    if tagOrId.kind_of?(TkcItem) || tagOrId.kind_of?(TkcTag)
      self.path + ';' + tagOrId.id.to_s
    else
      self.path + ';' + tagOrId.to_s
    end
  end
  private :__item_pathname
end

class TkCanvas<TkWindow
  include TkCanvasItemConfig
  include Tk::Scrollable

  TkCommandNames = ['canvas'.freeze].freeze
  WidgetClassName = 'Canvas'.freeze
  WidgetClassNames[WidgetClassName] = self

  def __destroy_hook__
    TkcItem::CItemID_TBL.delete(@path)
  end

  #def create_self(keys)
  #  if keys and keys != None
  #    tk_call_without_enc('canvas', @path, *hash_kv(keys, true))
  #  else
  #    tk_call_without_enc('canvas', @path)
  #  end
  #end
  #private :create_self

  def __numval_optkeys
    super() + ['closeenough']
  end
  private :__numval_optkeys

  def __boolval_optkeys
    super() + ['confine']
  end
  private :__boolval_optkeys

  def tagid(tag)
    if tag.kind_of?(TkcItem) || tag.kind_of?(TkcTag)
      tag.id
    else
      tag  # maybe an Array of configure paramters
    end
  end
  private :tagid


  # create a canvas item without creating a TkcItem object
  def create(type, *args)
    type.create(self, *args)
  end


  def addtag(tag, mode, *args)
    mode = mode.to_s
    if args[0] && mode =~ /^(above|below|with(tag)?)$/
      args[0] = tagid(args[0])
    end
    tk_send_without_enc('addtag', tagid(tag), mode, *args)
    self
  end
  def addtag_above(tagOrId, target)
    addtag(tagOrId, 'above', tagid(target))
  end
  def addtag_all(tagOrId)
    addtag(tagOrId, 'all')
  end
  def addtag_below(tagOrId, target)
    addtag(tagOrId, 'below', tagid(target))
  end
  def addtag_closest(tagOrId, x, y, halo=None, start=None)
    addtag(tagOrId, 'closest', x, y, halo, start)
  end
  def addtag_enclosed(tagOrId, x1, y1, x2, y2)
    addtag(tagOrId, 'enclosed', x1, y1, x2, y2)
  end
  def addtag_overlapping(tagOrId, x1, y1, x2, y2)
    addtag(tagOrId, 'overlapping', x1, y1, x2, y2)
  end
  def addtag_withtag(tagOrId, tag)
    addtag(tagOrId, 'withtag', tagid(tag))
  end

  def bbox(tagOrId, *tags)
    list(tk_send_without_enc('bbox', tagid(tagOrId), 
                             *tags.collect{|t| tagid(t)}))
  end

  #def itembind(tag, context, cmd=Proc.new, *args)
  #  _bind([path, "bind", tagid(tag)], context, cmd, *args)
  #  self
  #end
  def itembind(tag, context, *args)
    # if args[0].kind_of?(Proc) || args[0].kind_of?(Method)
    if TkComm._callback_entry?(args[0]) || !block_given?
      cmd = args.shift
    else
      cmd = Proc.new
    end
    _bind([path, "bind", tagid(tag)], context, cmd, *args)
    self
  end

  #def itembind_append(tag, context, cmd=Proc.new, *args)
  #  _bind_append([path, "bind", tagid(tag)], context, cmd, *args)
  #  self
  #end
  def itembind_append(tag, context, *args)
    # if args[0].kind_of?(Proc) || args[0].kind_of?(Method)
    if TkComm._callback_entry?(args[0]) || !block_given?
      cmd = args.shift
    else
      cmd = Proc.new
    end
    _bind_append([path, "bind", tagid(tag)], context, cmd, *args)
    self
  end

  def itembind_remove(tag, context)
    _bind_remove([path, "bind", tagid(tag)], context)
    self
  end

  def itembindinfo(tag, context=nil)
    _bindinfo([path, "bind", tagid(tag)], context)
  end

  def canvasx(screen_x, *args)
    #tk_tcl2ruby(tk_send_without_enc('canvasx', screen_x, *args))
    number(tk_send_without_enc('canvasx', screen_x, *args))
  end
  def canvasy(screen_y, *args)
    #tk_tcl2ruby(tk_send_without_enc('canvasy', screen_y, *args))
    number(tk_send_without_enc('canvasy', screen_y, *args))
  end

  def coords(tag, *args)
    if args == []
      tk_split_list(tk_send_without_enc('coords', tagid(tag)))
    else
      tk_send_without_enc('coords', tagid(tag), *(args.flatten))
      self
    end
  end

  def dchars(tag, first, last=None)
    tk_send_without_enc('dchars', tagid(tag), 
                        _get_eval_enc_str(first), _get_eval_enc_str(last))
    self
  end

  def delete(*args)
    if TkcItem::CItemID_TBL[self.path]
      args.each{|tag|
        find('withtag', tag).each{|item|
          if item.kind_of?(TkcItem)
            TkcItem::CItemID_TBL[self.path].delete(item.id)
          end
        }
      }
    end
    tk_send_without_enc('delete', *args.collect{|t| tagid(t)})
    self
  end
  alias remove delete

  def dtag(tag, tag_to_del=None)
    tk_send_without_enc('dtag', tagid(tag), tagid(tag_to_del))
    self
  end
  alias deltag dtag

  def find(mode, *args)
    list(tk_send_without_enc('find', mode, *args)).collect!{|id| 
      TkcItem.id2obj(self, id)
    }
  end
  def find_above(target)
    find('above', tagid(target))
  end
  def find_all
    find('all')
  end
  def find_below(target)
    find('below', tagid(target))
  end
  def find_closest(x, y, halo=None, start=None)
    find('closest', x, y, halo, start)
  end
  def find_enclosed(x1, y1, x2, y2)
    find('enclosed', x1, y1, x2, y2)
  end
  def find_overlapping(x1, y1, x2, y2)
    find('overlapping', x1, y1, x2, y2)
  end
  def find_withtag(tag)
    find('withtag', tag)
  end

  def itemfocus(tagOrId=nil)
    if tagOrId
      tk_send_without_enc('focus', tagid(tagOrId))
      self
    else
      ret = tk_send_without_enc('focus')
      if ret == ""
        nil
      else
        TkcItem.id2obj(self, ret)
      end
    end
  end

  def gettags(tagOrId)
    list(tk_send_without_enc('gettags', tagid(tagOrId))).collect{|tag|
      TkcTag.id2obj(self, tag)
    }
  end

  def icursor(tagOrId, index)
    tk_send_without_enc('icursor', tagid(tagOrId), index)
    self
  end

  def index(tagOrId, idx)
    number(tk_send_without_enc('index', tagid(tagOrId), idx))
  end

  def insert(tagOrId, index, string)
    tk_send_without_enc('insert', tagid(tagOrId), index, 
                        _get_eval_enc_str(string))
    self
  end

=begin
  def itemcget(tagOrId, option)
    case option.to_s
    when 'dash', 'activedash', 'disableddash'
      conf = tk_send_without_enc('itemcget', tagid(tagOrId), "-#{option}")
      if conf =~ /^[0-9]/
        list(conf)
      else
        conf
      end
    when 'text', 'label', 'show', 'data', 'file', 'maskdata', 'maskfile'
      _fromUTF8(tk_send_without_enc('itemcget', tagid(tagOrId), "-#{option}"))
    when 'font', 'kanjifont'
      #fnt = tk_tcl2ruby(tk_send('itemcget', tagid(tagOrId), "-#{option}"))
      fnt = tk_tcl2ruby(_fromUTF8(tk_send_with_enc('itemcget', tagid(tagOrId), '-font')))
      unless fnt.kind_of?(TkFont)
        fnt = tagfontobj(tagid(tagOrId), fnt)
      end
      if option.to_s == 'kanjifont' && JAPANIZED_TK && TK_VERSION =~ /^4\.*/
        # obsolete; just for compatibility
        fnt.kanji_font
      else
        fnt
      end
    else
      tk_tcl2ruby(_fromUTF8(tk_send_without_enc('itemcget', tagid(tagOrId), 
                                                "-#{option}")))
    end
  end

  def itemconfigure(tagOrId, key, value=None)
    if key.kind_of? Hash
      key = _symbolkey2str(key)
      coords = key.delete('coords')
      self.coords(tagOrId, coords) if coords

      if ( key['font'] || key['kanjifont'] \
          || key['latinfont'] || key['asciifont'] )
        tagfont_configure(tagid(tagOrId), key.dup)
      else
        _fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId), 
                                      *hash_kv(key, true)))
      end

    else
      if ( key == 'coords' || key == :coords )
        self.coords(tagOrId, value)
      elsif ( key == 'font' || key == :font || 
              key == 'kanjifont' || key == :kanjifont || 
              key == 'latinfont' || key == :latinfont || 
              key == 'asciifont' || key == :asciifont )
        if value == None
          tagfontobj(tagid(tagOrId))
        else
          tagfont_configure(tagid(tagOrId), {key=>value})
        end
      else
        _fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId), 
                                      "-#{key}", _get_eval_enc_str(value)))
      end
    end
    self
  end
#  def itemconfigure(tagOrId, key, value=None)
#    if key.kind_of? Hash
#      tk_send 'itemconfigure', tagid(tagOrId), *hash_kv(key)
#    else
#      tk_send 'itemconfigure', tagid(tagOrId), "-#{key}", value
#    end
#  end
#  def itemconfigure(tagOrId, keys)
#    tk_send 'itemconfigure', tagid(tagOrId), *hash_kv(keys)
#  end

  def itemconfiginfo(tagOrId, key=nil)
    if TkComm::GET_CONFIGINFO_AS_ARRAY
      if key
        case key.to_s
        when 'coords'
          return ['coords', '', '', '', self.coords(tagOrId)]
        when 'dash', 'activedash', 'disableddash'
          conf = tk_split_simplelist(tk_send_without_enc('itemconfigure', tagid(tagOrId), "-#{key}"))
          if conf[3] && conf[3] =~ /^[0-9]/
            conf[3] = list(conf[3])
          end
          if conf[4] && conf[4] =~ /^[0-9]/
            conf[4] = list(conf[4])
          end
        when 'text', 'label', 'show', 'data', 'file', 'maskdata', 'maskfile'
          conf = tk_split_simplelist(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId), "-#{key}")))
        when 'font', 'kanjifont'
          conf = tk_split_simplelist(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId),"-#{key}")))
          conf[4] = tagfont_configinfo(tagid(tagOrId), conf[4])
        else
          conf = tk_split_list(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId), "-#{key}")))
        end
        conf[0] = conf[0][1..-1]
        conf
      else
        ret = tk_split_simplelist(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId)))).collect{|conflist|
          conf = tk_split_simplelist(conflist)
          conf[0] = conf[0][1..-1]
          case conf[0]
          when 'text', 'label', 'show', 'data', 'file', 'maskdata', 'maskfile'
          when 'dash', 'activedash', 'disableddash'
            if conf[3] && conf[3] =~ /^[0-9]/
              conf[3] = list(conf[3])
            end
            if conf[4] && conf[4] =~ /^[0-9]/
              conf[4] = list(conf[4])
            end
          else
            if conf[3]
              if conf[3].index('{')
                conf[3] = tk_split_list(conf[3]) 
              else
                conf[3] = tk_tcl2ruby(conf[3]) 
              end
            end
            if conf[4]
              if conf[4].index('{')
                conf[4] = tk_split_list(conf[4]) 
              else
                conf[4] = tk_tcl2ruby(conf[4]) 
              end
            end
          end
          conf[1] = conf[1][1..-1] if conf.size == 2 # alias info
          conf
        }

        fontconf = ret.assoc('font')
        if fontconf
          ret.delete_if{|item| item[0] == 'font' || item[0] == 'kanjifont'}
          fontconf[4] = tagfont_configinfo(tagid(tagOrId), fontconf[4])
          ret.push(fontconf)
        end

        ret << ['coords', '', '', '', self.coords(tagOrId)]
      end
    else # ! TkComm::GET_CONFIGINFO_AS_ARRAY
      if key
        case key.to_s
        when 'coords'
          {'coords' => ['', '', '', self.coords(tagOrId)]}
        when 'dash', 'activedash', 'disableddash'
          conf = tk_split_simplelist(tk_send_without_enc('itemconfigure', 
                                                         tagid(tagOrId), 
                                                         "-#{key}"))
          if conf[3] && conf[3] =~ /^[0-9]/
            conf[3] = list(conf[3])
          end
          if conf[4] && conf[4] =~ /^[0-9]/
            conf[4] = list(conf[4])
          end
        when 'text', 'label', 'show', 'data', 'file', 'maskdata', 'maskfile'
          conf = tk_split_simplelist(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId), "-#{key}")))
        when 'font', 'kanjifont'
          conf = tk_split_simplelist(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId),"-#{key}")))
          conf[4] = tagfont_configinfo(tagid(tagOrId), conf[4])
        else
          conf = tk_split_list(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId), "-#{key}")))
        end
        key = conf.shift[1..-1]
        { key => conf }
      else
        ret = {}
        tk_split_simplelist(_fromUTF8(tk_send_without_enc('itemconfigure', tagid(tagOrId)))).each{|conflist|
          conf = tk_split_simplelist(conflist)
          key = conf.shift[1..-1]
          case key
          when 'text', 'label', 'show', 'data', 'file', 'maskdata', 'maskfile'
          when 'dash', 'activedash', 'disableddash'
            if conf[2] && conf[2] =~ /^[0-9]/
              conf[2] = list(conf[2])
            end
            if conf[3] && conf[3] =~ /^[0-9]/
              conf[3] = list(conf[3])
            end
          else
            if conf[2]
              if conf[2].index('{')
                conf[2] = tk_split_list(conf[2]) 
              else
                conf[2] = tk_tcl2ruby(conf[2]) 
              end
            end
            if conf[3]
              if conf[3].index('{')
                conf[3] = tk_split_list(conf[3]) 
              else
                conf[3] = tk_tcl2ruby(conf[3]) 
              end
            end
          end
          if conf.size == 1
            ret[key] = conf[0][1..-1]  # alias info
          else
            ret[key] = conf
          end
        }

        fontconf = ret['font']
        if fontconf
          ret.delete('font')
          ret.delete('kanjifont')
          fontconf[3] = tagfont_configinfo(tagid(tagOrId), fontconf[3])
          ret['font'] = fontconf
        end

        ret['coords'] = ['', '', '', self.coords(tagOrId)]

        ret
      end
    end
  end

  def current_itemconfiginfo(tagOrId, key=nil)
    if TkComm::GET_CONFIGINFO_AS_ARRAY
      if key
        conf = itemconfiginfo(tagOrId, key)
        {conf[0] => conf[4]}
      else
        ret = {}
        itemconfiginfo(tagOrId).each{|conf|
          ret[conf[0]] = conf[4] if conf.size > 2
        }
        ret
      end
    else # ! TkComm::GET_CONFIGINFO_AS_ARRAY
      ret = {}
      itemconfiginfo(tagOrId, key).each{|k, conf|
        ret[k] = conf[-1] if conf.kind_of?(Array)
      }
      ret
    end
  end
=end

  def lower(tag, below=nil)
    if below
      tk_send_without_enc('lower', tagid(tag), tagid(below))
    else
      tk_send_without_enc('lower', tagid(tag))
    end
    self
  end

  def move(tag, x, y)
    tk_send_without_enc('move', tagid(tag), x, y)
    self
  end

  def postscript(keys)
    tk_send("postscript", *hash_kv(keys))
  end

  def raise(tag, above=nil)
    if above
      tk_send_without_enc('raise', tagid(tag), tagid(above))
    else
      tk_send_without_enc('raise', tagid(tag))
    end
    self
  end

  def scale(tag, x, y, xs, ys)
    tk_send_without_enc('scale', tagid(tag), x, y, xs, ys)
    self
  end

  def scan_mark(x, y)
    tk_send_without_enc('scan', 'mark', x, y)
    self
  end
  def scan_dragto(x, y, gain=None)
    tk_send_without_enc('scan', 'dragto', x, y, gain)
    self
  end

  def select(mode, *args)
    r = tk_send_without_enc('select', mode, *args)
    (mode == 'item')? TkcItem.id2obj(self, r): self
  end
  def select_adjust(tagOrId, index)
    select('adjust', tagid(tagOrId), index)
  end
  def select_clear
    select('clear')
  end
  def select_from(tagOrId, index)
    select('from', tagid(tagOrId), index)
  end
  def select_item
    select('item')
  end
  def select_to(tagOrId, index)
    select('to', tagid(tagOrId), index)
  end

  def itemtype(tag)
    TkcItem.type2class(tk_send('type', tagid(tag)))
  end
end

class TkcItem<TkObject
  extend Tk
  include TkcTagAccess
  extend TkItemFontOptkeys
  extend TkItemConfigOptkeys

  CItemTypeName = nil
  CItemTypeToClass = {}
  CItemID_TBL = TkCore::INTERP.create_table

  TkCore::INTERP.init_ip_env{ CItemID_TBL.clear }

  def TkcItem.type2class(type)
    CItemTypeToClass[type]
  end

  def TkcItem.id2obj(canvas, id)
    cpath = canvas.path
    return id unless CItemID_TBL[cpath]
    CItemID_TBL[cpath][id]? CItemID_TBL[cpath][id]: id
  end

  ########################################
  def self._parse_create_args(args)
    fontkeys = {}
    methodkeys = {}
    if args[-1].kind_of? Hash
      keys = _symbolkey2str(args.pop)
      if args.size == 0
        args = keys.delete('coords')
        unless args.kind_of?(Array)
          fail "coords parameter must be given by an Array"
        end
      end

      #['font', 'kanjifont', 'latinfont', 'asciifont'].each{|key|
      #  fontkeys[key] = keys.delete(key) if keys.key?(key)
      #}
      __item_font_optkeys(nil).each{|key|
        fkey = key.to_s
        fontkeys[fkey] = keys.delete(fkey) if keys.key?(fkey)

        fkey = "kanji#{key}"
        fontkeys[fkey] = keys.delete(fkey) if keys.key?(fkey)

        fkey = "latin#{key}"
        fontkeys[fkey] = keys.delete(fkey) if keys.key?(fkey)

        fkey = "ascii#{key}"
        fontkeys[fkey] = keys.delete(fkey) if keys.key?(fkey)
      }

      __item_methodcall_optkeys(nil).each{|key|
        key = key.to_s
        methodkeys[key] = keys.delete(key) if keys.key?(key)
      }

      __item_ruby2val_optkeys(nil).each{|key, method|
        key = key.to_s
        keys[key] = method.call(keys[key]) if keys.has_key?(key)
      }

      #args = args.flatten.concat(hash_kv(keys))
      args = args.flatten.concat(itemconfig_hash_kv(nil, keys))
    else
      args = args.flatten
    end

    [args, fontkeys]
  end
  private_class_method :_parse_create_args

  def self.create(canvas, *args)
    unless self::CItemTypeName
      fail RuntimeError, "#{self} is an abstract class"
    end
    args, fontkeys = _parse_create_args(args)
    idnum = tk_call_without_enc(canvas.path, 'create', 
                                self::CItemTypeName, *args)
    canvas.itemconfigure(idnum, fontkeys) unless fontkeys.empty?
    idnum.to_i  # 'canvas item id' is an integer number
  end
  ########################################

  def initialize(parent, *args)
    #unless parent.kind_of?(TkCanvas)
    #  fail ArgumentError, "expect TkCanvas for 1st argument"
    #end
    @parent = @c = parent
    @path = parent.path

    @id = create_self(*args) # an integer number as 'canvas item id'
    CItemID_TBL[@path] = {} unless CItemID_TBL[@path]
    CItemID_TBL[@path][@id] = self
  end
  def create_self(*args)
    self.class.create(@c, *args) # return an integer number as 'canvas item id'
  end
  private :create_self

  def id
    @id
  end

  def exist?
    if @c.find_withtag(@id)
      true
    else
      false
    end
  end

  def delete
    @c.delete @id
    CItemID_TBL[@path].delete(@id) if CItemID_TBL[@path]
    self
  end
  alias remove  delete
  alias destroy delete
end

class TkcArc<TkcItem
  CItemTypeName = 'arc'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcBitmap<TkcItem
  CItemTypeName = 'bitmap'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcImage<TkcItem
  CItemTypeName = 'image'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcLine<TkcItem
  CItemTypeName = 'line'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcOval<TkcItem
  CItemTypeName = 'oval'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcPolygon<TkcItem
  CItemTypeName = 'polygon'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcRectangle<TkcItem
  CItemTypeName = 'rectangle'.freeze
  CItemTypeToClass[CItemTypeName] = self
end

class TkcText<TkcItem
  CItemTypeName = 'text'.freeze
  CItemTypeToClass[CItemTypeName] = self
  def self.create(canvas, *args)
    if args[-1].kind_of?(Hash)
      keys = _symbolkey2str(args.pop)
      txt = keys['text']
      keys['text'] = _get_eval_enc_str(txt) if txt
      args.push(keys)
    end
    super(canvas, *args)
  end
end

class TkcWindow<TkcItem
  CItemTypeName = 'window'.freeze
  CItemTypeToClass[CItemTypeName] = self
  def self.create(canvas, *args)
    if args[-1].kind_of?(Hash)
      keys = _symbolkey2str(args.pop)
      win = keys['window']
      # keys['window'] = win.epath if win.kind_of?(TkWindow)
      keys['window'] = _epath(win) if win
      args.push(keys)
    end
    super(canvas, *args)
  end
end