Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Bind to buttonrelease but not to double click

165 views
Skip to first unread message

Alexandru

unread,
Feb 28, 2022, 12:12:41 PM2/28/22
to
Is there a simple way (idealy only using bind tricks) to implement the behavior:

If user makes a double click, do nothing.
If user releases the first mouse button, do something.

I tried this:

pack [label .l1 -text "Test"]
bind .l1 <ButtonRelease-1> {puts works}
bind .l1 <Double-1> {break}

But the "works" message apears twice if a do a double click.
I'm trying to find something that does not need "after cancel".

Thanks
Alexandru

Andreas Leitgeb

unread,
Feb 28, 2022, 12:54:22 PM2/28/22
to
I doubt that there is a solution without "after ..."

Not sure why you want to avoid "after cancel"... you can
pass a specific after-id to "after cancel", if you don't
want to cancel by naming the full scriptlet.

Another approach could also be to set&check a global (or namespaced)
variable in the after-handler and in other handlers.

e.g.:
ButtonRelease: { set var armed; after 500 { if {$var eq "armed"} { do it } }
DoubleClick: { set var unarmed; ... }

Alexandru

unread,
Feb 28, 2022, 12:57:51 PM2/28/22
to
Andreas Leitgeb schrieb am Montag, 28. Februar 2022 um 18:54:22 UTC+1:
> I doubt that there is a solution without "after ..."
>
> Not sure why you want to avoid "after cancel"... you can
> pass a specific after-id to "after cancel", if you don't
> want to cancel by naming the full scriptlet.

Because I like the pure "bind" solution more.
For example bind .l1 <Button-1> {puts works} works! But then, I actually don't want to react to first button press, I want the release event. Not sure, why <Button-1> works as wanted, but not <ButtonDouble-1>

Andreas Leitgeb

unread,
Feb 28, 2022, 2:13:25 PM2/28/22
to
Alexandru <alexandr...@meshparts.de> wrote:
> Because I like the pure "bind" solution more.

The "pure bind" approach kind of fails on the point, where it is
supposed to peek into future, to determine whether another click
will follow. -- That, or I completely misunderstood the original
question.

There are a few more or less dirty tricks to achieve all those
behaviors that do not require peek-into-future directly without
after handlers. If yours is of that kind, please try again,
describing the situations you want to capture, and those you
want to not react for.

But if you need some event fired only if the user is done with
his action, then you will need some kind of timer and see later
if the user continued harrassing the mouse, or gave it a break. ;-)

Alexandru

unread,
Feb 28, 2022, 2:26:13 PM2/28/22
to
Okay, I get it.
Like I wrote I wanted this:

If user makes a double click, do nothing.
If user releases the first mouse button, do something.

But I undertand the "looking into future" thing.
The idea behind is that some users don't know if a single click is expected and thy start doing double clicks instead. So without anything else from the developer side, the user will trigger the action twice.

This stands for normal bindings on buttons and lables. Because of this simple fact, the developer always needs to reinvent the wheel and make sure double clicks are ignored or at least make sure that just one of the two click is triggering a function.

Andreas Leitgeb

unread,
Feb 28, 2022, 2:35:01 PM2/28/22
to
Wait, I've got a new idea of what you might have meant...
(sorry for implying you wanted tcl to read future)

My new interpretation is, that you want only the second
ButtonRelease to fire...

That would be:
bind .l1 <Button-1> { set var "1st" }
bind .l1 <Double-1> { set var "2nd" }
bind .l1 <ButtonRelease-1> {
if {$var eq "2nd"} { do it ... }
}

Note, that if the user just keeps hammering the Button, then you'll
still get multiple Doubles and thus multiple "do it ..."-calls.

To prevent that, you can either add a binding for <Triple-1> and
set the var to something else, e.g. "3rd".

I once had a case, where I wanted to handle Double Clicks, and then
reset the engine, so next click would become a plain click again.
For this I created a proc "reset_click":

