# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: datatab.tcl,v 2.74 2006/10/22 16:50:25 jfontain Exp $


class dataTable {                       ;# module data view in the form of a table, that can be sorted using a column as a reference

    set (extreme,integer) $global::32BitIntegerMinimum                                      ;# used for sorting data with void cells
    set (extreme,real) -1.7976931348623158e308
    set (list) {}
    set (scrollbarBorderWidth) [expr {$widget::option(scrollbar,borderwidth) == 0? 0: 1}]
    set (scrollbarWidth) [expr {2 * $widget::option(scrollbar,width) / 3}]

    proc dataTable {this parentPath args} composite {
        [new scroll table $parentPath\
            -scrollbarwidth $(scrollbarWidth) -scrollbarelementborderwidth $(scrollbarBorderWidth)\
            -width $global::viewerWidth -height $global::viewerHeight\
        ] $args
    } {
        set path $composite::($composite::($this,base),scrolled,path)
        # use arrow cursor instead of default insertion cursor, meaningless since cell editing is disabled:
        $path configure -font $font::(mediumNormal) -state disabled -colstretchmode last -variable dataTable::${this}data\
            -resizeborders col -cursor {} -highlightthickness 0 -takefocus 0 -ipadx 2
        # use transparent background not to interfere with cell color, set foreground for windows:
        $path tag configure sel -background {} -foreground black -borderwidth {2 1 2 1}
        # remove all class bindings for we do not use any and they would cause interferences
        bindtags $path [list $path [winfo toplevel $path] all]
        set ($this,tablePath) $path
        lappend (list) $this
        composite::complete $this

        bind $path <ButtonPress-1> "dataTable::buttonPress $this %x %y"
        bind $path <ButtonRelease-1> "dataTable::buttonRelease $this %x %y 0"
        bind $path <Control-ButtonRelease-1> "dataTable::buttonRelease $this %x %y 1"
        bind $path <Shift-ButtonRelease-1> "dataTable::buttonRelease $this %x %y 1"
        if {$composite::($this,-resizablecolumns)} {
            $path configure -bordercursor sb_h_double_arrow
            # allow border resizing with first button. does not interfere with drag bindings since command does nothing unless mouse
            # click occured in a column border, which cannot be the case when dragging (see drag validation procedure in this class)
            bind $path <Button1-Motion> "if {\[info exists ::dataTable::($this,borderHit)\]} {%W border dragto %x %y}"
        } else {
            $path configure -bordercursor {}
        }
        set ($this,swap) 0                                                                                             ;# by default
        if {$composite::($this,-view) ne ""} {
            catch {set ($this,swap) [set $composite::($this,-view)(swap)]}                                ;# swap option is optional
        }
        $path tag configure lastcell -borderwidth 1               ;# so that the right side and bottom are delimited by thin borders
        $path tag configure lastcolumn -borderwidth {1 1 1 0}                ;# so that the right side is delimited by a thin border
        $path tag configure lastrow -background {} -borderwidth {1 0 1 1}        ;# so that the bottom is delimited by a thin border
        if {$($this,swap)} {
            $path configure -cols 1 -titlecols 1 -colorigin -1
        } else {
            $path configure -rows 1 -titlerows 1 -roworigin -1
        }
        setupDataView $this                                                ;# wait till -data and possibly -view options are defined
        set ($this,dataRows) {}                                                   ;# data columns are initialized in data view setup
        set ($this,tipper) [new ::tk::table::rightColumnTipper $path]
    }

    proc ~dataTable {this} {
        variable ${this}data

        if {$composite::($this,-data) ne ""} {
            dataTrace::unregister $this $composite::($this,-data)
        }
        unset -nocomplain ${this}data                                                                    ;# possibly free table data
        delete $($this,tipper)
        if {[info exists ($this,arrow)]} {delete $($this,arrow)}
        if {[info exists ($this,tips)]} {eval delete $($this,tips)}
        if {[info exists ($this,drag)]} {delete $($this,drag)}
        if {[info exists ($this,selector)]} {                                       ;# selector may not exist if dragging disallowed
            delete $($this,selector)
        }
        ldelete (list) $this
    }

