File: drag.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 (209 lines) | stat: -rw-r--r-- 11,012 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
# 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: drag.tcl,v 2.25 2005/01/02 00:45:07 jfontain Exp $

class dragSite {                                          ;# make a Tk widget a drag site with multiple formats support for its data

    set (out) circle                                                        ;# when dragging, mouse cursor shown outside a drop site
    set (in) dot                                                             ;# when dragging, mouse cursor shown inside a drop site

    if {![info exists (grabber)]} {
        # use a specific invisible frame so that when dragging is active, the frame is grabbed and its specific cursor is used,
        # thus preventing any interferences from source grab widget. use a specific cursor for user feedback
        set (grabber) $widget::([new frame . -background {} -width 0 -height 0],path)
        place $(grabber) -x -1 -y -1                                                                 ;# make sure frame is invisible
    }

    proc dragSite {this args} switched {$args} {
        switched::complete $this
    }

    proc ~dragSite {this} {
        variable ${this}provider
        variable draggable

        unset ${this}provider
        if {[string length $switched::($this,-path)] > 0} {                                      ;# if there was an actual drag site
            delete $($this,bindings)                                                                         ;# remove drag bindings
            unset draggable($switched::($this,-path))                                              ;# unregister path as a drag site
        }
    }

    proc options {this} {
        return [list\
            [list -data {} {}]\
            [list -grab 1 1]\
            [list -path {} {}]\
            [list -validcommand {} {}]\
        ]
    }

    proc set-data {this value} {       ;# a way to provide unformatted data as a default, while data is stored at the switched level
        proc unformatted {this format} {return $switched::($this,-data)}
        provide $this {} "dragSite::unformatted $this"
    }

    proc set-grab {this value} {}

    proc set-path {this value} {                                                                               ;# source widget path
        variable draggable

        if {$switched::($this,complete)} {
            error {option -path cannot be set dynamically}
        }
        if {![winfo exists $value]} {
            error "invalid path: \"$value\""
        }
        if {[info exists draggable($value)]} {
            error "path \"$value\" is already a drag site"                                    ;# multiple drag behavior is undefined
        }
        set draggable($value) {}                                                                     ;# register path as a drag site
        set ($this,bindings) [new bindings $value end]                                    ;# do not interfere with existing bindings
        bindings::set $($this,bindings) <ButtonPress-1> "dragSite::button1Pressed $this"
    }

    proc set-validcommand {this value} {}       ;# command is invoked with x and y hit coordinates for widget, must return a boolean

    # public procedure to make new formats available or unavailable for data (can be invoked up to and within the validate command)
    proc provide {this {format {}} {command ?}} {
        variable ${this}provider

        if {[string length $format] == 0} {                                 ;# return existing formats for which there is a provider
            return [array names ${this}provider]
        }
        switch $command {
            ? {
                return [set ${this}provider($format)]                                ;# return existing command for specified format
            }
            {} {
                catch {unset ${this}provider($format)}                               ;# remove existing command for specified format
            }
            default {
                set ${this}provider($format) $command                                            ;# set command for specified format
            }
        }
    }

    proc start {this xRoot yRoot} {
        variable ${this}provider

        if {![info exists (X)] || ![info exists (Y)]} return               ;# can be made to happen by clicking around like crazy...
        # do not actually start drag until mouse pointer is far enough, thus mimicking Windows behavior
        if {(abs($xRoot - $(X)) + abs($yRoot - $(Y))) < 5} return
        if {$switched::($this,-grab)} {
            grab $(grabber)                                                                       ;# drag cursor is used from now on
            update idletasks
        }
        set (highlight) [new highlighter]
        $(grabber) configure -cursor $(out)
        update idletasks                                                                   ;# note: drop code must not invoke update
        # place smaller regions first so that if several regions come from the same window, embedded regions can be selected:
        set (dropRegions) [lsort -command dragSite::smaller [dropSite::regions [array names ${this}provider]]]
        set (lastSite) 0
        # setup bindings after initializations above:
        if {$switched::($this,-grab)} {
            bind $(grabber) <ButtonRelease-1> "dragSite::drop $this %X %Y"
            bind $(grabber) <Button1-Motion> "dragSite::track $this %X %Y"
        } else {
            bindings::set $($this,bindings) <ButtonRelease-1> "dragSite::drop $this %X %Y"
            bindings::set $($this,bindings) <Button1-Motion> "dragSite::track $this %X %Y"
        }
    }