proc reset_click {} {
event generate . <5>; event generate . <ButtonRelease-5>
}

and called it from the Double-handler.
That way, user pressing 4 times in a row would get through as
two separate doubleclicks.

Ralf Fassel

unread,
Feb 28, 2022, 2:41:56 PM2/28/22
to
* Alexandru <alexandr...@meshparts.de>
| The idea behind is that some users don't know if a single click is
| expected and thy start doing double clicks instead. So without
| anything else from the developer side, the user will trigger the
| action twice.

When executing the function you could record a timestamp. Then when the
function is invoked again, check the timestamp and immediately return if
it is "too young".

Pseudo:
proc func {} {
set now [clock milliseconds]
if {[info exists ::last_invocation]
&& $now - $::last_invocation < 500} {
# second click of double click
return
}
set ::last_invocation $now
# do whatever needs to be done
}



R'

Andreas Leitgeb

unread,
Feb 28, 2022, 2:48:29 PM2/28/22
to
I saw your last followup only after posting this one ...

change my suggestion slightly to:

> bind .l1 <Button-1> { set var "1st" }
> bind .l1 <Double-1> { set var "2nd" }
> bind .l1 <ButtonRelease-1> {
> if {$var eq "1st"} { do it ... }
> }

That would fire on first ButtonRelease, but not on shortly following
second, third, forth, ...

Rich

unread,
Feb 28, 2022, 2:49:23 PM2/28/22
to
Alexandru <alexandr...@meshparts.de> wrote:
> Andreas Leitgeb schrieb am Montag, 28. Februar 2022 um 20:13:25 UTC+1:
>> Alexandru <alexandr...@meshparts.de> wrote:
>> > Because I like the pure "bind" solution more.
>> The "pure bind" approach kind of fails on the point, where it is
>> supposed to peek into future, to determine whether another click
>> will follow. -- That, or I completely misunderstood the original
>> question.
>>
>> There are a few more or less dirty tricks to achieve all those
>> behaviors that do not require peek-into-future directly without
>> after handlers. If yours is of that kind, please try again,
>> describing the situations you want to capture, and those you
>> want to not react for.
>>
>> But if you need some event fired only if the user is done with
>> his action, then you will need some kind of timer and see later
>> if the user continued harrassing the mouse, or gave it a break. ;-)
>
> Okay, I get it.
> Like I wrote I wanted this:
>
> If user makes a double click, do nothing.
> If user releases the first mouse button, do something.

Except your "requirements" are ambigious. A "double click" is also two
"mouse button release" events as well. So it is impossible, using pure
bindings to events without timers, to ignore a double click while doing
something on a button release.

At the mouse hardware level there are only two button events: "button
down" and "button up" (release). "Double Clicks" (or any other plural
click types) are synthesized from these basic events by measuring
timing between them.

> But I undertand the "looking into future" thing.
> The idea behind is that some users don't know if a single click is
> expected and thy start doing double clicks instead. So without
> anything else from the developer side, the user will trigger the
> action twice.

1) Retrain users not to do that.

or

2) Inside the binding/command triggered from the widget, set the
respective widget to disabled state, and simultaneously attach a lambda
to an after event to fire in dt time (where dt is longer than the time
between double clicks) to reenable the UI element. During the disabled
time dt, the widget will ignore the second click.

Alexandru

unread,
Feb 28, 2022, 2:56:20 PM2/28/22
to
That's also an idea, except: it has the drawback in case an unexpected / uncatched error happens, then the button remains disabled. This also complicates things more.

Rich

unread,
Feb 28, 2022, 3:28:49 PM2/28/22
to
The lambda you attach to the after event should be so simple that it
will not error (unless the widget has been deleted). It would be as
simple as:

.widget_name configure -state normal

And, if you wanted to take care of the "widget deleted" possibility:

catch [list .widget_name configure -state normal]

