Skip to content

Commit

Permalink
Merge pull request fyne-io#4337 from dweymouth/entry-triple-tap
Browse files Browse the repository at this point in the history
Adding triple tap to select current row for widget.Entry
  • Loading branch information
dweymouth authored Dec 14, 2023
2 parents 2750fc8 + f710676 commit c90d255
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 10 deletions.
8 changes: 8 additions & 0 deletions driver.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fyne

import "time"

// Driver defines an abstract concept of a Fyne render driver.
// Any implementation must provide at least these methods.
type Driver interface {
Expand Down Expand Up @@ -29,4 +31,10 @@ type Driver interface {
StartAnimation(*Animation)
// StopAnimation stops an animation and unregisters from this driver.
StopAnimation(*Animation)

// DoubleTapDelay returns the maximum duration where a second tap after a first one
// will be considered a DoubleTap instead of two distinct Tap events.
//
// Since: 2.5
DoubleTapDelay() time.Duration
}
7 changes: 7 additions & 0 deletions internal/driver/glfw/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"runtime"
"sync"
"sync/atomic"
"time"

"github.com/fyne-io/image/ico"

Expand Down Expand Up @@ -36,6 +37,8 @@ var _ fyne.Driver = (*gLDriver)(nil)
// A workaround on Apple M1/M2, just use 1 thread until fixed upstream.
const drawOnMainThread bool = runtime.GOOS == "darwin" && runtime.GOARCH == "arm64"

const doubleTapDelay = 300 * time.Millisecond

type gLDriver struct {
windowLock sync.RWMutex
windows []fyne.Window
Expand Down Expand Up @@ -168,6 +171,10 @@ func (d *gLDriver) Run() {
d.runGL()
}

func (d *gLDriver) DoubleTapDelay() time.Duration {
return doubleTapDelay
}

// NewGLDriver sets up a new Driver instance implemented using the GLFW Go library and OpenGL bindings.
func NewGLDriver() *gLDriver {
repository.Register("file", intRepo.NewFileRepository())
Expand Down
5 changes: 2 additions & 3 deletions internal/driver/glfw/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import (
)

const (
doubleClickDelay = 300 // ms (maximum interval between clicks for double click detection)
dragMoveThreshold = 2 // how far can we move before it is a drag
dragMoveThreshold = 2 // how far can we move before it is a drag
windowIconSize = 256
)

Expand Down Expand Up @@ -646,7 +645,7 @@ func (w *window) mouseClickedHandleTapDoubleTap(co fyne.CanvasObject, ev *fyne.P
func (w *window) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent) {
var ctx context.Context
w.mouseLock.Lock()
ctx, w.mouseCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(time.Millisecond*doubleClickDelay))
ctx, w.mouseCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(doubleTapDelay))
defer w.mouseCancelFunc()
w.mouseLock.Unlock()

Expand Down
6 changes: 1 addition & 5 deletions internal/driver/mobile/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import (
"fyne.io/fyne/v2/widget"
)

const (
doubleClickDelay = 500 // ms (maximum interval between clicks for double click detection)
)

var _ fyne.Canvas = (*mobileCanvas)(nil)

type mobileCanvas struct {
Expand Down Expand Up @@ -377,7 +373,7 @@ func (c *mobileCanvas) tapUp(pos fyne.Position, tapID int,

func (c *mobileCanvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) {
var ctx context.Context
ctx, c.touchCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(time.Millisecond*doubleClickDelay))
ctx, c.touchCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(tapDoubleDelay))
defer c.touchCancelFunc()
<-ctx.Done()
if c.touchTapCount == 2 && c.touchLastTapped == co {
Expand Down
4 changes: 2 additions & 2 deletions internal/driver/mobile/canvas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,15 @@ func TestCanvas_Focusable(t *testing.T) {
c.tapUp(pos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) {
wid.Tapped(ev)
}, nil, nil, nil)
time.Sleep(time.Millisecond * (doubleClickDelay + 150))
time.Sleep(tapDoubleDelay + 150*time.Millisecond)
assert.Equal(t, 1, content.focusedTimes)
assert.Equal(t, 0, content.unfocusedTimes)

c.tapDown(pos, 1)
c.tapUp(pos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) {
wid.Tapped(ev)
}, nil, nil, nil)
time.Sleep(time.Millisecond * (doubleClickDelay + 150))
time.Sleep(tapDoubleDelay + 150*time.Millisecond)
assert.Equal(t, 1, content.focusedTimes)
assert.Equal(t, 0, content.unfocusedTimes)

Expand Down
5 changes: 5 additions & 0 deletions internal/driver/mobile/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
const (
tapMoveThreshold = 4.0 // how far can we move before it is a drag
tapSecondaryDelay = 300 * time.Millisecond // how long before secondary tap
tapDoubleDelay = 500 * time.Millisecond // max duration between taps for a DoubleTap event
)

// Configuration is the system information about the current device
Expand Down Expand Up @@ -550,6 +551,10 @@ func (d *mobileDriver) SetOnConfigurationChanged(f func(*Configuration)) {
d.onConfigChanged = f
}

func (d *mobileDriver) DoubleTapDelay() time.Duration {
return tapDoubleDelay
}

// NewGoMobileDriver sets up a new Driver instance implemented using the Go
// Mobile extension and OpenGL bindings.
func NewGoMobileDriver() fyne.Driver {
Expand Down
5 changes: 5 additions & 0 deletions test/testdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package test
import (
"image"
"sync"
"time"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/driver"
Expand Down Expand Up @@ -129,3 +130,7 @@ func (d *testDriver) removeWindow(w *testWindow) {
d.windows = append(d.windows[:i], d.windows[i+1:]...)
d.windowsMutex.Unlock()
}

func (d *testDriver) DoubleTapDelay() time.Duration {
return 300 * time.Millisecond
}
34 changes: 34 additions & 0 deletions widget/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ type Entry struct {
// undoStack stores the data necessary for undo/redo functionality
// See entryUndoStack for implementation details.
undoStack entryUndoStack

// doubleTappedAtUnixMillis stores the time the entry was last DoubleTapped
// used for deciding whether the next MouseDown/TouchDown is a triple-tap or not
doubleTappedAtUnixMillis int64
}

// NewEntry creates a new single line entry widget.
Expand Down Expand Up @@ -229,6 +233,7 @@ func (e *Entry) Disabled() bool {
//
// Implements: fyne.DoubleTappable
func (e *Entry) DoubleTapped(p *fyne.PointEvent) {
e.doubleTappedAtUnixMillis = time.Now().UnixMilli()
row := e.textProvider().row(e.CursorRow)
start, end := getTextWhitespaceRegion(row, e.CursorColumn, false)
if start == -1 || end == -1 {
Expand All @@ -250,6 +255,10 @@ func (e *Entry) DoubleTapped(p *fyne.PointEvent) {
})
}

func (e *Entry) isTripleTap(nowMilli int64) bool {
return nowMilli-e.doubleTappedAtUnixMillis <= fyne.CurrentApp().Driver().DoubleTapDelay().Milliseconds()
}

// DragEnd is called at end of a drag event.
//
// Implements: fyne.Draggable
Expand Down Expand Up @@ -407,6 +416,10 @@ func (e *Entry) MinSize() fyne.Size {
//
// Implements: desktop.Mouseable
func (e *Entry) MouseDown(m *desktop.MouseEvent) {
if e.isTripleTap(time.Now().UnixMilli()) {
e.selectCurrentRow()
return
}
e.propertyLock.Lock()
if e.selectKeyDown {
e.selecting = true
Expand Down Expand Up @@ -606,9 +619,14 @@ func (e *Entry) TappedSecondary(pe *fyne.PointEvent) {
//
// Implements: mobile.Touchable
func (e *Entry) TouchDown(ev *mobile.TouchEvent) {
now := time.Now().UnixMilli()
if !e.Disabled() {
e.requestFocus()
}
if e.isTripleTap(now) {
e.selectCurrentRow()
return
}

e.updateMousePointer(ev.Position, false)
}
Expand Down Expand Up @@ -1502,6 +1520,22 @@ func (e *Entry) typedKeyReturn(provider *RichText, multiLine bool) {
e.propertyLock.Unlock()
}

// Selects the row where the CursorColumn is currently positioned
// Do not call while holding the proeprtyLock
func (e *Entry) selectCurrentRow() {
provider := e.textProvider()
e.propertyLock.Lock()
e.selectRow = e.CursorRow
e.selectColumn = 0
if e.MultiLine {
e.CursorColumn = provider.rowLength(e.CursorRow)
} else {
e.CursorColumn = provider.len()
}
e.propertyLock.Unlock()
e.Refresh()
}

var _ fyne.WidgetRenderer = (*entryRenderer)(nil)

type entryRenderer struct {
Expand Down
4 changes: 4 additions & 0 deletions widget/entry_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,16 @@ func TestEntry_DoubleTapped(t *testing.T) {
entry.DoubleTapped(ev)
assert.Equal(t, "quick", entry.SelectedText())

entry.doubleTappedAtUnixMillis = 0 // make sure we don't register a triple tap next

// select the whitespace after 'quick'
ev = getClickPosition("The quick", 0)
clickPrimary(entry, ev)
entry.DoubleTapped(ev)
assert.Equal(t, " ", entry.SelectedText())

entry.doubleTappedAtUnixMillis = 0

// select all whitespace after 'jumped'
ev = getClickPosition("jumped ", 1)
clickPrimary(entry, ev)
Expand Down
11 changes: 11 additions & 0 deletions widget/entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,17 @@ func TestEntry_SelectSnapUp(t *testing.T) {
assert.Equal(t, "", e.SelectedText())
}

func TestEntry_Select_TripleTap(t *testing.T) {
e, _ := setupSelection(t, false)
e.MultiLine = true
assert.Equal(t, 1, e.CursorRow)
assert.Equal(t, "sti", e.SelectedText())
test.DoubleTap(e)
time.Sleep(50 * time.Millisecond)
e.MouseDown(&desktop.MouseEvent{PointEvent: fyne.PointEvent{Position: fyne.NewPos(1, 1)}})
assert.Equal(t, "Testing", e.SelectedText())
}

func TestEntry_SelectedText(t *testing.T) {
e, window := setupImageTest(t, false)
defer teardownImageTest(window)
Expand Down

0 comments on commit c90d255

Please sign in to comment.