File: selectab.tcl

package info (click to toggle)
moomps 4.6-2
  • links: PTS
  • area: main
  • in suites: sarge
  • size: 2,444 kB
  • ctags: 2,307
  • sloc: tcl: 34,882; sh: 167; makefile: 91
file content (301 lines) | stat: -rw-r--r-- 12,222 bytes parent folder | download | duplicates (2)
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
# copyright (C) 1997-2005 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: selectab.tcl,v 1.15 2005/01/02 00:45:07 jfontain Exp $


# a scrollable table with embedded selector using external outlines so that clean and independant selection can be implemented
# important: the underlying tktable widget must not manipulated directly, otherwise options data may be desynchronized

class selectTable {

    proc selectTable {this parentPath args} composite {
        [new scroll table $parentPath\
            -height 200 -yscrollcommand "selectTable::refreshBorders $this; selectTable::refreshSelection $this"\
        ] $args
    } {
        set path $composite::($composite::($this,base),scrolled,path)
        # leave a 1 pixel wide empty border so that selection rectangles can extend a bit outside the table
        $path configure -font $font::(mediumNormal) -colstretchmode last -cursor {} -bordercursor {} -highlightthickness 1\
            -highlightcolor [$path cget -background] -sparsearray 0 -exportselection 0 -rows 0\
            -drawmode single                                                      ;# in single mode, no light shadow lines are drawn
        set ($this,rows) 0  ;# internally maintain number of rows as tktable, for example, return 1 when -rows was actually set to 0
        bindtags $path [list $path all]                                                              ;# remove all existing bindings
        set ($this,left) [frame $path.left -background {} -highlightthickness 1]                                 ;# dark shadow line
        set ($this,right) [frame $path.right -background {} -highlightthickness 1]                             ;# light shadow lines
        set ($this,bottom) [frame $path.bottom -background {} -highlightthickness 1]
        set ($this,limit) [frame $path.limit -background {} -highlightthickness 1]                                ;# last row bottom
        set ($this,tablePath) $path
        bind $path <Configure> "selectTable::refreshBorders $this"                                   ;# needed when table is resized
        # implement single mode selection:
        set ($this,selector) [new objectSelector -selectcommand "selectTable::setRowsState $this"]
        bind $path <ButtonPress-1> "selectTable::select $this \[%W index @0,%y row\]"
        composite::complete $this
    }

    proc ~selectTable {this} {
        variable ${this}frame

        foreach {row frame} [array get ${this}frame] {
            ::delete $frame
        }
        ::delete $($this,selector)
    }

    proc options {this} {
        return [list\
            [list -background $widget::option(label,background)]\
            [list -columns 1]\
            [list -focuscommand {} {}]\
            [list -followfocus 1]\
            [list -roworigin 0 0]\
            [list -selectcommand {} {}]\
            [list -state normal normal]\
            [list -titlerows 0 0]\
            [list -variable {} {}]\
        ]
    }

    proc set-background {this value} {
        $($this,tablePath) configure -background $value
        foreach {dark light} [3DBorders $($this,tablePath) $value] {}
        $($this,left) configure -highlightbackground $dark
        $($this,right) configure -highlightbackground $light
        $($this,bottom) configure -highlightbackground $light
        $($this,limit) configure -highlightbackground $light
    }

    proc set-columns {this value} {                                                                             ;# number of columns
        $($this,tablePath) configure -cols $value
        refreshBorders $this
        ::adjustTableColumns $($this,tablePath)
    }

    proc set-focuscommand {this value} {}

    proc set-followfocus {this value} {
        if {$composite::($this,complete)} {
            error {option -followfocus cannot be set dynamically}
        }
        if {$value} {
            bind $widget::($this,path) <FocusIn> "selectTable::focus $this 1"     ;# in case focus is explicitely set on main widget
            bind $($this,tablePath) <FocusIn> "selectTable::focus $this 1"
            bind $($this,tablePath) <FocusOut> "selectTable::focus $this 0"
        } else {
            bind $widget::($this,path) <FocusIn> {}
            bind $($this,tablePath) <FocusIn> {}
            bind $($this,tablePath) <FocusOut> {}
        }
    }

    proc set-selectcommand {this value} {}     ;# command must return a boolean which will determine if selection should be canceled

    proc set-state {this value} {
        switch $value {
            normal {}
            disabled {
                clear $this
            }
            default {
                error "bad state value \"$value\": must be normal or disabled"
            }
        }
    }

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

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

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

    proc setRowsState {this rows select} {
        variable ${this}frame

        set path $($this,tablePath)
        if {$select} {
            foreach row $rows {
                set ${this}frame($row) [new selectFrame $path $row]
            }
        } else {
            foreach row $rows {
                ::delete [set ${this}frame($row)]
                unset ${this}frame($row)
            }
        }
    }

    # public procedures below:

