File: datatab.tcl

package info (click to toggle)
moodss 5.0-2
  • links: PTS
  • area: main
  • in suites: slink
  • size: 1,104 kB
  • ctags: 430
  • sloc: tcl: 12,266; sh: 59; makefile: 43
file content (358 lines) | stat: -rw-r--r-- 18,883 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# copyright (C) 1997-98 Jean-Luc Fontaine (mailto:jfontain@mygale.org)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

set rcsId {$Id: datatab.tcl,v 1.45 1998/10/10 20:56:43 jfontain Exp $}

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

    set dataTable::(list) {}

    set dataTable::(scrollbarBorderWidth) [expr {$widget::(default,ScrollbarBorderWidth)==0?0:1}]
    set dataTable::(scrollbarWidth) [expr {2*$widget::(default,ScrollbarWidth)/3}]

    proc dataTable {this parentPath args} composite {
        [new scroll table $parentPath\
            -scrollbarwidth $dataTable::(scrollbarWidth) -scrollbarelementborderwidth $dataTable::(scrollbarBorderWidth)\
        ]
        $args
    } {
        set path $composite::($composite::($this,base),scrolled,path)
        # only allow interactive colun resizing
        # use arrow cursor instead of default insertion cursor, meaningless since cell editing is disabled
        $path configure -font $font::(mediumNormal) -rows 2 -state disabled -titlerows 1 -roworigin -1 -colstretchmode unset\
            -variable dataTable::${this}data -resizeborders col -cursor {} -bordercursor sb_h_double_arrow -highlightthickness 0\
            -takefocus 0
        $path tag configure select -background white

        bindtags $path "$path . all"           ;# remove all class bindings for we do not use any and they would cause interferences

        # 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 <ButtonPress-1> "dataTable::buttonPress $this %x %y"
        bind $path <Button1-Motion> {%W border dragto %x %y}

        set dataTable::($this,sortOrder) increasing
        set dataTable::($this,tablePath) $path

        lappend dataTable::(list) $this

        composite::complete $this
        setupDataView $this                                              ;# wait till -data and eventually -view options are defined
    }

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

        catch {unset ${this}data}                                                                      ;# eventually free table data
        catch {eval delete $dataTable::($this,arrow) $dataTable::($this,tips)}
        catch {delete $dataTable::($this,drag)}
        catch {delete $dataTable::($this,selector)}
        ldelete dataTable::(list) $this
    }

    proc options {this} {
        return [list\
            [list -columns columns Columns 0 0]\
            [list -data data Data {} {}]\
            [list -draggable draggable Draggable 0 0]\
            [list -titlefont titleFont TitleFont $font::(mediumBold) $font::(mediumBold)]\
            [list -view view View {} {}]\
        ]
    }

    proc set-columns {this value} {
        $dataTable::($this,tablePath) configure -cols $value
    }

    proc set-titlefont {this value} {
        if {[string length $composite::($this,-data)]==0} return                                         ;# nothing to do if no data

        set path $dataTable::($this,tablePath)
        for {set column 0} {$column<[llength $dataTable::($this,dataColumns)]} {incr column} {
            $path.$column.label configure -font $value
        }
    }

    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 $dataTable::($this,tablePath)
        set dataTable::($this,drag) [new dragSite -path $path -validcommand "dataTable::validateDrag $this"]
        dragSite::provide $dataTable::($this,drag) DATACELLS "dataTable::dragData $this"

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

    # override default view defined in -data option with visibleColumns 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 buttonRelease {this x y} {
        if {!$dataTable::($this,columnBorderHit)} {                        ;# if column was resized, do not interfere with selection
            set number [expr {[$dataTable::($this,tablePath) cget -rows]-1}]                        ;# calculate number of data rows
            if {$number==0} return
            scan [$dataTable::($this,tablePath) index @$x,$y] %d,%d row column
            if {$row<0} return                                                                           ;# title row, nothing to do
            catch {selector::select $dataTable::($this,selector) $row,$column}      ;# selector may not exist if dragging disallowed
        }
        unset dataTable::($this,columnBorderHit)
    }

    proc columnSort {this column} {                        ;# sort table rows using the column that the user selected as a reference
        if {$column==$dataTable::($this,sortColumn)} {                                                       ;# sort the same column
            if {[string compare $dataTable::($this,sortOrder) increasing]==0} {                                ;# but toggle sorting
                set dataTable::($this,sortOrder) decreasing
            } else {
                set dataTable::($this,sortOrder) increasing
            }
        } else {                                                                ;# sort for the first time or for a different column
            set dataTable::($this,sortColumn) $column
            set dataTable::($this,sortOrder) increasing
        }
        # deselect all cells since reordering rows renders selection meaningless
        catch {selector::clear $dataTable::($this,selector)}                        ;# selector may not exist if dragging disallowed
        update $this                                                                                     ;# update table immediately
    }

    proc update {this args} {                                   ;# update display using module data. ignore eventual trace arguments
        variable ${this}data
        upvar #0 $composite::($this,-data) data                                              ;# data must be visible at global level

        set path $dataTable::($this,tablePath)

        set cursor [$path cget -cursor]                                                                               ;# save cursor
        $path configure -cursor watch                                                                  ;# show user that we are busy
        ::update idletasks

        set sortColumn $dataTable::($this,sortColumn)
        set lists {}
        if {[regexp {^integer|real$} $data($sortColumn,type)]} {                                                     ;# numeric type
            foreach name [array names data *,$sortColumn] {
                scan $name %u dataRow
                if {[catch {expr {double($data($dataRow,$sortColumn))}}]} {                   ;# handle empty values or ? characters
                    lappend lists [list $dataRow 0]                                                          ;# assume 0 for sorting
                } else {
                    lappend lists [list $dataRow $data($dataRow,$sortColumn)]
                }
            }
        } else {
            foreach name [array names data *,$sortColumn] {
                scan $name %u dataRow
                lappend lists [list $dataRow $data($dataRow,$sortColumn)]
            }
        }
        # sort data rows according to sort column (column numbering is identical for table data and source data)
        set lists [lsort -$dataTable::($this,sortOrder) -$data($sortColumn,type) -index 1 $lists]

        catch {set selector $dataTable::($this,selector)}                           ;# selector may not exist if dragging disallowed
        set changed 0                                                        ;# keep track of whether any rows were added or removed
        set row 0
        set rows {}
        foreach pair $lists {
            set dataRow [lindex $pair 0]
            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
            foreach dataColumn $dataTable::($this,dataColumns) {
                set ${this}data($row,$column) $data($dataRow,$dataColumn)
                incr column
            }
            incr row
        }
        $path configure -rows [expr {$row+1}]                                           ;# fit to data (take into account title row)

        set columns [llength $dataTable::($this,dataColumns)]

        if {[llength $rows]>0} {                                                                      ;# one or more rows were added
            set changed 1
            set cells {}
            foreach new $rows {
                for {set column 0} {$column<$columns} {incr column} {
                    lappend cells $new,$column
                }
            }
            catch {selector::add $selector $cells}                                       ;# make selector aware of new cells at once
        }

        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
            set changed 1
            set cells {}
            foreach old $rows {
                unset ${this}data($old,dataRow)
                for {set column 0} {$column<$columns} {incr column} {
                    lappend cells $old,$column
                    unset ${this}data($old,$column)
                }
            }
            catch {selector::remove $selector $cells}                                ;# make selector aware of removed cells at once
        }

        if {$changed} {
           catch {selector::clear $selector}           ;# deselect all cells since new or deleted rows renders selection meaningless
        }

        $path configure -cursor $cursor                                                                            ;# restore cursor
        ::update idletasks
    }

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

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

    proc validateDrag {this x y} {
        if {$dataTable::($this,columnBorderHit)} {
            return 0                                                                              ;# resizing a column: prevent drag
        }
        # allow dragging if only from a selected cell
        return [expr\
            {[lsearch -exact [selector::selected $dataTable::($this,selector)] [$dataTable::($this,tablePath) index @$x,$y]]>=0}\
        ]
    }

    proc setCellsState {this cells select} {
        if {$select} {
            eval $dataTable::($this,tablePath) tag cell select $cells
        } else {
            eval $dataTable::($this,tablePath) tag cell {{}} $cells
        }
    }

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

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

    proc updateSortingArrow {this column} {
        set path $widget::($dataTable::($this,arrow),path)

        set label $dataTable::($this,tablePath).$column.label      ;# copy title label bindings for contextual help and mouse action
        foreach event {<Enter> <Leave> <ButtonRelease-1>} {
            bind $path $event [bind $label $event]
        }
        if {[string compare $dataTable::($this,sortOrder) increasing]==0} {
            widget::configure $dataTable::($this,arrow) -direction down
        } else {
            widget::configure $dataTable::($this,arrow) -direction up
        }
        # place arrow in sorted column title frame on the right side of label
        place $path -in $dataTable::($this,tablePath).$column -anchor e -relx 1 -rely 0.5 -relheight 1
    }

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

        set path $dataTable::($this,tablePath)
        set font $composite::($this,-titlefont)
        set column 0
        foreach dataColumn $dataTable::($this,dataColumns) {
            # create table title labels in separate windows
            set frame [frame $path.$column]                       ;# use a frame as a container for label and eventual sorting arrow
            # force default arrow cursor as column resizing cursor sticks when moving across columns
            set label [label $path.$column.label -font $font -text $data($dataColumn,label) -cursor top_left_arrow]
            place $label -relwidth 1 -relheight 1         ;# use placer so that sorting arrow can eventually be displayed over label
            $path window configure -1,$column -window $frame -padx 2 -pady 2 -sticky nsew
            bind $label <ButtonRelease-1> "dataTable::columnSort $this $dataColumn; dataTable::updateSortingArrow $this $column"
            # setup context sensitive help on titles using help strings from module data
            bind $label <Enter> "lifoLabel::push $::messenger [list $data($dataColumn,message)]"
            bind $label <Leave> "lifoLabel::pop $::messenger"
            lappend dataTable::($this,tips) [new widgetTip -path $label -text {click to toggle sort}]
            incr column
        }
        $path configure -cols $column                                                              ;# fit table to number of columns
        if {![info exists dataTable::($this,arrow)]} {                                    ;# use 1 sorting arrow indicator per table
            set arrow [new arrowButton $path -state disabled -borderwidth 0 -highlightthickness 0 -width 12]
            widget::configure $arrow -disabledforeground [widget::cget $arrow -foreground]               ;# make arrow fully visible
            # force default arrow cursor as column resizing cursor sticks when moving across columns
            $widget::($arrow,path) configure -cursor top_left_arrow
            lappend dataTable::($this,tips) [new widgetTip -path $widget::($arrow,path) -text {click to toggle sort}]
            set dataTable::($this,arrow) $arrow
        }
    }

    proc buttonPress {this x y} {
        foreach {row column} [$dataTable::($this,tablePath) border mark $x $y] {}
        set dataTable::($this,columnBorderHit) [expr {[info exists column]&&([string length $column]>0)}]
    }

    proc setupDataView {this} {
        if {[string length $composite::($this,-data)]==0} return                                         ;# nothing to do if no data

        variable ${this}data
        if {[string length $composite::($this,-view)]>0} {
            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
        }
        if {[catch {set columns $data(visibleColumns)}]} {               ;# if not user defined visibility, make all columns visible
            set columns {}                                                                                  ;# gather column indices
            foreach name [array names data *,label] {
                if {[scan $name %u column]>0} {
                    lappend columns $column
                }
            }
        }
        set dataTable::($this,dataColumns) [lsort -integer $columns]                                     ;# then sort and store them
        set dataTable::($this,sortColumn) [lindex $data(sort) 0]
        if {[lsearch -exact $columns $dataTable::($this,sortColumn)]<0} {
            error "sort column $dataTable::($this,sortColumn) is not visible"
        }
        set dataTable::($this,sortOrder) [lindex $data(sort) 1]
        set column 0
        foreach dataColumn $dataTable::($this,dataColumns) {                                   ;# store table / data columns mapping
            set ${this}data($column,dataColumn) $dataColumn
            if {$dataColumn==$dataTable::($this,sortColumn)} {
                set sortColumnIndex $column
            }
            incr column
        }
        createTitles $this
        updateSortingArrow $this $sortColumnIndex
        trace variable $composite::($this,-data)(updates) w "dataTable::update $this"                   ;# track module data updates
    }
}