    proc options {this} {
        return [list\
            [list -background {} {}]\
            [list -columnwidths {} {}]\
            [list -data {} {}]\
            [list -draggable 0 0]\
            [list -leftcolumn 0 0]\
            [list -resizablecolumns 0 0]\
            [list -toprow 0 0]\
            [list -view {} {}]\
        ]
    }

    proc set-background {this value} {
        $($this,tablePath) configure -background $value
    }

    # list of column widths (as specified in the tkTable manual), applied to existing columns, can be empty.
    proc set-columnwidths {this value} {
        if {$composite::($this,complete)} {
            updateColumnWidths $this                                                                     ;# needs to know if swapped
        }
    }

    proc set-data {this value} {                                                ;# value must be a fully qualified module data array
        if {$composite::($this,complete)} {
            error {option -data cannot be set dynamically}
        }
        if {![array exists $value]} {
            error "\"$value\" argument is not an existing array"
        }
    }

    proc set-draggable {this value} {
        if {$composite::($this,complete)} {
            error {option -draggable cannot be set dynamically}
        }
        if {!$value} return                                                                                           ;# no dragging
        set path $($this,tablePath)
        set ($this,drag) [new dragSite -path $path -validcommand "dataTable::validateDrag $this"]
        dragSite::provide $($this,drag) DATACELLS "dataTable::dragData $this"

        set ($this,selector) [new tableSelector -selectcommand "dataTable::setCellsState $this"]
        bind $path <Control-ButtonPress-1> "dataTable::toggleSelection $this %x %y"
        bind $path <Shift-ButtonPress-1> "dataTable::extendSelection $this %x %y"
    }

    # override default view defined in -data option with indices (visibleColumns supported but obsolete) and sort members
    proc set-view {this value} {                                            ;# value must be a fully qualified module sub data array
        if {$composite::($this,complete)} {
            error {option -view cannot be set dynamically}
        }
        if {![array exists $value]} {
            error "\"$value\" argument is not an existing array"
        }
    }

    proc set-leftcolumn {this value} {                                                          ;# the leftmost visible column index
        if {$composite::($this,complete)} {
            error {option -leftcolumn cannot be set dynamically}
        }
        set ($this,leftColumn) $value          ;# actual setting is done right after first update so that table is in a stable state
    }

    proc set-resizablecolumns {this value} {
        if {$composite::($this,complete)} {
            error {option -resizablecolumns cannot be set dynamically}
        }
    }

    proc set-toprow {this value} {                                                                  ;# the topmost visible row index
        if {$composite::($this,complete)} {
            error {option -toprow cannot be set dynamically}
        }
        set ($this,topRow) $value              ;# actual setting is done right after first update so that table is in a stable state
    }

    proc selectable {this x y} {                                                                     ;# return empty cell if invalid
        if {$($this,swap)} {set option cols} else {set option rows}
        if {[$($this,tablePath) cget -$option] <= 1} {return {}}                    ;# number of data lines (account for title line)
        set cell [$($this,tablePath) index @$x,$y]
        scan $cell %d,%d row column
        if {($row >= 0) && ($column >= 0)} {
            return $cell
        } else {
            return {}                                                                                       ;# title line is invalid
        }
    }

    proc buttonPress {this x y} {
        if {$composite::($this,-resizablecolumns)} {
            foreach {row column} [$($this,tablePath) border mark $x $y] {}
            # do not allow resizing with rightmost column edge:
            if {[info exists column] && ($column ne "") && ($column < ([$($this,tablePath) cget -cols] - 1))} {
                set ($this,borderHit) {}
                return                                                             ;# do nothing else is starting to resize a column
            }
        }
        if {$composite::($this,-draggable) && ([set selected [selectable $this $x $y]] ne "")} {
            foreach cell [selector::selected $($this,selector)] {
                if {$cell eq $selected} return                               ;# in an already selected cell, do not change selection
            }
            selector::select $($this,selector) $selected
        }
    }