    # set or get number of rows, not counting title rows (used instead of -rows option as reliable synchronization with table actual
    # number of rows is too difficult to achieve)
    proc rows {this {number {}}} {
        if {[string length $number] == 0} {
            return $($this,rows)
        } else {
            $($this,tablePath) configure -rows [expr {$number + $composite::($this,-titlerows)}]
            # note: user should refresh borders and possibly adjust table columns when new rows are added or rows deleted
            set ($this,rows) $number
        }
    }

    proc select {this row} {
        if {$row < 0} {return 0}                                                                  ;# prevent selection on title line
        if {[string equal $composite::($this,-state) disabled]} {return 0}
        if {[info exists ($this,selected)] && ($row == $($this,selected))} {return 1}                          ;# selection is valid
        if {([string length $composite::($this,-selectcommand)] == 0) || [uplevel #0 $composite::($this,-selectcommand) $row]} {
            # selection may be canceled by user code
            set ($this,selected) $row
            selector::select $($this,selector) $row
            $($this,tablePath) see $row,[$($this,tablePath) index topleft col]                           ;# make sure row is visible
            return 1                                                                                           ;# selection is valid
        } else {
            return 0                                                                                         ;# selection is invalid
        }
    }

    proc refreshSelection {this first last} {
        variable ${this}frame

        set path $($this,tablePath)
        foreach {row frame} [array get ${this}frame] {
            ::delete $frame
            set ${this}frame($row) [new selectFrame $path $row]
        }
    }

    proc refreshBorders {this} {
        foreach {x y width height} [$($this,tablePath) bbox bottomright] {}
        if {![info exists x]} return                                                                                  ;# not visible
        incr y -1
        incr height $y
        place $($this,limit) -y $height -relwidth 1 -height 1
        place $($this,left) -width 1 -relheight 1 -height 1
        place $($this,right) -relx 1 -x -1 -y 1 -width 1 -relheight 1
        place $($this,bottom) -rely 1 -relwidth 1 -height 1
    }

    proc selected {this} {
        set list {}
        catch {lappend list $($this,selected)}
        return $list
    }

    proc clear {this} {                                                                                                 ;# selection
        selector::clear $($this,selector)
        catch {unset ($this,selected)}
    }

    proc focus {this in} {
        variable ${this}frame

        if {![info exists ($this,selected)]} return                                        ;# nothing to do if there is no selection
        if {$in} {
            selectFrame::refresh [set ${this}frame($($this,selected))] 0
        } else {
            selectFrame::refresh [set ${this}frame($($this,selected))] 1
        }
        if {[string length $composite::($this,-focuscommand)] > 0} {
            uplevel #0 $composite::($this,-focuscommand) $($this,selected) $in
        }
    }

    proc delete {this rows} {                                                    ;# any row deletion must be done here, not directly
        set path $($this,tablePath)
        foreach row $rows {$path delete rows $row}
        incr ($this,rows) -[llength $rows]
    }

    proc windows {this} {
        set path $($this,tablePath)
        set list {}
        foreach cell [$path window names] {
            lappend list [$path window cget $cell -window]
        }
        return $list
    }

    proc windowConfigure {this cell args} {
        return [eval $($this,tablePath) window configure $cell $args]
    }

    proc window {this cell} {
        return [$($this,tablePath) window cget $cell -window]
    }

    proc see {this cell} {
        $($this,tablePath) see $cell
    }

    proc spans {this args} {
        return [eval $($this,tablePath) spans $args]
    }

    proc tag {this option args} {
        return [eval $($this,tablePath) tag $option $args]
    }

    proc height {this args} {
        return [eval $($this,tablePath) height $args]
    }

    proc adjustTableColumns {this} {
        ::adjustTableColumns $($this,tablePath)
    }

}


class selectTable {

    class selectFrame {

        proc selectFrame {this table row} {                                           ;# use 4 border frames to make selector hollow
            foreach side {left top right bottom} {
                lappend ($this,frames) [new frame $table -background {} -highlightthickness 1 -highlightbackground black]
            }
            set ($this,table) $table
            set ($this,row) $row
            refresh $this 0
        }

        proc ~selectFrame {this} {
            eval delete $($this,frames)
        }

        proc refresh {this hide} {
            set table $($this,table)
            foreach {x y width height}\
                [$table bbox $($this,row),[$table index topleft col] $($this,row),[$table index bottomright col]] {}
            if {![info exists x]} return                                                                          ;# row not visible
            if {$hide} {
                foreach frame $($this,frames) {
                    place forget $widget::($frame,path)
                }
            } else {                                ;# draw a rectangle that horizontally extends beyond the table limits by 1 pixel
                foreach {left top right bottom} $($this,frames) {}
                incr y -1
                place $widget::($left,path) -x -1 -y $y -width 1 -height $height
                place $widget::($top,path) -x -1 -y $y -relwidth 1 -width 1 -height 1
                place $widget::($right,path) -relx 1 -x 0 -y $y -width 1 -height [expr {$height + 1}]
                place $widget::($bottom,path) -x -1 -y [expr {$y + $height}] -relwidth 1 -width 1 -height 1
            }
        }

    }

}