Skip to content

Commit

Permalink
Experimental double-click support for the button widget
Browse files Browse the repository at this point in the history
For example:

  w := button.New(tw, button.Options{
          Decoration:       button.NormalDecoration,
          DoubleClickDelay: 1 * time.Second,
  })

If DoubleClickDelay is omitted, it is set to a default of 0.5s. Handlers
can be added with the new

  OnDoubleClick(..)

API present on the button widget. If the user interacts with the button
in a way that triggers the double-click callback, and if at least one
handler is registered, then the Click(..) handler is suppressed.
  • Loading branch information
gcla committed May 1, 2022
1 parent ae1ce15 commit 3d53c7c
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 11 deletions.
12 changes: 8 additions & 4 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 195,10 @@ func (t ClickTargets) DeleteClickTargets(k tcell.ButtonMask) {
//======================================================================

type MouseState struct {
MouseLeftClicked bool
MouseMiddleClicked bool
MouseRightClicked bool
MouseLeftClicked bool
MouseMiddleClicked bool
MouseRightClicked bool
MouseLastClickedTime time.Time
}

func (m MouseState) String() string {
Expand Down Expand Up @@ -515,9 516,12 @@ func (a *App) HandleTCellEvent(ev interface{}, unhandled IUnhandledInput) {
a.ClickTargets.DeleteClickTargets(tcell.Button1)
a.ClickTargets.DeleteClickTargets(tcell.Button2)
a.ClickTargets.DeleteClickTargets(tcell.Button3)
a.MouseLastClickedTime = ev.When()
}
a.lastMouse = a.MouseState
a.MouseState = MouseState{}
a.MouseState = MouseState{
MouseLastClickedTime: a.MouseLastClickedTime,
}
a.RedrawTerminal()
}
case *tcell.EventResize:
Expand Down
6 changes: 6 additions & 0 deletions callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 10,7 @@ import (
//======================================================================

type ClickCB struct{}
type DoubleClickCB struct{}
type KeyPressCB struct{}
type SubWidgetCB struct{}
type SubWidgetsCB struct{}
Expand Down Expand Up @@ -63,6 64,7 @@ type ICallbacks interface {
RunCallbacks(name interface{}, args ...interface{})
AddCallback(name interface{}, cb ICallback)
RemoveCallback(name interface{}, cb IIdentity) bool
HaveCallbacks() bool
}

func NewCallbacks() *Callbacks {
Expand All @@ -74,6 76,10 @@ func NewCallbacks() *Callbacks {
return cb
}

func (f *Callbacks) HaveCallbacks() bool {
return len(f.callbacks) > 0
}

// CopyOfCallbacks is used when callbacks are run - they are copied
// so that any callers modifying the callbacks themselves can do so
// safely with the modifications taking effect after all callbacks
Expand Down
14 changes: 12 additions & 2 deletions examples/gowid-widgets1/widgets1.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 6,7 @@ package main

import (
"fmt"
"time"

"github.com/gcla/gowid"
"github.com/gcla/gowid/examples"
Expand All @@ -22,6 23,7 @@ import (
"github.com/gcla/gowid/widgets/text"
"github.com/gcla/gowid/widgets/vpadding"
tcell "github.com/gdamore/tcell/v2"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -101,12 103,15 @@ func main() {
xt := text.New("something else")
xt2 := styled.New(xt, gowid.MakePaletteEntry(gowid.NewUrwidColor("dark red"), gowid.NewUrwidColor("light red")))

tw1 := text.New("click me█ █xx")
tw1 := text.New("click me or double-click me█ █xx")
tw := styled.NewWithRanges(tw1,

[]styled.AttributeRange{styled.AttributeRange{0, 2, nl("test1notfocus")}}, []styled.AttributeRange{styled.AttributeRange{0, -1, nl("test1focus")}})

bw1i := button.New(tw)
bw1i := button.New(tw, button.Options{
Decoration: button.NormalDecoration,
DoubleClickDelay: 200 * time.Millisecond,
})
bw1 := holder.New(bw1i)

dv1 := divider.NewAscii()
Expand All @@ -126,6 131,11 @@ func main() {
}
}})

bw1i.OnDoubleClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) {
logrus.Infof("GCLA: got double click")
pb1.SetProgress(app, 0)
}})

