Skip to content

Commit

Permalink
Add support for double click / drag selection
Browse files Browse the repository at this point in the history
  • Loading branch information
jestor committed Jun 10, 2019
1 parent 75e6ebb commit 2ca95b8
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 16 deletions.
5 changes: 5 additions & 0 deletions canvasobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ type Tappable interface {
TappedSecondary(*PointEvent)
}

// DoubleTappable describes any CanvasObject that can also be double tapped.
type DoubleTappable interface {
DoubleTapped(*PointEvent)
}

// Disableable describes any CanvasObject that can be disabled.
// This is primarily used with objects that also implement the Tappable interface.
type Disableable interface {
Expand Down
49 changes: 41 additions & 8 deletions internal/driver/gl/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"runtime"
"strconv"
"sync"
"time"

"fyne.io/fyne"
Expand All @@ -19,7 +20,8 @@ import (
)

const (
scrollSpeed = 10
scrollSpeed = 10
doubleClickDelay = 500 // ms (minimum interval between clicks for double click detection)
)

var (
Expand Down Expand Up @@ -52,11 +54,12 @@ type window struct {
padded bool
visible bool

mousePos fyne.Position
mouseDragPos fyne.Position
mouseButton desktop.MouseButton
mouseOver desktop.Hoverable
onClosed func()
mousePos fyne.Position
mouseDragPos fyne.Position
mouseButton desktop.MouseButton
mouseOver desktop.Hoverable
mouseClickTime time.Time
onClosed func()

xpos, ypos int
width, height int
Expand Down Expand Up @@ -628,14 +631,25 @@ func (w *window) mouseClicked(viewport *glfw.Window, button glfw.MouseButton, ac
ev := new(fyne.PointEvent)
ev.Position = fyne.NewPos(x, y)

// Prevent async mouse functions from arriving out of order
var mouseWG sync.WaitGroup

if wid, ok := co.(desktop.Mouseable); ok {
mev := new(desktop.MouseEvent)
mev.Position = ev.Position
mev.Button = convertMouseButton(button)
if action == glfw.Press {
go wid.MouseDown(mev)
mouseWG.Add(1)
go func() {
defer mouseWG.Done()
wid.MouseDown(mev)
}()
} else if action == glfw.Release {
go wid.MouseUp(mev)
mouseWG.Add(1)
go func() {
defer mouseWG.Done()
wid.MouseUp(mev)
}()
}
}

Expand All @@ -661,8 +675,27 @@ func (w *window) mouseClicked(viewport *glfw.Window, button glfw.MouseButton, ac
w.canvas.Focus(wid)
}
}

// Check for double click on mouse left release
if action == glfw.Release && button == glfw.MouseButtonLeft {
now := time.Now()
// we can safely subtract the first "zero" time as it'll be much larger than doubleClickDelay
if now.Sub(w.mouseClickTime).Nanoseconds()/1e6 <= doubleClickDelay {
if wid, ok := co.(fyne.DoubleTappable); ok {
mouseWG.Wait() // wait for any mouse events to finish
mouseWG.Add(1)
go func() {
defer mouseWG.Done()
wid.DoubleTapped(ev)
}()
}
}
w.mouseClickTime = now
}

if wid, ok := co.(fyne.Tappable); ok {
if action == glfw.Release {
mouseWG.Wait() // wait for any mouse events to finish
switch button {
case glfw.MouseButtonRight:
go wid.TappedSecondary(ev)
Expand Down
86 changes: 78 additions & 8 deletions widget/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,25 +426,41 @@ func (e *Entry) Focused() bool {
func (e *Entry) cursorColAt(text []rune, pos fyne.Position) int {
for i := 0; i < len(text); i++ {
str := string(text[0 : i+1])
wid := textMinSize(str, theme.TextSize(), e.textStyle()).Width
wid := textMinSize(str, theme.TextSize(), e.textStyle()).Width + theme.Padding()
if wid > pos.X {
return i
}
}
return len(text)
}

// Tapped is called when this entry has been tapped so we should update the cursor position.
func (e *Entry) Tapped(ev *fyne.PointEvent) {
if !e.focused {
e.FocusGained()
}
// MouseDown called on mouse click, this triggers a mouse click which can move the cursor,
// update the existing selection (if shift is held), or start a selection dragging operation.
func (e *Entry) MouseDown(m *desktop.MouseEvent) {
if e.selectKeyDown {
e.selecting = true
}
if e.selecting && e.selectKeyDown == false {
e.selecting = false
}
e.updateMousePointer(&m.PointEvent, e.selecting == false)
}

// MouseUp called on mouse release (ignored)
func (e *Entry) MouseUp(m *desktop.MouseEvent) {
}

// Dragged is called when the pointer moves while a button is held down
func (e *Entry) Dragged(d *fyne.DragEvent) {
e.selecting = true
e.updateMousePointer(&d.PointEvent, false)
}

func (e *Entry) updateMousePointer(ev *fyne.PointEvent, startSelect bool) {

if !e.focused {
e.FocusGained()
}

rowHeight := e.textProvider().charMinSize().Height
row := int(math.Floor(float64(ev.Position.Y-theme.Padding()) / float64(rowHeight)))
Expand All @@ -461,12 +477,66 @@ func (e *Entry) Tapped(ev *fyne.PointEvent) {
e.Lock()
e.CursorRow = row
e.CursorColumn = col
if startSelect {
e.selectRow = row
e.selectColumn = col
}
e.Unlock()
Renderer(e).(*entryRenderer).moveCursor()
}

// TappedSecondary is called when right or alternative tap is invoked - this is currently ignored.
func (e *Entry) TappedSecondary(_ *fyne.PointEvent) {
// DoubleTapped is called when this entry has been double tapped so we should select text below the pointer
func (e *Entry) DoubleTapped(ev *fyne.PointEvent) {
row := e.textProvider().row(e.CursorRow)
start, end := e.CursorColumn, e.CursorColumn+1
if start >= len(row) {
start = len(row) - 1
}
if end >= len(row) {
end = len(row) - 1
}

if row[start] == ' ' || row[start] == '\t' {
// whitespace clicked, select all whitespace (search in both directions for non-whitespace)
for ; start > 0; start-- {
if row[start-1] != ' ' && row[start-1] != '\t' {
break
}
}
for ; end < len(row); end++ {
// find first non-whitespace rune
if row[end] != ' ' && row[end] != '\t' {
break
}
}
} else {
// text clicked, select all text (search in both directions for whitespace)
for ; start > 0; start-- {
if row[start-1] == ' ' || row[start-1] == '\t' {
break
}
}
for ; end < len(row); end++ {
if row[end] == ' ' || row[end] == '\t' || row[end] == '\r' || row[end] == '\n' {
break
}
}
}

e.Lock()
if e.selectKeyDown == false {
e.selectRow = e.CursorRow
e.selectColumn = start
}
// Always aim to maximise the selected region
if e.selectRow > e.CursorRow || (e.selectRow == e.CursorRow && e.selectColumn > e.CursorColumn) {
e.CursorColumn = start
} else {
e.CursorColumn = end
}
e.selecting = true
e.Unlock()
Renderer(e).(*entryRenderer).moveCursor()
}

// TypedRune receives text input events when the Entry widget is focused.
Expand Down

0 comments on commit 2ca95b8

Please sign in to comment.