And, you can toggle the state, and launch the after, as the very last
thing you do in the binding, then any other uncaught errors in the rest
of the binding would not leave the button in disabled state. Due to
Tcl's event loop, no events will be processed while your
binding/command runs (unless your binding/command itself triggers an
[update] somehow).

Andreas Leitgeb

unread,
Feb 28, 2022, 6:00:25 PM2/28/22
to
Rich <ri...@example.invalid> wrote:
> Except your "requirements" are ambigious. A "double click" is also two
> "mouse button release" events as well. So it is impossible, using pure
> bindings to events without timers, to ignore a double click while doing
> something on a button release.

He had a special kind of "ignore the double click". he meant: ignore the
second one of a double-click, after having already dealt with the first
click. That is perfectly possible without timers.

What is similar, but not possible without timers would be: "if it is a
doubleclick, do nothing at all, even suppress any action on first click".

Rich

unread,
Feb 28, 2022, 9:29:56 PM2/28/22
to
Andreas Leitgeb <a...@logic.at> wrote:
> Rich <ri...@example.invalid> wrote:
>> Except your "requirements" are ambigious. A "double click" is also two
>> "mouse button release" events as well. So it is impossible, using pure
>> bindings to events without timers, to ignore a double click while doing
>> something on a button release.
>
> He had a special kind of "ignore the double click". he meant: ignore the
> second one of a double-click, after having already dealt with the first
> click. That is perfectly possible without timers.

Yes, the "real meaning" finally did eventually get posted, although I
believe it arrived after I posted the above.

> What is similar, but not possible without timers would be: "if it is
> a doubleclick, do nothing at all, even suppress any action on first
> click".

Yes, that, indeed, would need timers.

jtyler

unread,
Feb 28, 2022, 11:18:35 PM2/28/22
to
On 2/28/2022 9:12 AM, Alexandru wrote:
> Is there a simple way (idealy only using bind tricks) to implement the behavior:
>
> If user makes a double click, do nothing.
> If user releases the first mouse button, do something.
>

In the OP's case, I would suggest a somewhat different approach to
eliminate the need for an event timer and be more user responsive.

Treat the double-click the same as a single click. This way the first
button up could "do something" while the second one - if it occurs too
quickly - would just be ignored.

Otherwise, the button release to "do something" would need to wait to be
certain it wasn't part of a double click - less responsive.



Here's an approach I use in a remote control program using the right
mouse button that I think could work here to do the above:

Use a global array to keep track of the current right mouse button state
plus recent timestamps. Bind only to button up and down individually.

::rmb(down) - 0 = up, 1 down
::rmb(0..3) - [clock miliseconds] of last 4 up/down events

For the OP, the button release event would look at the timestamps to
decide if enough time had gone by that this was indeed another separate
button click. The timings for double and triple click would be used to
just ignore 2nd and 3rd clicks.

I use this approach is to implement a mouse-only "shift" capability for
an external device controller. An rmb down/up quickly sends one command,
but if the rmb is held down (1/2 second or more), then other mouse ops
(e.g. wheel and buttons 4/5) do a secondary operation. Then when the rmb
is eventually released, it is ignored, mimicking a shift key

Lots of uses and pretty easy to implement.



clt.to...@dfgh.net

unread,
Mar 1, 2022, 11:15:21 AM3/1/22
to
Try binding the double click to a procedure that does nothing

package require Tk

destroy .bb

proc log1c {w} {puts "1c on $w"}

proc nop {args} {return}

pack .bb [label .bb -text "Label Button!"] -side top

bind .bb <Button-1> "log1c %W"
bind .bb <Double-Button-1> "nop"

This appears to ignore the single click event once a double click is recognized. I'm using on 8.6.8 and 8.7a3 on an old Slackware 14.2ish Linux using fvwm2 window manager. I can start mindlessly clicking and only get one message from log1c (until I pause clicking).

If you remove the double click binding

bind .bb <Double-Button-1> ""

every click triggers the message.

Dave B