pw := pile.New([]gowid.IContainerWidget{
&gowid.ContainerWidget{pb1, flowme},
&gowid.ContainerWidget{dv1, flowme},
Expand Down
8 changes: 5 additions & 3 deletions gwtest/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 8,7 @@ package gwtest
import (
"errors"
"testing"
"time"

"github.com/gcla/gowid"
tcell "github.com/gdamore/tcell/v2"
Expand Down Expand Up @@ -73,9 74,10 @@ func (d testApp) GetColorMode() gowid.ColorMode {

func (d testApp) GetMouseState() gowid.MouseState {
return gowid.MouseState{
MouseLeftClicked: true,
MouseMiddleClicked: false,
MouseRightClicked: false,
MouseLeftClicked: true,
MouseMiddleClicked: false,
MouseRightClicked: false,
MouseLastClickedTime: time.Now(),
}
}

Expand Down
29 changes: 29 additions & 0 deletions support.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 8,7 @@ package gowid
import (
"fmt"
"strings"
"time"

"github.com/gcla/gowid/gwutil"
tcell "github.com/gdamore/tcell/v2"
Expand Down Expand Up @@ -562,6 563,15 @@ type IClickable interface {
Click(app IApp)
}

// IDoubleClickable is implemented by any type that implements a DoubleClick()
// method, intended to be run in response to a user interaction with the
// type such as two left mouse clicks close together in time.
//
type IDoubleClickable interface {
DoubleClick(app IApp) bool // Return true if action was taken; to suppress Click()
DoubleClickDelay() time.Duration
}

// IKeyPress is implemented by any type that implements a KeyPress()
// method, intended to be run in response to a user interaction with the
// type such as hitting the escape key.
Expand Down Expand Up @@ -1055,6 1065,25 @@ type ClickCallbacks struct {
CB **Callbacks
}

func (w *ClickCallbacks) OnDoubleClick(f IWidgetChangedCallback) {
if *w.CB == nil {
*w.CB = NewCallbacks()
}
AddWidgetCallback(*w.CB, DoubleClickCB{}, f)
}

func (w *ClickCallbacks) RemoveOnDoubleClick(f IIdentity) {
RemoveWidgetCallback(*w.CB, DoubleClickCB{}, f)
}

//======================================================================

// ClickCallbacks is a convenience struct for embedding in a widget, providing methods
// to add and remove callbacks that are executed when the widget is "clicked".
type DoubleClickCallbacks struct {
CB **Callbacks
}

func (w *ClickCallbacks) OnClick(f IWidgetChangedCallback) {
if *w.CB == nil {
*w.CB = NewCallbacks()
Expand Down
33 changes: 32 additions & 1 deletion widgets/button/button.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 7,7 @@ package button

import (
"fmt"
"time"

"github.com/gcla/gowid"
"github.com/gcla/gowid/gwutil"
Expand Down Expand Up @@ -58,6 59,8 @@ var (
BareDecoration = Decoration{Left: "", Right: ""}
NormalDecoration = Decoration{Left: "<", Right: ">"}
AltDecoration = Decoration{Left: "[", Right: "]"}

noTime time.Time
)

//======================================================================
Expand All @@ -79,6 82,7 @@ type Options struct {
Decoration
SelectKeysProvided bool
SelectKeys []gowid.IKey
DoubleClickDelay time.Duration
}

type Widget struct {
Expand All @@ -87,6 91,7 @@ type Widget struct {
*gowid.Callbacks
gowid.SubWidgetCallbacks
gowid.ClickCallbacks
gowid.DoubleClickCallbacks
*Decoration
gowid.AddressProvidesID
gowid.IsSelectable
Expand All @@ -101,18 106,24 @@ func New(inner gowid.IWidget, opts ...Options) *Widget {
opt.Decoration = NormalDecoration
}

if opt.DoubleClickDelay == 0 {
opt.DoubleClickDelay = 500 * time.Millisecond
}

res := &Widget{
inner: inner,
opts: opt,
}

res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks}
res.ClickCallbacks = gowid.ClickCallbacks{CB: &res.Callbacks}
res.DoubleClickCallbacks = gowid.DoubleClickCallbacks{CB: &res.Callbacks}

res.Decoration = &res.opts.Decoration

var _ gowid.IWidget = res
var _ gowid.ICompositeWidget = res
var _ gowid.IDoubleClickable = res
var _ IWidget = res
var _ ICustomKeys = res

Expand Down Expand Up @@ -148,6 159,12 @@ func (w *Widget) Click(app gowid.IApp) {
}
}

func (w *Widget) DoubleClick(app gowid.IApp) bool {
res := w.HaveCallbacks()
gowid.RunWidgetCallbacks(w.Callbacks, gowid.DoubleClickCB{}, app, w)
return res
}

func (w *Widget) SubWidget() gowid.IWidget {
return w.inner
}
Expand Down Expand Up @@ -189,6 206,10 @@ func (w *Widget) SelectKeys() []gowid.IKey {
return w.opts.SelectKeys
}

func (w *Widget) DoubleClickDelay() time.Duration {
return w.opts.DoubleClickDelay
}

//======================================================================

func SubWidgetSize(w IWidget, size interface{}, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize {
Expand Down Expand Up @@ -239,7 260,17 @@ func UserInput(w IClickableIdentityWidget, ev interface{}, size gowid.IRenderSiz
}
})
if clickit {
w.Click(app)
skipClick := false
if dw, ok := w.(gowid.IDoubleClickable); ok {
if dw.DoubleClickDelay() != 0 {
if app.GetLastMouseState().MouseLastClickedTime.Add(dw.DoubleClickDelay()).After(ev.When()) {
skipClick = dw.DoubleClick(app)
}
}
}
if !skipClick {
w.Click(app)
}
res = true
}
}
Expand Down
36 changes: 35 additions & 1 deletion widgets/button/button_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 6,12 @@ package button
import (
"strings"
"testing"
"time"

"github.com/gcla/gowid"
"github.com/gcla/gowid/gwtest"
"github.com/gcla/gowid/widgets/text"
"github.com/gdamore/tcell/v2"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
Expand All @@ -18,10 20,14 @@ import (

func TestButton1(t *testing.T) {
tw := text.New("click")
w := New(tw)
w := New(tw, Options{
Decoration: NormalDecoration,
DoubleClickDelay: 1 * time.Second,
})

ct := &gwtest.ButtonTester{Gotit: false}
assert.Equal(t, ct.Gotit, false)
dct := &gwtest.ButtonTester{Gotit: false}

w.OnClick(ct)

Expand All @@ -31,7 37,35 @@ func TestButton1(t *testing.T) {
w.Click(gwtest.D)
assert.Equal(t, ct.Gotit, true)

sz := gowid.RenderFlowWith{C: 10}
cled1 := tcell.NewEventMouse(1, 0, tcell.Button1, 0)
cleu1 := tcell.NewEventMouse(1, 0, tcell.ButtonNone, 0)

w.OnDoubleClick(dct)

ct.Gotit = false
dct.Gotit = false
assert.Equal(t, ct.Gotit, false)
assert.Equal(t, dct.Gotit, false)

w.UserInput(cled1, sz, gowid.Focused, gwtest.D)
gwtest.D.SetLastMouseState(gowid.MouseState{MouseLeftClicked: true, MouseLastClickedTime: cleu1.When().Add(-500 * time.Millisecond)})
w.UserInput(cleu1, sz, gowid.Focused, gwtest.D)

assert.Equal(t, ct.Gotit, false)
assert.Equal(t, dct.Gotit, true)
ct.Gotit = false
dct.Gotit = false

w.UserInput(cled1, sz, gowid.Focused, gwtest.D)
gwtest.D.SetLastMouseState(gowid.MouseState{MouseLeftClicked: true, MouseLastClickedTime: cleu1.When().Add(-2 * time.Second)})
w.UserInput(cleu1, sz, gowid.Focused, gwtest.D)

assert.Equal(t, ct.Gotit, true)
assert.Equal(t, dct.Gotit, false)

ct.Gotit = false
dct.Gotit = false
assert.Equal(t, ct.Gotit, false)
w.RemoveOnClick(ct)
w.Click(gwtest.D)
Expand Down

0 comments on commit 3d53c7c

Please sign in to comment.