    proc buttonRelease {this x y extended} {                        ;# extended means that there is an extended selection in process
        if {[info exists ($this,borderHit)]} {                             ;# if column was resized, do not interfere with selection
            unset ($this,borderHit)
        } elseif {$composite::($this,-draggable) && ([set selected [selectable $this $x $y]] ne "") && !$extended} {
            set cells [selector::selected $($this,selector)]
            foreach cell $cells {
                if {$cell eq $selected} {                                                             ;# in an already selected cell
                    selector::select $($this,selector) $selected                                       ;# set selection to sole cell
                    return
                }
            }
        }
    }

    # sort table rows or columns using the data column corresponding to the display line that the user selected as a reference
    proc lineSort {this dataColumn} {
        if {$dataColumn == $($this,dataSortColumn)} {                                                          ;# sort the same line
            if {$($this,sortOrder) eq "increasing"} {                                                          ;# but toggle sorting
                set ($this,sortOrder) decreasing
            } else {
                set ($this,sortOrder) increasing
            }
        } else {                                                                  ;# sort for the first time or for a different line
            set ($this,dataSortColumn) $dataColumn
            set ($this,sortOrder) increasing
        }
        if {[info exists ($this,selector)]} {
            selector::clear $($this,selector)   ;# deselect all cells since reordering rows or columns renders selection meaningless
        }
        update $this                                                                                     ;# update table immediately
    }

    proc update {this} {                                                                         ;# update display using module data
        upvar #0 $composite::($this,-data) data                                              ;# data must be visible at global level

        set path $($this,tablePath)
        set lists {}
        set rows {}
        if {[catch {set dataSortColumn $($this,dataSortColumn)}]} {                              ;# simply sort rows by their number
            foreach name [array names data *,0] {
                scan $name %lu dataRow
                lappend rows $dataRow
            }
            set rows [sortRows $rows]
        } else {
            set type $data($dataSortColumn,type)
            switch $type {
                integer - real {
                    # numeric type: handle undefined values
                    set extreme $(extreme,$type)
                    foreach name [array names data *,$dataSortColumn] {         ;# source data is always in a row/column arrangement
                        scan $name %lu dataRow
                        # handle void values (which use the ? character. value must always exist)
                        if {$data($dataRow,$dataSortColumn) eq "?"} {
                            lappend lists [list $dataRow $extreme]                      ;# assume largest negative value for sorting
                        } else {
                            lappend lists [list $dataRow $data($dataRow,$dataSortColumn)]
                        }
                    }
                    foreach pair [lsort -$($this,sortOrder) -$type -index 1 $lists] {     ;# sort data rows according to sort column
                        lappend rows [lindex $pair 0]
                    }
                }
                default {
                    foreach name [array names data *,$dataSortColumn] {
                        scan $name %lu dataRow
                        lappend lists [list $dataRow $data($dataRow,$dataSortColumn)]
                    }
                    if {$type eq "clock"} {
                        set list [lsort -$($this,sortOrder) -index 1 -command compareClocks $lists]
                    } else {
                        set list [lsort -$($this,sortOrder) -$type -index 1 $lists]
                    }
                    foreach pair $list {                                                  ;# sort data rows according to sort column
                        lappend rows [lindex $pair 0]
                    }
                }
            }
        }
        set ($this,dataRows) $rows                                                                  ;# store rows for cells coloring
        # possibly reset last cell aspect in case number of data rows changes:
        catch {$path tag cell {} [$path tag cell lastcell]}
        if {$($this,swap)} {
            # possibly reset last column aspect in case number of data rows changes:
            catch {$path tag col {} [$path tag col lastcolumn]}
            foreach {old new} [swap $this $rows] {}                                       ;# lists of old and new cells are returned
        } else {
            catch {$path tag row {} [$path tag row lastrow]}   ;# possibly reset last row aspect in case number of data rows changes
            foreach {old new} [copy $this $rows] {}                                            ;# may change number of rows in table
        }
        if {[info exists ($this,selector)]} {
            set changed 0
            if {[llength $new] > 0} {
                selector::add $($this,selector) $new                                             ;# make selector aware of new cells
                set changed 1
            }
            if {[llength $old] > 0} {
                selector::remove $($this,selector) $old                                      ;# make selector aware of removed cells
                set changed 1
            }
            if {$changed} {
                selector::clear $($this,selector)   ;# deselect all cells since new or deleted columns renders selection meaningless
            }
        }
        if {[info exists ($this,leftColumn)]} {                      ;# now that table knows all its cells, view can be properly set
            $path xview $($this,leftColumn)
            unset ($this,leftColumn)
        }
        if {[info exists ($this,topRow)]} {
            $path yview $($this,topRow)
            unset ($this,topRow)
        }
        if {!$composite::($this,-resizablecolumns)} {
            adjustTableColumns $path
        }
        drawTableLimits $this
        updateCellsColor $this                       ;# after table limits are drawn otherwise bottom right cell may loose its color
    }

