Updated 2012-10-17 01:30:28 by RLE

FF 2007-05-26 - This widget has various uses. Basically it's a grid/spreadsheet like widget. --

WJP 2008-06-29 - A little bit more of an explanation of what this is for and how to use it would be very helpful. FF Spreadsheet/data-grid. Basically viewing and editing a grid of data (not necessarily numbers... try [.t columnconfigure 0 -type note] for example ;p HexEdit - an hexadecimal editor is another example, although incomplete, and doesn't feature almost any cool feature of TrackerWidget... more to come...)

ChangeLog

2008-07-17 - FF - Standardized methods arguments, added selectionmethod, notify callbacks for cursor movement, selection, data change, added selection iterator, added readonly state; introduced public/private methods in its little OO system.

2008-07-01 - FF - Added configure command, columnconfigure command, improved keyboard support - this is coming up really nice, although it needs a bit more optimization :] -- and: minor fixes, support for very long numbers

2008-06-30 - FF - After counting how many tracker_* and tracker_struct was repeated in the source, I created a tiny object system :)) you can see the benefit looking at the diff; thanks to DGP for helping me in removing all those ugly evals

2008-06-29 - FF - Full scroll/visibility support

2008-06-28 - FF - Added more color keys, added partial scroll support

older - FF - The big mess

Usage example:
 pack [tracker::tracker .t \
             -rows 32 -cols 24 \
             -colchars {8 1 1 1 2 4 1 4 1 2 3 2 1} \
             -hl1 4 -hl2 16]
 
 # fill with random data
 set dgts {0 1 2 3 4 5 6 7 8 9 a b c d e f} ; set dgtsl [llength $dgts]

 for {set y 0} {$y < 32} {incr y} {
     for {set x 0} {$x < 24} {incr x} {
         set d {}
         for {set z 0} {$z < 8} {incr z} {
             append d [lindex $dgts [expr {int(rand()*$dgtsl)}]]
         }
         .t setdata $y $x $d
     }
 }
 
 .t move 5 8
 .t setsel 2 2 5 5


AMG: This example doesn't work, since the -colchars option doesn't exist.