    proc dropSite {xRoot yRoot} {
        set path [winfo containing $xRoot $yRoot]
        foreach region $(dropRegions) {                                        ;# first try to find which drop site the cursor is in
            foreach {site container left top right bottom} $region {}
            if {($xRoot < $left) || ($xRoot > $right) || ($yRoot < $top) || ($yRoot > $bottom)} continue
            if {[contains $container $path]} {                                                              ;# in a drop site window
                return $region                                                                                               ;# done
            }
        }
        return [list 0 {} {} {} {}]                                                                          ;# not over a drop site
    }

    proc track {this xRoot yRoot} {
        foreach {site path left top right bottom} [dropSite $xRoot $yRoot] {}
        if {$site == $(lastSite)} {                                                           ;# in the same drop site or in no site
            return                                                                                       ;# no change, nothing to do
        } elseif {($site == 0) || [string equal $switched::($site,-path) $switched::($this,-path)]} {
            # no longer in a drop site (if drag site itself is also a drop site, it is not considered to be valid)
            highlighter::hide $(highlight)
            $(grabber) configure -cursor $(out)
            update idletasks
        } else {
            highlighter::show $(highlight)\
                [expr {$left - 1}] [expr {$top - 1}] [expr {$right - $left + 2}] [expr {$bottom - $top + 2}]
            $(grabber) configure -cursor $(in)
            update idletasks
        }
        set (lastSite) $site
    }

    proc drop {this xRoot yRoot} {
        variable ${this}provider
        variable data

        if {$switched::($this,-grab)} {
            bind $(grabber) <ButtonRelease-1> {}
            bind $(grabber) <Button1-Motion> {}
            grab release $(grabber)                                                                ;# cursor before grab is restored
            update idletasks
        } else {
            bindings::set $($this,bindings) <ButtonRelease-1> {}
            bindings::set $($this,bindings) <Button1-Motion> {}
        }
        delete $(highlight); unset (highlight)
        $(grabber) configure -cursor $(out)
        update idletasks
        unset (lastSite)
        foreach {site path left top right bottom} [dropSite $xRoot $yRoot] {}
        unset (dropRegions)
        if {($site == 0) || [string equal $switched::($site,-path) $switched::($this,-path)]} {
            return                                                               ;# no point in being able to drop data in drag site
        }
        foreach format [switched::cget $site -formats] {      ;# copy formatted data into data array so that drop site can access it
            if {[catch {set command [set ${this}provider($format)]}]} continue                           ;# skip unavailable formats
            set data($format) [uplevel #0 $command [list $format]]                   ;# invoke at global level as Tk buttons command
        }
        unset (X) (Y)
        dropSite::dropped $site                                                                    ;# tell drop site to process data
        catch {unset data}          ;# free memory after data has been used by drop site (data may not exist... (actually happened))
    }

    proc contains {container path} {
        while {[string length $path] > 0} {
            if {[string equal $path $container]} {
                return 1
            }
            set path [winfo parent $path]
        }
        return 0
    }

    # Public procedure under certain conditions (see below).
    # Invoked when the mouse button 1 is pressed in the drag area. That event signal the eventual start of the drag procedure.
    # Normally invoked automatically by the drag implementation, but must also be invoked by the client code when the
    # <ButtonPress-1> binding does not work on the -path option widget.
    proc button1Pressed {this} {
        set path $switched::($this,-path)
        bindings::set $($this,bindings) <Button1-Motion> {}                                                         ;# reset binding
        set command $switched::($this,-validcommand)
        set (X) [winfo pointerx .]
        set (Y) [winfo pointery .]
        if {\
            ([string length $command] > 0) &&\
            ![uplevel #0 $command [expr {$(X) - [winfo rootx $path]}] [expr {$(Y) - [winfo rooty $path]}]]\
        } return
        bindings::set $($this,bindings) <Button1-Motion> "dragSite::start $this %X %Y"
    }

    proc smaller {region1 region2} {              ;# compare 2 regions (returns -1 if first smaller than second, 0 if equal, else 1)
        foreach {site container left top right bottom} $region1 {}
        set area [expr {($right - $left) * ($bottom - $top)}]
        foreach {site container left top right bottom} $region2 {}
        return [expr {$area - (($right - $left) * ($bottom - $top))}]
    }

}