    proc copy {this dataRows} {
        variable ${this}data
        upvar #0 $composite::($this,-data) data                                              ;# data must be visible at global level

        set path $($this,tablePath)
        set row 0
        set rows {}
        foreach dataRow $dataRows {
            if {![info exists ${this}data($row,dataRow)]} {                                                       ;# gather new rows
                lappend rows $row
            }
            set ${this}data($row,dataRow) $dataRow                                        ;# keep track of table / data rows mapping
            set column 0
            set lines 1
            foreach dataColumn $($this,dataColumns) {
                set count [linesCount [set ${this}data($row,$column) $data($dataRow,$dataColumn)]]               ;# also count lines
                if {$count > $lines} {set lines $count}                                      ;# keep track of number of lines in row
                incr column
            }
            $path height $row $lines                                                                    ;# height is number of lines
            incr row
        }
        $path configure -rows [expr {$row + 1}]                                         ;# fit to data (take into account title row)
        set newCells {}
        set columns [llength $($this,dataColumns)]
        if {[llength $rows] > 0} {                                                                    ;# one or more rows were added
            foreach new $rows {
                for {set column 0} {$column < $columns} {incr column} {
                    lappend newCells $new,$column
                }
            }
        }
        set oldCells {}
        set rows {}
        while {[info exists ${this}data($row,dataRow)]} {                                                     ;# gather removed rows
            lappend rows $row
            incr row
        }
        if {[llength $rows] > 0} {                                                                  ;# one or more rows were removed
            foreach old $rows {
                unset ${this}data($old,dataRow)
                for {set column 0} {$column < $columns} {incr column} {
                    lappend oldCells $old,$column
                    unset ${this}data($old,$column)
                }
            }
        }
        return [list $oldCells $newCells]
    }

    proc swap {this dataRows} {                       ;# swap axes (row, column) when copying from source data to display table data
        variable ${this}data
        upvar #0 $composite::($this,-data) data                                              ;# data must be visible at global level

        set path $($this,tablePath)
        set numberOfRows [llength $($this,dataColumns)]
        for {set row 0} {$row < $numberOfRows} {incr row} {                  ;# initialize number of lines per row from title column
            set lines($row) $($this,height,$row)
        }
        set column 0
        set columns {}
        foreach dataRow $dataRows {
            if {![info exists ${this}data($column,dataRow)]} {                                                 ;# gather new columns
                lappend columns $column
            }
            set ${this}data($column,dataRow) $dataRow                             ;# keep track of table columns / data rows mapping
            set row 0
            foreach dataColumn $($this,dataColumns) {
                set count [linesCount [set ${this}data($row,$column) $data($dataRow,$dataColumn)]]               ;# also count lines
                if {$count > $lines($row)} {
                    set lines($row) $count                                                   ;# keep track of number of lines in row
                }
                incr row
            }
            incr column
        }
        for {set row 0} {$row < $numberOfRows} {incr row} {         ;# set row heights now that all data has been scanned and loaded
            $path height $row $lines($row)                                                              ;# height is number of lines
        }
        $path configure -cols [expr {$column + 1}]                                   ;# fit to data (take into account title column)
        set newCells {}
        if {[llength $columns] > 0} {                                                              ;# one or more columns were added
            foreach new $columns {
                for {set row 0} {$row < $numberOfRows} {incr row} {
                    lappend newCells $row,$new
                }
            }
        }
        set oldCells {}
        set columns {}
        while {[info exists ${this}data($column,dataRow)]} {                                               ;# gather removed columns
            lappend columns $column
            incr column
        }
        if {[llength $columns] > 0} {                                                            ;# one or more columns were removed
            foreach old $columns {
                unset ${this}data($old,dataRow)
                for {set row 0} {$row < $numberOfRows} {incr row} {
                    lappend oldCells $row,$old
                    unset ${this}data($row,$old)
                }
            }
        }
        return [list $oldCells $newCells]
    }