jtyler

unread,
Mar 1, 2022, 4:37:18 PM3/1/22
to
On 2/28/2022 8:18 PM, jtyler wrote:
>
> In the OP's case, I would suggest a somewhat different approach to
> eliminate the need for an event timer and be more user responsive.
>

Here's an implementation of what I suggested, with button 1. It doesn't
look into the future, only the past. It will trigger a do something on
the first left click, but not on ones that occur quickly after that (for
any number of extra fast clicks). No timers. No delays to wait for
possible double clicking. But a bit more code than I would have hoped.

It also doesn't do the something if the left button was long pressed and
a right click callback can test if the left is still down.

array set ::lmb [list down 0 0 [clock milliseconds] 1 [clock
milliseconds] 2 [clock milliseconds] 3 [clock milliseconds] ]
set ::double_time 300
proc shift {status} {
global lmb
set lmb(3) $lmb(2)
set lmb(2) $lmb(1)
set lmb(1) $lmb(0)
set lmb(0) [clock milliseconds]
set lmb(down) $status
}
proc btime {} { ;# time for button 1 down and up again
expr { $::lmb(0) - $::lmb(2) }
}
proc htime {} { ;# time button 1 was held down
expr { $::lmb(0) - $::lmb(1) }
}

proc press {} {
shift 1
puts "[format %10s [btime] ] press down = $::lmb(down) "
}

proc release {} {
shift 0
# puts "hold time [htime]"
if { [htime] < 500 } {
if { [btime] > $::double_time } {
puts "\ndo it YES ********"
}
} else {
puts "\ndo it NO hold time too long [htime]"
}
puts "[format %10s [btime] ] release down = $::lmb(down) "
}

proc rpress {} {
if { $::lmb(down) } {
puts "right click while left is down"
} else {
puts "right click while left is up"
}
}

package require Tk
catch {console show}

button .path -text "label"
pack .path -fill both -expand true

bind .path <ButtonPress-3> rpress
bind .path <ButtonPress-1> press
bind .path <ButtonRelease-1> release








Julian H J Loaring

unread,
Mar 25, 2022, 9:46:18 AM3/25/22
to
Here is my attempt at a "debounce" class. I have stripped it from a package - the comments are for Natural Docs...
-----
#
# Namespace: ::Debounce
#
# Author: J H J Loaring
#
# Version: 1.0
#
# Last modified: 25th March 2022
#
# Creates a debounced function that delays invoking a target script
# until after _wait_ milliseconds have elapsed since the last time the
# debounced function was invoked.
#
# Usage:
# ==== Tcl ====
# set debouncer [Debounce new]
# ...
# somecmd [$debounce 800 {script}]
# =============
#
# Example:
# ==== Tcl ====
# set debouncer [Debounce new]
# pack [button .b -text "Double Click Me" -font {TkDefaultFont 30}] -padx 10 -pady 10
# bind .b <1> [$debounce 800 {puts "%W clicked"}]
# =============
#
# Note:
# The *unknown* method is used to allow a simplified call
# without having to use the bind method
#
# $debounce 1000 {...script...}
# vs
# $debounce bind {...script...}
#
package require TclOO
package require oo::util