It's interesting that you made your own object system, but have you considered rewriting this in terms of [namespace ensemble], TclOO, or (heh) [sproc]? It could also be turned into a snidget.
 #!/usr/bin/env wish
 
 #######################################################################
 #
 # TrackerWidget
 # written by Federico Ferri - 2007-2008
 #
 #######################################################################
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # the Free Software Foundation; version 2 of the License.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 #######################################################################
 
 package require Tk
 
 package provide tracker 1.0
 
 namespace eval tracker {
         variable valid_options {
                 -rows -cols -hl1 -hl2
                 -width -height -hspacing -vspacing
                 -xscrollcommand -yscrollcommand
                 -background -backgroundHl1 -backgroundHl2
                 -foreground -foregroundHl1 -foregroundHl2
                 -backgroundsel -backgroundselHl1 -backgroundselHl2
                 -foregroundsel -foregroundselHl1 -foregroundselHl2
                 -backgroundcur -backgroundcurHl1 -backgroundcurHl2
                 -foregroundcur -foregroundcurHl1 -foregroundcurHl2
                 -selectionmethod
                 -selectionnotify -setdatanotify -cursornotify
                 -state
         }
         variable public_method_prefix @
         variable private_method_prefix #
 
         proc static {method name arglist body} {
                 # just eye candy wrapper for proc => static method
                 if {$method != {method}} {
                         return -code error "must be: static method <name> <arglist> <body>"
                 }
                 uplevel 1 [list proc ${name} $arglist $body]
         }
 
         proc constructor {arglist body} {
                 uplevel 1 [list method public tracker $arglist $body]
         }
 
         proc method {qualifier name arglist body} {
                 set valid_qualifiers {public private}
                 if {[lsearch -exact $valid_qualifiers $qualifier] < 0} {
                         return -code error "qualifier must be one of: $valid_qualifiers"
                 }
                 variable ${qualifier}_method_prefix
                 variable _
                 set m_prefix [set ${qualifier}_method_prefix]
                 # preprocess method body:
                 set pbody [regsub -all {self\(} $body _(\$w:]
                 if {"::$name" != [namespace current]} {set name "${m_prefix}${name}"}
                 uplevel 1 [list proc $name [linsert $arglist 0 w] "variable _; $pbody"]
         }
 
         proc public {method name arglist body} {
                 uplevel 1 [list method public $name $arglist $body]
         }
 
         proc private {method name arglist body} {
                 uplevel 1 [list method private $name $arglist $body]
         }
 
         proc method_call_unknown {method_name args} {
                 if [catch {set m [method_spec $method_name]}] {
                         uplevel 1 [list ::unknown $method_name $args]
                 }
                 upvar 1 w w
                 uplevel 1 [linsert $args 0 $m $w]
         }
 
         proc method_spec {method_name {private 1}} {
                 variable public_method_prefix
                 variable private_method_prefix
                 set ns [namespace current]
                 if [llength [info procs ${ns}::${public_method_prefix}${method_name}]] {
                         return ${ns}::${public_method_prefix}${method_name}
                 } elseif {$private && [llength [info procs ${ns}::${private_method_prefix}${method_name}]]} {
                         return ${ns}::${private_method_prefix}${method_name}
                 } else {
                         return -code error "no such method: $method_name"
                 }
         }
 
         proc method_call_spec {method_name {args_l {}}} {
                 # always call public methods only
                 return [linsert $args_l 0 [method_spec $method_name 0] [uplevel 1 {set w}]]
         }
 
         namespace unknown {method_call_unknown}
         namespace export -clear tracker
 
         constructor {args} {
                 # set default options
                 set self(-rows) 16
                 set self(-cols) 6
                 set self(-width) 0
                 set self(-height) 0
                 set self(-hspacing) 8
                 set self(-vspacing) 4
                 set self(-hl1) -1
                 set self(-hl2) -1
                 set self(-background) "#000000"
                 set self(-backgroundHl1) "#222222"
                 set self(-backgroundHl2) "#444444"
                 set self(-foreground) "#0011fc"
                 set self(-foregroundHl1) "#0065db"
                 set self(-foregroundHl2) "#0092e9"
                 set self(-backgroundsel) "#18e02b"
                 set self(-backgroundselHl1) "#79e383"
                 set self(-backgroundselHl2) "#aaedb0"
                 set self(-foregroundsel) "#000000"
                 set self(-foregroundselHl1) "#404980"
                 set self(-foregroundselHl2) "#354cd9"
                 set self(-backgroundcur) "#ff0505"
                 set self(-backgroundcurHl1) "#e75b5b"
                 set self(-backgroundcurHl2) "#e88b8b"
                 set self(-foregroundcur) "white"
                 set self(-foregroundcurHl1) "black"
                 set self(-foregroundcurHl2) "black"
                 set self(-xscrollcommand) ""
                 set self(-yscrollcommand) ""
                 set self(-selectionmethod) "rect"
                 set self(-state) "normal"
 
                 set self(-col:default:-type) "number"
                 set self(-col:default:-inputmethod) "number"
                 set self(-col:default:-displaymethod) "number"
                 set self(-col:default:-width) 3
 
                 # parse options
                 variable valid_options
                 foreach {opt val} $args {
                         if {[lsearch -exact $valid_options $opt] == -1} {
                                 return -code error -errorinfo \
                                         "tracker($w): unknown option: $opt"
                         } else {
                                 set self($opt) $val
                         }
                 }
 
                 foreach k {-selectionnotify -setdatanotify -cursornotify} {
                         catch [concat configure $k {$self($k); unset self($k)}]
                 }
                 
                 # font stuff:
                 set self(font) \
                  [font create -family Courier -size 10 \
                  -weight bold -slant roman -underline false -overstrike false]
                 set self(font:-ascent) [font metrics $self(font) -ascent]
                 set self(font:-descent) [font metrics $self(font) -descent]
                 set self(font:-linespace) [font metrics $self(font) -linespace]
                 set self(font:-width) [font measure $self(font) m]
                 set self(-charwidth) $self(font:-width)
                 set self(-charheight) $self(font:-linespace)
                 
                 set c [uplevel #1 [list canvas $w \
                         -background $self(-background) \
                         -xscrollcommand $self(-xscrollcommand) \
                         -yscrollcommand $self(-yscrollcommand) \
                         -width $self(-width) -height $self(-height) \
                         -takefocus 1]]
 
                 rename $c ${w}_canvas
                 set self(canvas) ${w}_canvas
                 set self(window) $w
 
                 set self(cursor:col) 0
                 set self(cursor:row) 0
                 set self(internal:subcol) 0
                 set self(sel:start:col) 0
                 set self(sel:start:row) 0
                 set self(sel:stop:col) 0
                 set self(sel:stop:row) 0
                 set self(copybuf:w) 0
                 set self(copybuf:h) 0
                 
                 # setup callback proc
                 uplevel 1 [list proc $w args \
                         "if {\[llength \$args\] < 1} {
                                 return -code error \"wrong # args: should be \\\"$w option ?arg arg ...?\\\"\"
                         }
                         return \[uplevel 1 \[linsert \$args 0 [method_spec callback] $w\]\]"]
 
                 # this also calls initialization (@init)
                 configure -rows $self(-rows) -cols $self(-cols)
         }
 
         public method callback {command {args {}}} {
                 # dispatch command, if it exists
                 return [uplevel 1 [method_call_spec $command $args]]
         }
 
         public method xview {args} {
                 return [uplevel #1 [linsert $args 0 $self(canvas) xview]]
         }
 
         public method yview {args} {
                 return [uplevel #1 [linsert $args 0 $self(canvas) yview]]
         }
 
         private method get_row_color {colorkey nrow} {
                 set colors {background foreground backgroundsel foregroundsel backgroundcur foregroundcur}
                 if {[lsearch -exact $colors $colorkey] == -1} {
                         return -code error -errorinfo "tracker($w): wrong color-key: $colorkey"
                 }
                 set suffix {}
                 if {$self(-hl1)>1 && [expr {$nrow%$self(-hl1)}]==0} {set suffix Hl1}
                 if {$self(-hl2)>1 && [expr {$nrow%$self(-hl2)}]==0} {set suffix Hl2}
                 set colorkey2 $colorkey$suffix
                 if {$self(-$colorkey2) == {}} {
                         return $self(-$colorkey)
                 } else {
                         return $self(-$colorkey2)
                 }
         }
 
         static method item_rc_tag {row col {prefix xy}} {
                 # make an xy tag index
                 return "${prefix}_${row}_${col}"
         }
 
         private method init {} {
                 # initialize tracker, canvas items
                 $self(canvas) delete bg txt playhead
                 for {set x 0} {$x < $self(-cols)} {incr x} {
                         set xpp [expr {$x+1}]
                         for {set y 0} {$y < $self(-rows)} {incr y} {
                                 set ypp [expr {$y+1}]
                                 set tagid_bg [item_rc_tag $y $x]
                                 set tagid_txt [item_rc_tag $y $x xytxt]
                                 set rx [col_to_x $x]
                                 set ry [row_to_y $y]
                                 $self(canvas) create rectangle \
                                         $rx $ry [col_to_x $xpp] [row_to_y $ypp] \
                                         -fill [get_row_color background $y] -outline {} \
                                         -tags [list bg bg_row_$y $tagid_bg]
                                 $self(canvas) create text \
                                         [expr {$rx+1}] [expr {$ry+1}] \
                                         -fill {} -text {} -font $self(font) -anchor nw \
                                         -tags [list txt txt_row_$y $tagid_txt]
                                 bindcell $y $x $tagid_bg
                                 bindcell $y $x $tagid_txt
                                 updatecelltext $y $x
                                 updatecellcolor $y $x
                         }
                 }
                 $self(canvas) create rectangle -1 -1 -1 -1 -fill yellow -outline {} -tags playhead
                 $self(canvas) raise playhead
                 $self(canvas) raise txt
                 
                 set self(regionw) [col_to_x $self(-cols)]
                 set self(regionh) [row_to_y $self(-rows)]
                 set lh [expr {$self(-vspacing)+$self(-charheight)}]
                 set cw [expr {$self(-hspacing)+$self(-charwidth)}]
                 $self(canvas) configure \
                         -xscrollincrement $cw -yscrollincrement $lh \
                         -scrollregion [list 0 0 $self(regionw) $self(regionh)]
                 
                 set self(-width) [expr {$self(-cols)*$cw}]
                 set self(-height) [expr {$self(-rows)*$lh}]
                 
                 bind $self(window) <KeyPress> "[method_call_spec keypress] %K %s"
                 bind $self(window) <ButtonPress-1> "focus %W"
                 bind $self(window) <MouseWheel> "%W yview scroll \[expr {-(%D)}\] units"
                 bind $self(window) <Button-4> "%W yview scroll -1 units"
                 bind $self(window) <Button-5> "%W yview scroll 1 units"
                 setsel -1 -1 -1 -1
                 moveby 0 0
         }
 
         public method cget {key} {
                 variable valid_options
                 if {[lsearch -exact $valid_options $key] >= 0} {
                         return $self($key)
                 } else {
                         switch -- $key {
                                 -takefocus {return 1}
                                 -state {return {normal}}
                         }
                 }
                 return -code error "unknown option \"$key\""
         }
 
         public method configure {args} {
                 variable valid_options
                 set re_init 0
                 set update_colors 0
                 foreach {key value} $args {
                         switch -- $key {
                                 -background - -foreground - -backgroundHl1 - -backgroundHl2 - -foregroundHl1 -
                                 -foregroundHl2 - -backgroundsel - -backgroundselHl1 - -backgroundselHl2 -
                                 -foregroundsel - -foregroundselHl1 - -foregroundselHl2 - -backgroundcur -
                                 -backgroundcurHl1 - -backgroundcurHl2 - -foregroundcur - -foregroundcurHl1 -
                                 -foregroundcurHl2 - -hl1 - -hl2 {
                                         # stuff which only requires @updatecolors:
                                         if {$key == {-background}} {$self(canvas) configure -background $value}
                                         set self($key) $value
                                         set update_colors 1
                                 }
                                 -hspacing - -vspacing {
                                         set self($key) $value
                                         set self(regionw) [col_to_x $self(-cols)]
                                         set self(regionh) [row_to_y $self(-rows)]
                                         $self(canvas) configure -scrollregion \
                                                 [list 0 0 $self(regionw) $self(regionh)]
                                         set re_init 1
                                 }
                                 -rows - -cols {
                                         set self($key) $value
                                         set self(regionw) [col_to_x $self(-cols)]
                                         set self(regionh) [row_to_y $self(-rows)]
                                         $self(canvas) configure -scrollregion \
                                                 [list 0 0 $self(regionw) $self(regionh)]
                                         set re_init 1
                                 }
                                 -xscrollcommand - -yscrollcommand -
                                 -width - -height {
                                         set self($key) $value
                                         $self(canvas) configure $key $value
                                 }
                                 -selectionmethod {
                                         set valid_methods {rect text}
                                         if {[lsearch -exact $valid_methods $value] >= 0} {
                                                 set self($key) $value
                                         } else {
                                                 return -code error "$key <method> must be one of: $valid_methods"
                                         }
                                 }
                                 -selectionnotify {
                                         set self(-notify:selection) $value
                                 }
                                 -setdatanotify {
                                         set self(-notify:setdata) $value
                                 }
                                 -cursornotify {
                                         set self(-notify:cursor) $value
                                 }
                                 -state {
                                         set valid_states {normal readonly}
                                         if {[lsearch -exact $valid_states $value] >= 0} {
                                                 set self($key) $value
                                         } else {
                                                 return -code error "$key <method> must be one of: $valid_states"
                                         }
                                 }
                                 default {
                                         if {[lsearch -exact $valid_options $key] >= 0} {
                                                 return -code error "unsupported option for configure \"$key\""
                                         } else {
                                                 return -code error "unknown option \"$key\""
                                         }
                                 }
                         }
                 }
                 if {$re_init} {init}
                 if {$update_colors} {updatecolors}
         }
 
         public method columnconfigure {col args} {
                 if {$col != {default} && ($col < 0 || $col >= $self(-cols))} {
                         return -code error "column index $col out fo range"
                 }
                 if {[llength $args] == 1} {
                         set key [lindex $args 0]
                         if [info exists self(-col:$col:$key)] {
                                 return $self(-col:$col:$key)
                         }
                         if [info exists self(-col:default:$key)] {
                                 return $self(-col:default:$key)
                         }
                         return -code error "invalid column option \"$key\""
                 }
                 set re_init 0
                 set update_col 0
                 foreach {key value} $args {
                         switch -- $key {
                                 -type {
                                         # sets both input and display methods
                                         set valid_types {number numberhex note symbol}
                                         if {[lsearch -exact $valid_types $value] >= 0} {
                                                 set self(-col:$col:$key) $value
                                                 columnconfigure $col -inputmethod $value
                                                 columnconfigure $col -displaymethod $value
                                         } else {
                                                 return -code error "invalid value for $key: \"$value\". must be one of [join $valid_types {, }]"
                                         }
                                 }
                                 -width {
                                         set self(-col:$col:$key) [expr {int($value)}]
                                         set re_init 1
                                 }
                                 -inputmethod {
                                         # this triggers an error in case of missing method:
                                         method_spec input$value
                                         set self(-col:$col:$key) $value
                                 }
                                 -displaymethod {
                                         # this triggers an error in case of missing method:
                                         method_spec display$value
                                         set self(-col:$col:$key) $value
                                         set update_col 1
                                 }
                         }
                 }
                 if {$re_init} {init}
                 if {$update_col} {updatecolumn $col}
         }
 
         private method callnotify {type args} {
                 if ![info exists self(-notify:$type)] {return}
                 switch -- $type {
                         cursor - selection - setdata {
                                 uplevel 1 [concat $self(-notify:$type) $args]
                         }
                 }
         }
 
         public method moveby {drow dcol} {
                 # move cursor by relative amount dx dy, wrap if needed
                 set old_row $self(cursor:row)
                 set old_col $self(cursor:col)
                 incr self(cursor:row) $drow
                 incr self(cursor:col) $dcol
                 while {$self(cursor:row) < 0} {incr self(cursor:row) $self(-rows)}
                 while {$self(cursor:col) < 0} {incr self(cursor:col) $self(-cols)}
                 set self(cursor:row) [expr {$self(cursor:row)%$self(-rows)}]
                 set self(cursor:col) [expr {$self(cursor:col)%$self(-cols)}]
                 set self(internal:subcol) 0
                 # do the scrolling if cell is not visible:
                 while {[cursor_scroll_proc] > 0} {}
                 if {$old_col == $self(cursor:col) && $old_row == $self(cursor:row)} {
                         return
                 }
                 callnotify cursor $self(cursor:row) $self(cursor:col)
                 if ![deselect] {
                         updatecellcolor $old_row $old_col
                         updatecellcolor $self(cursor:row) $self(cursor:col)
                 }
         }
 
         public method moveabs {row col} {
                 # move cursor to absolute position
                 set old_row $self(cursor:row)
                 set old_col $self(cursor:col)
                 set self(cursor:row) $row
                 set self(cursor:col) $col
                 moveby 0 0
                 if {$self(cursor:row) != $old_row  || $self(cursor:col) != $old_col} {
                         updatecellcolor $old_row $old_col
                         updatecellcolor $self(cursor:row) $self(cursor:col)
                         callnotify cursor $self(cursor:row) $self(cursor:col)
                 }
                 if ![deselect] {
                         updatecellcolor $old_row $old_col
                         updatecellcolor $self(cursor:row) $self(cursor:col)
                 }
         }
 
         public method moveabs_mousewrap {row col} {
                 if {[cget -state] == {readonly}} {return}
                 return [moveabs $row $col]
         }
 
         public method moveplayhead {row} {
                 if {$row < 0} {
                         $self(canvas) coords playhead -1 -1 -1 -1
                 } else {
                         $self(canvas) coords playhead 0 [row_to_y $row] [col_to_x $self(-cols)] [row_to_y [incr row]]
                 }
         }
 
         private method cursor_scroll_proc {} {
                 set fx1 [col_to_x $self(cursor:col)]
                 set fx2 [col_to_x [expr {$self(cursor:col)+1}]]
                 foreach {rx1 rx2} [$self(canvas) xview] {break}
                 set rx1 [expr {$rx1*$self(regionw)}]
                 set rx2 [expr {$rx2*$self(regionw)}]
                 set fy1 [row_to_y $self(cursor:row)]
                 set fy2 [row_to_y [expr {$self(cursor:row)+1}]]
                 foreach {ry1 ry2} [$self(canvas) yview] {break}
                 set ry1 [expr {$ry1*$self(regionh)}]
                 set ry2 [expr {$ry2*$self(regionh)}]
                 if {$rx1 != $rx2} {
                         if {$fx1 < $rx1 || $fx2 < $rx1} {
                                 $self(canvas) xview scroll -1 units
                                 return 1
                         }
                         if {$fx1 > $rx2 || $fx2 > $rx2} {
                                 $self(canvas) xview scroll 1 units
                                 return 1
                         }
                 }
                 if {$ry1 != $ry2} {
                         if {$fy1 < $ry1 || $fy2 < $ry1} {
                                 $self(canvas) yview scroll -1 units
                                 return 1
                         }
                         if {$fy1 > $ry2 || $fy2 > $ry2} {
                                 $self(canvas) yview scroll 1 units
                                 return 1
                         }
                 }
                 return 0
         }
 
         private method updatecellcolor {row col} {
                 if {$col == $self(cursor:col) && $row == $self(cursor:row)} {
                         set fgcolkey foregroundcur
                         set bgcolkey backgroundcur
                 } elseif {[isselected $row $col]} {
                         set fgcolkey foregroundsel
                         set bgcolkey backgroundsel
                 } else {
                         set fgcolkey foreground
                         set bgcolkey background
                 }
                 $self(canvas) itemconfigure [item_rc_tag $row $col] -fill [get_row_color $bgcolkey $row]
                 $self(canvas) itemconfigure [item_rc_tag $row $col xytxt] -fill [get_row_color $fgcolkey $row]
         }
 
         private method updatecelltext {row col} {
                 # call the proper displayXXX method to render the cell's text:
                 set vis_data [uplevel 0 [list display[columnconfigure $col -displaymethod] $row $col]]
                 $self(canvas) itemconfigure [item_rc_tag $row $col xytxt] -text $vis_data
         }
 
         private method updatecolor {row_start col_start {row_end -1} {col_end -1}} {
                 if {$col_end == {end}} {
                         set col_end [expr {$self(-cols)-1}]
                 } elseif {$col_end == -1} {
                         set col_end $col_start
                 }
                 if {$row_end == {end}} {
                         set row_end [expr {$self(-rows)-1}]
                 } elseif {$row_end == -1} {
                         set row_end $row_start
                 }
                 for {set col $col_start} {$col <= $col_end} {incr col} {
                         for {set row $row_start} {$row <= $row_end} {incr row} {
                                 if {$col == $self(cursor:col) && $row == $self(cursor:row)} {
                                         set fgcolkey foregroundcur
                                         set bgcolkey backgroundcur
                                 } elseif {[isselected $row $col]} {
                                         set fgcolkey foregroundsel
                                         set bgcolkey backgroundsel
                                 } else {
                                         set fgcolkey foreground
                                         set bgcolkey background
                                 }
                                 $self(canvas) itemconfigure [item_rc_tag $row $col] -fill [get_row_color $bgcolkey $row]
                                 $self(canvas) itemconfigure [item_rc_tag $row $col xytxt] -fill [get_row_color $fgcolkey $row]
                         }
                 }
         }
 
         private method updatetext {row_start col_start {row_end -1} {col_end -1}} {
                 if {$col_end == {end}} {
                         set col_end [expr {$self(-cols)-1}]
                 } elseif {$col_end == -1} {
                         set col_end $col_start
                 }
                 if {$row_end == {end}} {
                         set row_end [expr {$self(-rows)-1}]
                 } elseif {$row_end == -1} {
                         set row_end $row_start
                 }
                 for {set col $col_start} {$col <= $col_end} {incr col} {
                         set display_method display[columnconfigure $col -displaymethod]
                         for {set row $row_start} {$row <= $row_end} {incr row} {
                                 $self(canvas) itemconfigure [item_rc_tag $row $col xytxt] \
                                         -text [uplevel 0 [list $display_method $row $col]]
                         }
                 }
         }
 
         private method updatecolumn {col} {
                 if {$col == {default}} {
                         set colstart 0
                         set colend end
                 } else {
                         set colstart $col
                         set colend $colstart
                 }
                 updatetext 0 $colstart end $colend
                 updatecolor 0 $colstart end $colend
         }
 
         private method displaynumber {row col} {
                 set d [getdata $row $col]
                 set width [columnconfigure $col -width]
                 if {$d == {}} {return [string repeat . $width]}
                 set d [expr {$d%10**$width}]
                 return [format %${width}.lld $d]
         }
 
         private method displaynumberhex {row col} {
                 set d [getdata $row $col]
                 set width [columnconfigure $col -width]
                 if {$d == {}} {return [string repeat . $width]}
                 set d [expr {$d%16**$width}]
                 return [format %.0${width}llx $d]
         }
 
         private method displaynote {row col} {
                 set d [getdata $row $col]
                 if {$d == {}} {return {...}}
                 set note [expr {$d%12}]
                 set oct [expr {($d-$note)/12}]
                 if {$oct >= 10} {set oct 9}
                 if {$oct < 0} {set oct 0}
                 return "[lindex {C- C# D- D# E- F- F# G- G# A- A# B-} $note]${oct}"
         }
 
         private method displaysymbol {row col} {
                 #TODO:
                 return [string repeat ? [columnconfigure $col -width]]
         }
 
         private method displaybyte {row col} {
                 set d [getdata $row $col]
                 if {$d == {}} {return {}}
                 return [format %c $d]
         }
 
         private method resetcolors {} {
                 $self(canvas) itemconfigure bg -fill {}
                 $self(canvas) itemconfigure txt -fill {}
                 for {set row 0} {$row < $self(-rows)} {incr row} {
                         $self(canvas) itemconfigure bg_row_$row -fill [get_row_color background $row]
                         $self(canvas) itemconfigure txt_row_$row -fill [get_row_color foreground $row]
                 }
                 updatecursor
         }
 
         public method setdata {row col d} {
                 # change data at specified position
                 # d is an integer
                 setdata_noupdate $row $col $d
                 updatecelltext $row $col
                 updatecellcolor $row $col
         }
 
         private method setdata_noupdate {row col d} {
                 if {$d == {}} {
                         catch {unset self(data:$col,$row)}
                 } else {
                         if [catch {set d [expr {entier($d)}]}] {set d 0}
                         set self(data:$col,$row) $d
                         callnotify setdata $row $col $self(data:$col,$row)
                 }
         }
 
         public method getdata {row col {d {}}} {
                 # get data at specified position
                 set r $d
                 catch {set r [expr {entier($self(data:$col,$row))}]}
                 return $r
         }
 
         public method resetdata {} {
                 array unset _ $w:data:*
         }
 
         public method setdata_from_array {arrayname {prefix {}}} {
                 set data [uplevel 1 [list array get $arrayname "${prefix}*"]]
                 set default_prefix "$w:data:"
                 if {$prefix == {}} {set prefix $default_prefix}
                 if {$prefix != $default_prefix} {
                         set data2 {}
                         foreach {k v} $data {
                                 lappend data2 [regsub "^$prefix" $k $default_prefix] $v
                         }
                         set data $data2 ; unset data2
                         # TODO: implement setdata notify
                 }
                 array unset _ "${default_prefix}*"
                 array set _ $data
                 updatetext 0 0 end end
         }
 
         public method getdata_to_array {arrayname {prefix {}}} {
                 set default_prefix "$w:data:"
                 set data [array get _ "${default_prefix}*"]
                 if {$prefix == {}} {set prefix $default_prefix}
                 if {$prefix != $default_prefix} {
                         set data2 {}
                         foreach {k v} $data {
                                 lappend data2 [regsub "^$default_prefix" $k $prefix] $v
                         }
                         set data $data2 ; unset data2
                 }
                 uplevel 1 [list array unset $arrayname "${prefix}*"]
                 uplevel 1 [list array set $arrayname $data]
         }
 
         public method copy {} {
                 # perform a copy of selected area
                 array unset _ $w:copybuf:*
                 set idx 0
                 iterateselection row col {
                         set self(copybuf:$idx) [getdata $row $col]
                         incr idx
                 }
                 set self(copybuf:start:col) $self(sel:start:col)
                 set self(copybuf:start:row) $self(sel:start:row)
                 set self(copybuf:stop:col) $self(sel:stop:col)
                 set self(copybuf:stop:row) $self(sel:stop:row)
         }
 
         public method delete {} {
                 # delete selected area
                 iterateselection row col {
                         setdata $row $col {}
                 }
         }
 
         public method cut {} {
                 # perform a cut (that is: copy & delete)
                 copy
                 delete
         }
 
         public method paste {} {
                 # paste data from copybuf to current position
 
                 # if selection is empty, use the copybuf dimensions
                 set sel_flag 0
                 if {![sel]} {
                         set sel_flag 1
                         setsel $self(cursor:row) $self(cursor:col) \
                                 [expr {$self(cursor:row)+$self(copybuf:stop:row)-$self(copybuf:start:row)}] \
                                 [expr {$self(cursor:col)+$self(copybuf:stop:col)-$self(copybuf:start:col)}]
                 }
 
                 set idx 0
                 iterateselection row col {
                         catch {setdata $row $col $self(copybuf:$idx)}
                         incr idx
                 }
 
                 if {$sel_flag} {
                         deselect
                 }
                 moveby 0 0
         }
 
         public method randomize {} {
                 # randomize area in selected area. usefull when testing & debugging
                 iterateselection row col {
                         set width [columnconfigure $col -width]
                         # TODO: use proper charset depending on column type
                         set charset [split {0123456789} {}]
                         set d {}
                         while {$width > 0} {
                                 incr width -1
                                 append d [lindex $charset [expr {int(rand()*[llength $charset])}]]
                         }
                         setdata $row $col $d
                 }
         }
 
         private method bindcell {row col tag} {
                 # re-bind events to specific cell
                 $self(canvas) bind $tag <ButtonPress-1> "[method_call_spec moveabs_mousewrap] $row $col"
                 $self(canvas) bind $tag <B1-Motion> "[method_call_spec extendsel] %x %y"
         }
 
         public method select_all {} {
                 setsel 0 0 [expr {$self(-rows)-1}] [expr {$self(-cols)-1}]
         }
 
         public method select_none {} {
                 setsel $self(cursor:row) $self(cursor:col) $self(cursor:row) $self(cursor:col)
         }
 
         public method select_row {} {
                 setsel $self(cursor:row) 0 $self(cursor:row) [expr {$self(-cols)-1}]
         }
 
         public method select_column {} {
                 setsel 0 $self(cursor:col) [expr {$self(-rows)-1}] $self(cursor:col)
         }
 
         public method keypress {ks st} {
                 if {[cget -state] == {readonly}} {return}
                 # handle global (canvas) keypress (from event)
                 set shift [expr {($st&0x0001)>0}]
                 set caps [expr {($st&0x0002)>0}]
                 set control [expr {($st&0x0004)>0}]
                 set alt [expr {($st&0x0008)>0}]
                 set super [expr {($st&0x0040)>0}]
 
                 if $shift { switch -- $ks {
                         Left {extendsel_rel 0 -1; return}
                         Right {extendsel_rel 0 1; return}
                         Up {extendsel_rel -1 0; return}
                         Down {extendsel_rel 1 0; return}
                         Home {moveabs $self(cursor:row) 0; return}
                         End {moveabs $self(cursor:row) -1; return}
                 } }
                 if $control { switch -- $ks {
                         c {copy; return}
                         x {cut; return}
                         v {paste; return}
                         r {randomize; return}
                         a {select_all; return}
                         u {select_none; return}
                         l {if $shift {select_row} else {select_column}}
                 } }
                 switch -- $ks {
                         Left {moveby 0 -1; return}
                         Right {moveby 0 1; return}
                         Up {moveby -1 0; return}
                         Down {moveby 1 0; return}
                         BackSpace {delete; return}
                         Home {moveabs 0 $self(cursor:col); return}
                         End {moveabs -1 $self(cursor:col); return}
                         Next {moveby [expr {max(4,$self(-hl2))}] 0; return}
                         Prior {moveby [expr {-max(4,$self(-hl2))}] 0; return}
                 }
 
                 uplevel 0 [list input[columnconfigure $self(cursor:col) -inputmethod] $self(cursor:row) $self(cursor:col) $ks $st]
         }
 
         private method inputnumber {row col keysym state} {
                 # keypresses are sent here from method keypress, based on <col> -inputmethod (-type)
                 if {![regexp -nocase -- {^[0123456789]$} $keysym]} {
                         return
                 }
                 set d [getdata $row $col 0]
                 set colch [columnconfigure $col -width]
                 if {$self(internal:subcol) == 0} {
                         set d $keysym
                 } else {
                         set d [expr {($d*10+$keysym)%(10**$colch)}]
                 }
                 setdata $row $col $d
                 incr self(internal:subcol)
                 if {$self(internal:subcol) >= $colch} {
                         moveby 1 0
                 }
         }
 
         private method inputnumberhex {row col keysym state} {
                 if {![regexp -nocase -- {^[0123456789abcdef]$} $keysym]} {
                         return
                 }
                 set keysym "0x$keysym"
                 set d [getdata $row $col 0]
                 set width [columnconfigure $col -width]
                 if {$self(internal:subcol) == 0} {
                         set d $keysym
                 } else {
                         set d [expr {($d*16+$keysym)%(16**$width)}]
                 }
                 setdata $row $col $d
                 incr self(internal:subcol)
                 if {$self(internal:subcol) >= $width} {
                         moveby 1 0
                 }
         }
 
         private method inputnote {row col keysym state} {
                 set map {z 0 s 1 x 2 d 3 c 4 v 5 g 6 b 7 h 8 n 9 j 10 m 11}
                 set d [getdata $row $col 0]
                 set note [expr {$d%12}]
                 set oct [expr {($d-$note)/12}]
                 switch -- $keysym {
                         z - s - x - d - c - v - g - b - h - n - j - m {
                                 set note [string map -nocase $map $keysym]
                         }
                         0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 {
                                 set oct $keysym
                         }
                         default {
                                 return
                         }
                 }
                 setdata $row $col [expr {$oct*12+$note}]
                 moveby 1 0
         }
 
         private method inputsymbol {row col keysym state} {
                 moveby 1 0
         }
 
         private method col_from_x {x} {
                 # get col# from x position
                 set curcol 0
                 set xlim 0
                 while {$curcol < $self(-cols)} {
                         set colwidth [expr {[columnconfigure $curcol -width]*$self(-charwidth)+$self(-hspacing)}]
                         incr xlim $colwidth
                         if {$x <= $xlim} {return $curcol}
                         incr curcol
                 }
                 return -1
         }
 
         private method row_from_y {y} {
                 # get row# from y position
                 set currow 0
                 set ylim 0
                 set lineheight [expr {$self(-charheight)+$self(-vspacing)}]
                 while {$currow < $self(-rows)} {
                         incr ylim $lineheight
                         if {$y <= $ylim} {return $currow}
                         incr currow
                 }
                 return -1
         }
 
         private method col_to_x {col} {
                 # get the x position of given col#
                 set x 0
                 set li 0
                 while {$col > 0} {
                         incr x [expr {($self(-charwidth)*\
                          [columnconfigure $li -width])+\
                          $self(-hspacing)}]
                         incr li
                         if {$li >= $self(-cols)} {
                                 return $x
                         }
                         incr col -1
                 }
                 return $x
         }
 
         private method row_to_y {row} {
                 # get the y position of given row#
                 return [expr {$row*($self(-charheight)+$self(-vspacing))}]
         }
 
         public method extendsel {_x _y} {
                 if {[cget -state] == {readonly}} {return}
                 # extend selection to specified (pixel) position (from event)
                 set oldsel [selstring]
                 clip_x_y _x _y
                 set self(sel:stop:col) [col_from_x $_x]
                 set self(sel:stop:row) [row_from_y $_y]
                 clipsel
                 set newsel [selstring]
                 if {$oldsel != $newsel} {uplevel 1 [notifysel]}
                 updatesel
         }
 
         private method extendsel_rel {drow dcol} {
                 set oldsel [selstring]
                 incr self(sel:stop:col) $dcol
                 incr self(sel:stop:row) $drow
                 clipsel
                 set newsel [selstring]
                 if {$oldsel != $newsel} {uplevel 1 [notifysel]}
                 updatesel
         }
 
         public method setsel {startrow startcol stoprow stopcol} {
                 set oldsel [selstring]
                 if {$stoprow == {end}} {set stoprow [expr {[cget -rows]-1}]}
                 if {$stopcol == {end}} {set stopcol [expr {[cget -cols]-1}]}
                 set self(sel:start:row) $startrow
                 set self(sel:start:col) $startcol
                 set self(sel:stop:row) $stoprow
                 set self(sel:stop:col) $stopcol
                 clipsel
                 set newsel [selstring]
                 if {$oldsel != $newsel} {uplevel 1 [notifysel]}
                 updatesel
         }
 
         private method notifysel {} {
                 callnotify selection \
                         $self(sel:start:row) $self(sel:start:col) \
                         $self(sel:stop:row) $self(sel:stop:col)
         }
 
         private method selstring {} {
                 return [join [list \
                         $self(sel:start:row) $self(sel:start:col) \
                         $self(sel:stop:row) $self(sel:stop:col)] ,]
         }
 
         private method clipsel {} {
                 # clip selection bounds based upon the current selection method
                 if {$self(sel:start:col) < 0} {set self(sel:start:col) 0}
                 if {$self(sel:start:row) < 0} {set self(sel:start:row) 0}
                 switch -- [cget -selectionmethod] {
                         rect {
                                 if {$self(sel:stop:col) < $self(sel:start:col)} {set self(sel:stop:col) $self(sel:start:col)}
                                 if {$self(sel:stop:row) < $self(sel:start:row)} {set self(sel:stop:row) $self(sel:start:row)}
                         }
                         text {
                                 if {$self(sel:stop:row) < $self(sel:start:row)} {set self(sel:stop:row) $self(sel:start:row)}
                                 if {$self(sel:stop:row) == $self(sel:start:row)} {
                                         if {$self(sel:stop:col) < $self(sel:start:col)} {set self(sel:stop:col) $self(sel:start:col)}
                                 }
                         }
                 }
         }
 
         private method updatesel {} {
                 $self(canvas) dtag sel
                 resetcolors
                 set old_row {}
                 iterateselection row col {
                         if {$row != $old_row} {
                                 set bgcol [get_row_color backgroundsel $row]
                                 set fgcol [get_row_color foregroundsel $row]
                                 set old_row $row
                         }
                         $self(canvas) addtag sel withtag [item_rc_tag $row $col]
                         $self(canvas) itemconfigure [item_rc_tag $row $col] -fill $bgcol
                         $self(canvas) itemconfigure [item_rc_tag $row $col xytxt] -fill $fgcol
                 }
                 updatecursor
         }
 
         private method iterateselection {row_var_name col_var_name body} {
                 upvar 1 $row_var_name row
                 upvar 1 $col_var_name col
                 switch -- [cget -selectionmethod] {
                         rect {
                                 for {set row $self(sel:start:row)} {$row <= $self(sel:stop:row)} {incr row} {
                                         for {set col $self(sel:start:col)} {$col <= $self(sel:stop:col)} {incr col} {
                                                 uplevel 1 $body
                                         }
                                 }
                         }
                         text {
                                 if {$self(sel:start:row) == $self(sel:stop:row)} {
                                         set row $self(sel:start:row)
                                         for {set col $self(sel:start:col)} {$col <= $self(sel:stop:col)} {incr col} {
                                                 uplevel 1 $body
                                         }
                                 } else {
                                         set row $self(sel:start:row)
                                         for {set col $self(sel:start:col)} {$col < $self(-cols)} {incr col} {
                                                 uplevel 1 $body
                                         }
 
                                         for {set row [expr {$self(sel:start:row)+1}]} {$row < $self(sel:stop:row)} {incr row} {
                                                 for {set col 0} {$col < $self(-cols)} {incr col} {
                                                         uplevel 1 $body
                                                 }
                                         }
 
                                         set row $self(sel:stop:row)
                                         for {set col 0} {$col <= $self(sel:stop:col)} {incr col} {
                                                 uplevel 1 $body
                                         }
                                 }
                         }
                 }
         }
 
         private method isselected {row col} {
                 switch -- [cget -selectionmethod] {
                         rect {
                                 if {$col < $self(sel:start:col) || $col > $self(sel:stop:col)} {return 0}
                                 if {$row < $self(sel:start:row) || $row > $self(sel:stop:row)} {return 0}
                                 return 1
                         }
                         text {
                                 if {$row == $self(sel:start:row) && $row == $self(sel:stop:row)} {
                                         return 0
                                         if {$col >= $self(sel:start:col) && $col <= $self(sel:stop:col)} {
                                                 return 1
                                         } else {
                                                 return 0
                                         }
                                 }
                                 if {$row == $self(sel:start:row) && $col >= $self(sel:start:col)} {return 1}
                                 if {$row == $self(sel:stop:row) && $col <= $self(sel:stop:col)} {return 1}
                                 if {$row > $self(sel:start:row) && $row < $self(sel:stop:row)} {return 1}
                                 return 0
                         }
                         default {
                                 # trigger an error, eventually
                                 configure -selectionmethod [cget -selectionmethod]
                         }
                 }
         }
 
         public method sel {} {
                 # return whether selection is set or not
                 if {$self(sel:start:col) != $self(sel:stop:col)} {return 1}
                 if {$self(sel:start:row) != $self(sel:stop:row)} {return 1}
                 return 0
         }
 
         public method deselect {} {
                 # clears current selection
                 set retval 0
                 if {$self(sel:start:col)<$self(sel:stop:col) || $self(sel:start:row)<$self(sel:stop:row)} {
                         set retval 1
                 }
                 set self(sel:start:col) $self(cursor:col)
                 set self(sel:start:row) $self(cursor:row)
                 set self(sel:stop:col) $self(sel:start:col)
                 set self(sel:stop:row) $self(sel:start:row)
                 uplevel 1 [notifysel]
                 if $retval resetcolors
                 return $retval
         }
 
         private method updatecolors {} {
                 resetcolors
                 updatesel
         }
 
         private method updatecursor {} {
                 $self(canvas) itemconfigure \
                  [item_rc_tag $self(cursor:row) $self(cursor:col)] \
                  -fill [get_row_color backgroundcur $self(cursor:row)]
                 $self(canvas) itemconfigure \
                  [item_rc_tag $self(cursor:row) $self(cursor:col) xytxt] \
                  -fill [get_row_color foregroundcur $self(cursor:row)]
         }
 
         public method focus {} {
                 # take focus (by a click, from event)
                 focus $w
         }
 
         private method clip_x_y {varw varh} {
                 set width [col_to_x $self(-cols)]
                 set height [row_to_y $self(-rows)]
                 upvar 1 $varw _varw
                 upvar 1 $varh _varh
                 if {$_varw < 0} {set _varw 0}
                 if {$_varw >= $width} {set _varw [expr {$width-1}]}
                 if {$_varh < 0} {set _varh 0}
                 if {$_varh >= $height} {set _varh [expr {$height-1}]}
         }
 }
 
 namespace import tracker::tracker