    proc dragData {this format} {
        variable ${this}data

        set data $composite::($this,-data)
        set coordinates {}
        foreach cell [selector::selected $($this,selector)] {
            scan $cell %d,%d row column                                                     ;# data cell format is array(row,column)
            lappend coordinates $row $column
        }
        set list {}
        if {$($this,swap)} {
            foreach {row column} $coordinates {
                lappend list ${data}([set ${this}data($column,dataRow)],[set ${this}data($row,dataColumn)])
            }
        } else {
            foreach {row column} $coordinates {
                lappend list ${data}([set ${this}data($row,dataRow)],[set ${this}data($column,dataColumn)])
            }
        }
        return $list
    }

    proc validateDrag {this x y} {
        if {[info exists ($this,borderHit)]} {
            return 0                                                                              ;# resizing a column: prevent drag
        }
        if {\
            (!$($this,swap) && ([$($this,tablePath) cget -rows] <= 1)) || ($($this,swap) && ([$($this,tablePath) cget -cols] <= 1))\
        } {
            return 1                                                   ;# allow dragging of empty table (with possibly 1 title line)
        }
        # allow dragging only from a selected cell
        return [expr {[lsearch -exact [selector::selected $($this,selector)] [$($this,tablePath) index @$x,$y]] >= 0}]
    }

    proc setCellsState {this cells select} {
        set path $($this,tablePath)
        if {$select} {
            foreach cell $cells {
                $path selection set $cell
            }
        } else {
            foreach cell $cells {
                $path selection clear $cell
            }
        }
    }

    proc toggleSelection {this x y} {
        set cell [$($this,tablePath) index @$x,$y]
        scan $cell %d,%d row column
        if {($row < 0) || ($column < 0)} return                                                   ;# prevent selection on title line
        selector::toggle $($this,selector) $cell
    }

    proc extendSelection {this x y} {
        set cell [$($this,tablePath) index @$x,$y]
        scan $cell %d,%d row column
        if {($row < 0) || ($column < 0)} return                                                   ;# prevent selection on title line
        selector::extend $($this,selector) $cell
    }

    proc updateSortingArrow {this line} {
        set path $widget::($($this,arrow),path)
        set label $($this,tablePath).$line.label                   ;# copy title label bindings for contextual help and mouse action
        foreach event {<Enter> <Leave> <ButtonRelease-1>} {
            bind $path $event [bind $label $event]
        }
        array set direction {0,0 up 0,1 down 1,0 left 1,1 right}
        widget::configure $($this,arrow) -direction $direction($($this,swap),[string equal $($this,sortOrder) increasing])
        if {$::tcl_platform(platform) eq "windows"} {                     ;### windows Tk bug: avoid stepping on cell bottom border:
            widget::configure $($this,arrow) -height [winfo reqheight $label]
        }
        grid $path -in $($this,tablePath).$line -row 0 -column 1 ;# show arrow in sorted line title frame on the right side of label
    }