oo::class create Debounce {
classmethod now {} {
clock milliseconds
}

classmethod newref {} {
classvariable counter
return "[self]::__debounce_cmd_[incr counter]__"
}

constructor {} {
variable interval 0
variable starttime -1
variable cmdref ""
}

destructor {
variable cmdref
rename $cmdref ""
}
method Starttimer {} {
variable starttime
set starttime [Debounce now]
}

method Wait {ms} {
variable interval
set interval $ms
}

method Delta {} {
variable starttime
expr {[Debounce now] - $starttime}
}

method IntervalElapsed {} {
variable interval
expr { [my Started] ? $_wait < [my Delta] : 0}
}

method Started {} {
variable starttime
expr {$starttime != -1}
}

method execscript {body} {
if {![my Started]} {
error "do not call method execscript directly"
}
if {![my Started] || [my IntervalElapsed]} {
my Starttimer
uplevel #0 $body
}
}

method unknown {name args} {
if {[llength $args] < 1} {
return -code rerror "[self object] unknown method $args"
}

if {[string is integer $name]} {
return [my bind $name {*}$args]
}
}
#
# $debounce bind 100 {puts "%W text"}
#
method bind {ms body} {
variable cmdref
my Wait $ms
set cmdref [Debounce newref]
interp alias {} $cmdref {} [self object] execscript
return [list $cmdref $body]
}
}
----
A little test
---
pack [button .b1 -text "Click Me" -font {TkDefaultFont 30}] -padx 10 -pady 10
pack [button .b2 -text "Double Click Me" -font {TkDefaultFont 30}] -padx 10 -pady 10
bind .b1 <1> [list puts "%W clicked"]
bind .b2 <1> [$debounce 800 {puts "%W clicked"}]
---
The 800 ms value in the example was from trial an error...
Hope this helps

Ralf Fassel

unread,
Mar 25, 2022, 11:40:25 AM3/25/22
to
* Julian H J Loaring <jhjlo...@gmail.com>
| Here is my attempt at a "debounce" class. I have stripped it from a package - the comments are for Natural Docs...
--<snip-snip>--
| #
| package require TclOO
| package require oo::util
--<snip-snip>--

$ wish8.6
% info patchlevel
8.6.12
% package require TclOO
1.1.0
% package require oo::util
can't find package oo::util

?