    proc createTitles {this} {
        upvar #0 $composite::($this,-data) data                                              ;# data must be visible at global level

        set path $($this,tablePath)
        set sortable [info exists ($this,dataSortColumn)]
        set sortTipText [mc {click to toggle sorting order}]
        set arrowWidth 12
        set line 0
        if {$($this,swap)} {
            $path configure -rows [llength $($this,dataColumns)]                                               ;# set number of rows
        } else {
            $path configure -cols [llength $($this,dataColumns)]                                            ;# set number of columns
        }
        set lines 1
        foreach dataColumn $($this,dataColumns) {
            # create table title labels in separate windows
            set frame [frame $path.$line -cursor left_ptr]          ;# use a frame as a container for label and sorting arrow if any
            # force default arrow cursor as column resizing cursor sticks when moving across columns
            set label [label $path.$line.label -font $font::(mediumBold) -text $data($dataColumn,label) -cursor left_ptr -pady 0]
            grid columnconfigure $frame 0 -weight 1                                   ;# expand to make sure label gets mouse events
            if {$sortable} {
                grid columnconfigure $frame 1 -minsize $arrowWidth                                           ;# leave room for arrow
            }
            grid $label -row 0 -column 0 -sticky nsew
            set count [linesCount $data($dataColumn,label)]
            if {$($this,swap)} {
                set cell $line,-1
                set ($this,height,$line) $count
                if {$count > 1} {$path height $line $count}
            } else {
                set cell -1,$line
                if {$count > $lines} {$path height -1 [set lines $count]}                    ;# keep track of number of lines in row
            }
            $path window configure $cell -window $frame -padx 2 -pady 1 -sticky nsew
            if {$sortable} {
                bind $frame <ButtonRelease-1> "dataTable::lineSort $this $dataColumn; dataTable::updateSortingArrow $this $line"
                bind $label <ButtonRelease-1> "dataTable::lineSort $this $dataColumn; dataTable::updateSortingArrow $this $line"
                bind $frame <Enter> "lifoLabel::push $global::messenger [list $sortTipText]"
                bind $frame <Leave> "lifoLabel::pop $global::messenger"
            }
            lappend ($this,tips) [new widgetTip -path $label -text $data($dataColumn,message)]
            incr line
        }
        updateColumnWidths $this
        if {$sortable} {
            set arrow [new arrowButton $path -state disabled -borderwidth 0 -highlightthickness 0 -width $arrowWidth]
            widget::configure $arrow -disabledforeground [widget::cget $arrow -foreground]               ;# make arrow fully visible
            set path $widget::($arrow,path)
            # force default arrow cursor as column resizing cursor sticks when moving across columns
            $path configure -cursor left_ptr
            bind $path <ButtonRelease-1> "dataTable::lineSort $this $dataColumn; dataTable::updateSortingArrow $this $line"
            lappend ($this,tips) [new widgetTip -path $widget::($arrow,path) -text $sortTipText]
            set ($this,arrow) $arrow
        }
    }

    proc setupDataView {this} {
        variable ${this}data

        if {$composite::($this,-data) eq ""} return                                                      ;# nothing to do if no data
        if {$composite::($this,-view) ne ""} {
            upvar #0 $composite::($this,-view) data                                          ;# data must be visible at global level
        } else {
            upvar #0 $composite::($this,-data) data                                          ;# data must be visible at global level
        }
        catch {set columns $data(visibleColumns)}          ;# visibleColumns keyword obsolete but supported for backward compatility
        catch {set columns $data(indices)}
        if {![info exists columns]} {                                    ;# if not user defined visibility, make all columns visible
            set columns {}                                                                                    ;# gather line indices
            foreach name [array names data *,label] {
                if {[scan $name %u column] > 0} {
                    lappend columns $column
                }
            }
        }
        set ($this,dataColumns) [lsort -integer $columns]                                                ;# then sort and store them
        if {[info exists data(sort)]} {                                                                     ;# if data can be sorted
            set ($this,dataSortColumn) [lindex $data(sort) 0]
            if {[lsearch -exact $columns $($this,dataSortColumn)] < 0} {
                error "sort column $($this,dataSortColumn) is not visible"
            }
            set ($this,sortOrder) [lindex $data(sort) 1]
        }
        set line 0
        foreach dataColumn $($this,dataColumns) {                                                ;# store table / data lines mapping
            set ${this}data($line,dataColumn) $dataColumn
            if {[info exists ($this,dataSortColumn)] && ($dataColumn == $($this,dataSortColumn))} {
                set sortLineIndex $line
            }
            incr line
        }
        catch {composite::configure $this -swap $data(swap)}
        createTitles $this
        drawTableLimits $this
        if {[info exists sortLineIndex]} {
            updateSortingArrow $this $sortLineIndex
        }
        setupLinesAnchoring $this
        dataTrace::register $this $composite::($this,-data) "dataTable::update $this"
    }