| #
| # $debounce bind 100 {puts "%W text"}
| #
| method bind {ms body} {
| variable cmdref
| my Wait $ms
| set cmdref [Debounce newref]
| interp alias {} $cmdref {} [self object] execscript

- Who is responsible for deleting the 'used' aliases?

R'

clt.to...@dfgh.net

unread,
Mar 25, 2022, 11:57:26 AM3/25/22
to
>From: Julian H J Loaring <jhjlo...@gmail.com>
>Date: Fri Mar 25 13:46:15 GMT 2022
>Subject: Bind to buttonrelease but not to double click


>Here is my attempt at a "debounce" class. I have stripped it from a package - the comments are for
.....
>The 800 ms value in the example was from trial an error...
>Hope this helps


I think you are dupicating logic in the Tk event system. The 800 ms delay will have to be tuned for each system and user.

For a wiget (for example a label) that only respnds to double clicks
(ignores single clicks or more than two clicks) try:


label .odc -text "only double clik"

pack .odc

bind .odc <Button-1> return
bind .odc <Double-Button-1> "puts ok"
bind .odc <Triple-Button-1> return


For a wiget that only responds to a single click when shift is not pressed try:

label .osc -text "Only Single Click"

pack .osc

bind .osc <Button-1> "puts osc"
bind .osc <Shift-Button-1> return
bind .osc <Double-Button-1> return


It just works, no variables, objects or timers are necessary.

Dave B

Julian H J Loaring

unread,
Mar 26, 2022, 5:12:55 AM3/26/22
to
On Friday, 25 March 2022 at 15:40:25 UTC, Ralf Fassel wrote:
> * Julian H J Loaring
> | Here is my attempt at a "debounce" class. I have stripped it from a package - the comments are for Natural Docs...
> --<snip-snip>--
> | #
> | package require TclOO
> | package require oo::util
> --<snip-snip>--
>
> $ wish8.6
> % info patchlevel
> 8.6.12
> % package require TclOO
> 1.1.0
> % package require oo::util
> can't find package oo::util
>
oo::util is in Tcllib. It is used for classvariable and classmethod in lieu of Tcl8.7
> ?
> | #
> | # $debounce bind 100 {puts "%W text"}
> | #
> | method bind {ms body} {
> | variable cmdref
> | my Wait $ms
> | set cmdref [Debounce newref]
> | interp alias {} $cmdref {} [self object] execscript
> - Who is responsible for deleting the 'used' aliases?
>
This is done in the object destructor
> R'

Julian H J Loaring

unread,
Mar 26, 2022, 5:44:08 AM3/26/22
to
On Friday, 25 March 2022 at 13:46:18 UTC, Julian H J Loaring wrote:
> Here is my attempt at a "debounce" class. I have stripped it from a package - the comments are for Natural Docs...
Apologies - I had tidied up the code just before posting and in introduced a bug :(

Note: uses oo::util form Tcllib (https://core.tcl-lang.org/tcllib/technote/cd3a11c3065120d491009e64a19f7676176045cd)

package require TclOO
package require oo::util

oo::class create Debounce {
classmethod now {} {
clock milliseconds
}

classmethod newref {} {
classvariable counter
# Use the Debounce class namespace
return "[self]::__debounce_cmd_[incr counter]__"
}

constructor {} {
variable interval 0
variable starttime -1
variable cmdref ""
}

destructor {
variable cmdref
# Delete the alias when the object dies
rename $cmdref ""
}
method Starttimer {} {
variable starttime
set starttime [Debounce now]
}

method Wait {ms} {
variable interval
set interval $ms
}

method Delta {} {
variable starttime
expr {[Debounce now] - $starttime}
}

method IntervalElapsed {} {
variable interval

expr { [my Started] ? $interval < [my Delta] : 0}
}

method Started {} {
variable starttime
expr {$starttime != -1}
}

method execscript {body} {
variable starttime

if {![my Started] || [my IntervalElapsed]} {
my Starttimer
uplevel #0 $body
}
}

method unknown {name args} {
if {[llength $args] < 1} {
return -code rerror "[self object] unknown method $args"
}

if {[string is integer $name]} {
return [my bind $name {*}$args]
}
}

method bind {ms body} {
variable cmdref
my Wait $ms
set cmdref [Debounce newref]
interp alias {} $cmdref {} [self object] execscript
return [list $cmdref $body]
}
}

# Test
package require Tk
set debounce [Debounce new]

pack [button .b1 -text "Click Me" -font {TkDefaultFont 30}] -padx 10 -pady 10
pack [button .b2 -text "Double Click Me" -font {TkDefaultFont 30}] -padx 10 -pady 10
bind .b1 <1> [list puts "%W clicked"]
bind .b2 <1> [$debounce 800 {puts "%W clicked"}]

# Change 800 to taste and click away
# ...
# eventually delete the debounce object and the alias
$debounce destroy

Julian H J Loaring

unread,
Mar 26, 2022, 5:56:01 AM3/26/22
to
On Friday, 25 March 2022 at 15:57:26 UTC, clt.to...@dfgh.net wrote:
> >From: Julian H J Loaring
I think I have spend too much time looking at JavaScript :) But who knows, a debounce may come in handy elsewhere...

Ralf Fassel

unread,
Mar 28, 2022, 5:16:03 AM3/28/22
to
* Julian H J Loaring <jhjlo...@gmail.com>
| > % package require oo::util
| > can't find package oo::util
| >
| oo::util is in Tcllib. It is used for classvariable and classmethod in lieu of Tcl8.7

Ah, seems my tcllib is too old.

| > | # $debounce bind 100 {puts "%W text"}
| > | #
| > | method bind {ms body} {
| > | variable cmdref
| > | my Wait $ms
| > | set cmdref [Debounce newref]
| > | interp alias {} $cmdref {} [self object] execscript
| > - Who is responsible for deleting the 'used' aliases?
| >
| This is done in the object destructor

What about overwriting an already set cmdref in the bind method?

R'

Julian H J Loaring

unread,
Mar 29, 2022, 8:26:38 AM3/29/22
to
On Monday, 28 March 2022 at 10:16:03 UTC+1, Ralf Fassel wrote:
> * Julian H J Loaring
I will have to think about that!
0 new messages