    proc updateColumnWidths {this} {                                                        ;# best apply widths to existing columns
        if {!$composite::($this,-resizablecolumns)} return                                  ;# inapplicable when columns auto resize
        set path $($this,tablePath)
        if {$($this,swap)} {
            set column -1                                                                        ;# title column can also be resized
        } else {
            set column 0
        }
        foreach width $composite::($this,-columnwidths) {                                                       ;# list may be empty
            $path width $column $width
            if {[incr column] >= [$path cget -cols]} return                                                       ;# no more columns
        }
    }

    proc initializationConfiguration {this} {
        set path $($this,tablePath)
        if {$($this,swap)} {
            set column -1                                                                        ;# title column can also be resized
        } else {
            set column 0
        }
        for {} {$column < [$path cget -cols]} {incr column} {
            lappend widths [$path width $column]
        }
        if {$composite::($this,-resizablecolumns)} {
            set list [list -columnwidths $widths]                                    ;# note: always return widths in the same order
        } else {
            set list {}
        }
        set row [expr {round([lindex [$path yview] 0] * [$path cget -rows])}]
        if {$row != 0} {
            lappend list -toprow $row
        }
        set column [expr {round([lindex [$path xview] 0] * [$path cget -cols])}]
        if {$column != 0} {
            lappend list -leftcolumn $column
        }
        return $list
    }

    proc setupLinesAnchoring {this} {                                                           ;# handle data anchor line attribute
        upvar #0 $composite::($this,-data) data

        set line -1
        set path $($this,tablePath)
        foreach dataColumn $($this,dataColumns) {
            incr line
            if {[catch {set anchor $data($dataColumn,anchor)}]} continue        ;# no anchor for this line, default (center) is used
            switch $anchor {
                center {
                    continue                                                    ;# nothing to do as center is line anchoring default
                }
                left - right {}
                default {
                    error "bad anchor value \"$anchor\": must be center, left or right"
                }
            }
            if {![$path tag exists $anchor]} {        ;# create anchor tag as needed and use valid anchor values (see above) as name
                array set convert {left w right e}
                $path tag configure $anchor -anchor $convert($anchor)
            }
            if {$($this,swap)} {
                if {$line == [$path index end row]} {                                                       ;# last row special case
                    $path tag configure lastrow -anchor $convert($anchor)                                 ;# merge with existing tag
                } else {
                    $path tag row $anchor $line
                }
            } else {
                if {$line == [$path index end col]} {                                                    ;# last column special case
                    $path tag configure lastcolumn -anchor $convert($anchor)                              ;# merge with existing tag
                } else {
                    $path tag col $anchor $line
                }
            }
        }
    }

    proc cellThresholdCondition {array row column} {
        foreach table $(list) {
            if {$composite::($table,-data) ne $array} continue
            if {![info exists color]} {
                set color [viewer::cellThresholdColor $array $row $column]                  ;# most important threshold color (slow)
            }
            if {[info exists dataTable::${table}color($row,$column)]} {
                if {$color eq [set dataTable::${table}color($row,$column)]} continue                                    ;# no change
            } else {
                if {$color eq ""} continue                                                                              ;# no change
            }
            setCellColor $table $row $column $color
        }
    }

    proc setCellColor {this dataRow dataColumn color} {
        variable ${this}color

        set row [lsearch -exact $($this,dataRows) $dataRow]
        if {$row < 0} return                                                                     ;# cell not displayed in this table
        set column [lsearch -exact $($this,dataColumns) $dataColumn]
        if {$column < 0} return                                                                  ;# cell not displayed in this table
        if {$($this,swap)} {
            set index $row
            set row $column
            set column $index
        }
        set path $($this,tablePath)
        set corner [expr {($row == [$path index end row]) && ($column == [$path index end col])}]
        if {$color eq ""} {
            $path tag cell {} $row,$column                                                                       ;# reset cell color
            if {$corner} {                                                                       ;# restore bottom right cell aspect
                $path tag cell lastcell $row,$column
            }
            unset -nocomplain ${this}color($dataRow,$dataColumn)
        } else {
            $path tag configure color$color -background $color
            if {$corner} {                                                                                       ;# draw bottom line
                $path tag lower color$color sel                                    ;# raise but do not cover selection sunken effect
                $path tag configure color$color -borderwidth 1
            }
            $path tag cell color$color $row,$column
            set ${this}color($dataRow,$dataColumn) $color
        }
    }

    proc updateCellsColor {this} {
        variable ${this}color

        set path $($this,tablePath)
        foreach tag [$path tag names color*] {                                                        ;# clear all cell colors first
            $path tag delete $tag
        }
        foreach {cell color} [array get ${this}color] {
            scan $cell %lu,%u dataRow dataColumn
            setCellColor $this $dataRow $dataColumn $color
        }
    }

    proc drawTableLimits {this} {
        set path $($this,tablePath)
        set row [$path index end row]
        set column [$path index end col]
        if {$($this,swap)} {
            $path tag row lastrow [$path index end row]                          ;# so that the bottom is delimited by a thin border
            if {$column < 0} {                                                                                    ;# no data columns
                $path configure -borderwidth {1 1 1 0}              ;# so that title column right side is delimited by a thin border
                # so that the bottom cell side and bottom are delimited by thin borders:
                $path window configure $row,-1 -borderwidth 1
            } else {
                $path configure -borderwidth {1 0 1 0}                    ;# only draw a thin dark line on top and left of each cell
                $path window configure $row,-1 -borderwidth {1 0 1 1}            ;# so that the bottom is delimited by a thin border
                $path tag col lastcolumn $column                             ;# so that the right side is delimited by a thin border
                $path tag cell lastcell $row,$column
            }
        } else {
            $path tag col lastcolumn [$path index end col]                   ;# so that the right side is delimited by a thin border
            if {$row < 0} {                                                                                          ;# no data rows
                $path configure -borderwidth {1 0 1 1}                     ;# so that title row bottom is delimited by a thin border
                # so that the rightmost cell side and bottom are delimited by thin borders:
                $path window configure -1,$column -borderwidth 1
            } else {
                $path configure -borderwidth {1 0 1 0}                    ;# only draw a thin dark line on top and left of each cell
                $path window configure -1,$column -borderwidth {1 1 1 0}     ;# so that the right side is delimited by a thin border
                $path tag row lastrow $row                                       ;# so that the bottom is delimited by a thin border
                $path tag cell lastcell $row,$column
            }
        }
    }

    proc compareUnsigned64Bits {value1 value2} {                                           ;# comparison command for 64 bits support
        # split comparisons into same sign and different signs (above 0x7FFFFFFFFFFFFFFF) cases
        return [expr {$value1 == $value2? 0: (($value1 ^ $value2) > 0? ($value1 < $value2? -1: 1): ($value1 < 0? 1: -1))}]
    }
    proc sortRows {list} {
        return [lsort -command compareUnsigned64Bits $list]
    }

    proc monitoring {cell} {
        set tables {}
        foreach table $(list) {
            if {[monitored $table $cell]} {
                lappend tables $table
            }
        }
        return $tables
    }

    proc monitored {this cell} {
        viewer::parse $cell array row column type                                                            ;# determine cell array
        return [expr {\
            ($composite::($this,-data) eq $array) &&\
            ([lsearch -exact $($this,dataRows) $row] >= 0) && ([lsearch -exact $($this,dataColumns) $column] >= 0)\
        }]
    }

    proc updateTitleLabels {this} {
        upvar #0 $composite::($this,-data) data                                              ;# data must be visible at global level

        set path $($this,tablePath)
        set line 0
        foreach dataColumn $($this,dataColumns) {
            $path.$line.label configure -text $data($dataColumn,label)
            incr line
        }
        updateColumnWidths $this
    }

    proc dataRow {this row} {                                                    ;# public function: data row from display table row
        variable ${this}data

        set return {}
        catch {set return [set ${this}data($row,dataRow)]}
        return $return
    }

}
