From 96875c75b9f927585f21b08f09841b4fea8ab4bf Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Mon, 4 Nov 2019 05:34:46 +0000 Subject: [PATCH 01/24] Added mouse handling --- application.go | 120 +++++++++++++++++++++++++++++++++++++++++++++---- box.go | 49 ++++++++++++++++++++ button.go | 12 +++++ flex.go | 8 ++++ form.go | 14 ++++++ frame.go | 4 ++ grid.go | 8 ++++ modal.go | 4 ++ pages.go | 12 +++++ primitive.go | 13 ++++++ 10 files changed, 236 insertions(+), 8 deletions(-) diff --git a/application.go b/application.go index 05a4357..a212691 100644 --- a/application.go +++ b/application.go @@ -38,6 +38,9 @@ type Application struct { // Whether or not the application resizes the root primitive. rootFullscreen bool + // Enable mouse events? + enableMouse bool + // An optional capture function which receives a key event and returns the // event to be forwarded to the default input handler (nil if nothing should // be forwarded). @@ -62,6 +65,33 @@ type Application struct { // (screen.Init() and draw() will be called implicitly). A value of nil will // stop the application. screenReplacement chan tcell.Screen + + // An optional capture function which receives a mouse event and returns the + // event to be forwarded to the default mouse handler (nil if nothing should + // be forwarded). + mouseCapture func(event EventMouse) EventMouse +} + +// EventKey is the key input event info. +// This exists for some consistency with EventMouse, +// even though it's just an alias to *tcell.EventKey for backwards compatibility. +type EventKey = *tcell.EventKey + +// EventMouse is the mouse event info. +type EventMouse struct { + *tcell.EventMouse + Target Primitive + Application *Application +} + +// IsZero returns true if this is a zero object. +func (e EventMouse) IsZero() bool { + return e == EventMouse{} +} + +// SetFocus will set focus to the primitive. +func (e EventMouse) SetFocus(p Primitive) { + e.Application.SetFocus(p) } // NewApplication creates and returns a new application. @@ -93,6 +123,22 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event return a.inputCapture } +// SetMouseCapture sets a function which captures mouse events before they are +// forwarded to the appropriate mouse event handler. +// This function can then choose to forward that event (or a +// different one) by returning it or stop the event processing by returning +// nil. +func (a *Application) SetMouseCapture(capture func(event EventMouse) EventMouse) *Application { + a.mouseCapture = capture + return a +} + +// GetMouseCapture returns the function installed with SetMouseCapture() or nil +// if no such function has been installed. +func (a *Application) GetMouseCapture() func(event EventMouse) EventMouse { + return a.mouseCapture +} + // SetScreen allows you to provide your own tcell.Screen object. For most // applications, this is not needed and you should be familiar with // tcell.Screen when using this function. @@ -121,6 +167,13 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application { return a } +// EnableMouse enables mouse events. +func (a *Application) EnableMouse() { + a.Lock() + a.enableMouse = true + a.Unlock() +} + // Run starts the application and thus the event loop. This function returns // when Stop() was called. func (a *Application) Run() error { @@ -138,6 +191,9 @@ func (a *Application) Run() error { a.Unlock() return err } + if a.enableMouse { + a.screen.EnableMouse() + } } // We catch panics to clean up because they mess up the terminal. @@ -207,13 +263,15 @@ EventLoop: break EventLoop } + a.RLock() + p := a.focus + inputCapture := a.inputCapture + mouseCapture := a.mouseCapture + screen := a.screen + a.RUnlock() + switch event := event.(type) { case *tcell.EventKey: - a.RLock() - p := a.focus - inputCapture := a.inputCapture - a.RUnlock() - // Intercept keys. if inputCapture != nil { event = inputCapture(event) @@ -238,14 +296,32 @@ EventLoop: } } case *tcell.EventResize: - a.RLock() - screen := a.screen - a.RUnlock() if screen == nil { continue } screen.Clear() a.draw() + case *tcell.EventMouse: + atX, atY := event.Position() + ptarget := a.GetPrimitiveAtPoint(atX, atY) // p under mouse. + if ptarget == nil { + ptarget = p // Fallback to focused. + } + event2 := EventMouse{event, ptarget, a} + + // Intercept event. + if mouseCapture != nil { + event2 = mouseCapture(event2) + if event2.IsZero() { + a.draw() + continue // Don't forward event. + } + } + + if handler := ptarget.MouseHandler(); handler != nil { + handler(event2) + a.draw() + } } // If we have updates, now is the time to execute them. @@ -261,6 +337,34 @@ EventLoop: return nil } +func findAtPoint(atX, atY int, p Primitive) Primitive { + x, y, w, h := p.GetRect() + if atX < x || atY < y { + return nil + } + if atX >= x+w || atY >= y+h { + return nil + } + bestp := p + for _, pchild := range p.GetChildren() { + x := findAtPoint(atX, atY, pchild) + if x != nil { + // Always overwrite if we find another one, + // this is because if any overlap, the last one is "on top". + bestp = x + } + } + return bestp +} + +// GetPrimitiveAtPoint returns the Primitive at the specified point, or nil. +// Note that this only works with a valid hierarchy of primitives (children) +func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive { + a.RLock() + defer a.RUnlock() + return findAtPoint(atX, atY, a.root) +} + // Stop stops the application, causing Run() to return. func (a *Application) Stop() { a.Lock() diff --git a/box.go b/box.go index 809b3c9..ff11586 100644 --- a/box.go +++ b/box.go @@ -58,6 +58,11 @@ type Box struct { // An optional function which is called before the box is drawn. draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) + + // An optional capture function which receives a mouse event and returns the + // event to be forwarded to the primitive's default mouse event handler (nil if + // nothing should be forwarded). + mouseCapture func(event EventMouse) EventMouse } // NewBox returns a Box without a border. @@ -192,6 +197,45 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { return b.inputCapture } +// WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the +// functionality to capture input (see SetMouseCapture()) before passing it +// on to the provided (default) event handler. +// +// This is only meant to be used by subclassing primitives. +func (b *Box) WrapMouseHandler(mouseHandler func(EventMouse)) func(EventMouse) { + return func(event EventMouse) { + if b.mouseCapture != nil { + event = b.mouseCapture(event) + } + if !event.IsZero() && mouseHandler != nil { + mouseHandler(event) + } + } +} + +// MouseHandler returns nil. +func (b *Box) MouseHandler() func(event EventMouse) { + return b.WrapMouseHandler(nil) +} + +// SetMouseCapture installs a function which captures events before they are +// forwarded to the primitive's default event handler. This function can +// then choose to forward that event (or a different one) to the default +// handler by returning it. If nil is returned, the default handler will not +// be called. +// +// Providing a nil handler will remove a previously existing handler. +func (b *Box) SetMouseCapture(capture func(EventMouse) EventMouse) *Box { + b.mouseCapture = capture + return b +} + +// GetMouseCapture returns the function installed with SetMouseCapture() or nil +// if no such function has been installed. +func (b *Box) GetMouseCapture() func(EventMouse) EventMouse { + return b.mouseCapture +} + // SetBackgroundColor sets the box's background color. func (b *Box) SetBackgroundColor(color tcell.Color) *Box { b.backgroundColor = color @@ -353,3 +397,8 @@ func (b *Box) HasFocus() bool { func (b *Box) GetFocusable() Focusable { return b.focus } + +// GetChildren gets the children. +func (b *Box) GetChildren() []Primitive { + return nil +} diff --git a/button.go b/button.go index d3a6aed..eb8a676 100644 --- a/button.go +++ b/button.go @@ -135,3 +135,15 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim } }) } + +// InputHandler returns the handler for this primitive. +func (b *Button) MouseHandler() func(event EventMouse) { + return b.WrapMouseHandler(func(event EventMouse) { + // Process mouse event. + if event.Buttons()&tcell.Button1 != 0 { + if b.selected != nil { + b.selected() + } + } + }) +} diff --git a/flex.go b/flex.go index 56cbc75..660d67d 100644 --- a/flex.go +++ b/flex.go @@ -195,3 +195,11 @@ func (f *Flex) HasFocus() bool { } return false } + +func (f *Flex) GetChildren() []Primitive { + children := make([]Primitive, len(f.items)) + for i, item := range f.items { + children[i] = item.Item + } + return children +} diff --git a/form.go b/form.go index 8b9e77a..47051cb 100644 --- a/form.go +++ b/form.go @@ -600,3 +600,17 @@ func (f *Form) focusIndex() int { } return -1 } + +func (f *Form) GetChildren() []Primitive { + children := make([]Primitive, len(f.items)+len(f.buttons)) + i := 0 + for _, item := range f.items { + children[i] = item + i++ + } + for _, button := range f.buttons { + children[i] = button + i++ + } + return children +} diff --git a/frame.go b/frame.go index 77c5316..99d092a 100644 --- a/frame.go +++ b/frame.go @@ -155,3 +155,7 @@ func (f *Frame) HasFocus() bool { } return false } + +func (f *Frame) GetChildren() []Primitive { + return []Primitive{f.primitive} +} diff --git a/grid.go b/grid.go index 2de0f0c..70d0303 100644 --- a/grid.go +++ b/grid.go @@ -660,3 +660,11 @@ func (g *Grid) Draw(screen tcell.Screen) { } } } + +func (g *Grid) GetChildren() []Primitive { + children := make([]Primitive, len(g.items)) + for i, item := range g.items { + children[i] = item.Item + } + return children +} diff --git a/modal.go b/modal.go index f359a14..c0707b1 100644 --- a/modal.go +++ b/modal.go @@ -169,3 +169,7 @@ func (m *Modal) Draw(screen tcell.Screen) { m.frame.SetRect(x, y, width, height) m.frame.Draw(screen) } + +func (m *Modal) GetChildren() []Primitive { + return []Primitive{m.frame} +} diff --git a/pages.go b/pages.go index 155da73..35a1d9d 100644 --- a/pages.go +++ b/pages.go @@ -278,3 +278,15 @@ func (p *Pages) Draw(screen tcell.Screen) { page.Item.Draw(screen) } } + +func (p *Pages) GetChildren() []Primitive { + var children []Primitive + for _, page := range p.pages { + // Considering invisible pages as not children. + // Even though we track all the pages, not all are "children" currently. + if page.Visible { + children = append(children, page.Item) + } + } + return children +} diff --git a/primitive.go b/primitive.go index 88a9d46..6c7a6c7 100644 --- a/primitive.go +++ b/primitive.go @@ -43,4 +43,17 @@ type Primitive interface { // GetFocusable returns the item's Focusable. GetFocusable() Focusable + + // GetChildren gets the children. + GetChildren() []Primitive + + // MouseHandler returns a handler which receives mouse events. + // It is called by the Application class. + // + // A zero value of EventMouse{} may also be returned to stop propagation. + // + // The Box class provides functionality to intercept mouse events. If you + // subclass from Box, it is recommended that you wrap your handler using + // Box.WrapMouseHandler() so you inherit that functionality. + MouseHandler() func(event EventMouse) } From 2f4b6ad7481e3811374dd70a3d5a0d1a1f086253 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Mon, 4 Nov 2019 05:42:46 +0000 Subject: [PATCH 02/24] Return self --- application.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application.go b/application.go index a212691..9d8a51f 100644 --- a/application.go +++ b/application.go @@ -168,10 +168,11 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application { } // EnableMouse enables mouse events. -func (a *Application) EnableMouse() { +func (a *Application) EnableMouse() *Application { a.Lock() a.enableMouse = true a.Unlock() + return a } // Run starts the application and thus the event loop. This function returns From d7250288e2ad0b3b67b91db2e36901b8ed42bdce Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Mon, 4 Nov 2019 06:30:25 +0000 Subject: [PATCH 03/24] More mouse handling for primitives --- button.go | 2 +- checkbox.go | 13 +++++++++++++ demos/presentation/main.go | 2 ++ dropdown.go | 11 +++++++++++ inputfield.go | 10 ++++++++++ list.go | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 1 deletion(-) diff --git a/button.go b/button.go index eb8a676..339f01b 100644 --- a/button.go +++ b/button.go @@ -136,7 +136,7 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim }) } -// InputHandler returns the handler for this primitive. +// MouseHandler returns the mouse handler for this primitive. func (b *Button) MouseHandler() func(event EventMouse) { return b.WrapMouseHandler(func(event EventMouse) { // Process mouse event. diff --git a/checkbox.go b/checkbox.go index 8f099d8..5092b3d 100644 --- a/checkbox.go +++ b/checkbox.go @@ -201,3 +201,16 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } }) } + +// MouseHandler returns the mouse handler for this primitive. +func (c *Checkbox) MouseHandler() func(event EventMouse) { + return c.WrapMouseHandler(func(event EventMouse) { + // Process mouse event. + if event.Buttons()&tcell.Button1 != 0 { + c.checked = !c.checked + if c.changed != nil { + c.changed(c.checked) + } + } + }) +} diff --git a/demos/presentation/main.go b/demos/presentation/main.go index c75fdc0..f72687b 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -91,6 +91,8 @@ func main() { return event }) + app.EnableMouse() + // Start the application. if err := app.SetRoot(layout, true).Run(); err != nil { panic(err) diff --git a/dropdown.go b/dropdown.go index 58eee24..d406504 100644 --- a/dropdown.go +++ b/dropdown.go @@ -483,3 +483,14 @@ func (d *DropDown) HasFocus() bool { } return d.hasFocus } + +// MouseHandler returns the mouse handler for this primitive. +func (d *DropDown) MouseHandler() func(event EventMouse) { + return d.WrapMouseHandler(func(event EventMouse) { + // Process mouse event. + if event.Buttons()&tcell.Button1 != 0 { + //d.open = !d.open // FIXME: clicks go through the dropdown! + event.SetFocus(d) + } + }) +} diff --git a/inputfield.go b/inputfield.go index 3cd255e..992f7ab 100644 --- a/inputfield.go +++ b/inputfield.go @@ -591,3 +591,13 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p } }) } + +// MouseHandler returns the mouse handler for this primitive. +func (i *InputField) MouseHandler() func(event EventMouse) { + return i.WrapMouseHandler(func(event EventMouse) { + // Process mouse event. + if event.Buttons()&tcell.Button1 != 0 { + event.SetFocus(i) + } + }) +} diff --git a/list.go b/list.go index 848f605..b36ef78 100644 --- a/list.go +++ b/list.go @@ -527,3 +527,35 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit } }) } + +// returns -1 if not found. +func (l *List) indexAtPoint(atX, atY int) int { + _, y, _, h := l.GetInnerRect() + if atY < y || atY >= y+h { + return -1 + } + + n := atY - y + if l.showSecondaryText { + n /= 2 + } + + if n >= len(l.items) { + return -1 + } + return n +} + +// MouseHandler returns the mouse handler for this primitive. +func (l *List) MouseHandler() func(event EventMouse) { + return l.WrapMouseHandler(func(event EventMouse) { + // Process mouse event. + if event.Buttons()&tcell.Button1 != 0 { + atX, atY := event.Position() + index := l.indexAtPoint(atX, atY) + if index != -1 { + l.SetCurrentItem(index) + } + } + }) +} From d891191b780e5133f3042532d465edca4863d742 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Mon, 4 Nov 2019 06:55:58 +0000 Subject: [PATCH 04/24] Fire Selected on list item click --- demos/list/main.go | 1 + list.go | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/demos/list/main.go b/demos/list/main.go index 50cae17..3ca90eb 100644 --- a/demos/list/main.go +++ b/demos/list/main.go @@ -15,6 +15,7 @@ func main() { AddItem("Quit", "Press to exit", 'q', func() { app.Stop() }) + app.EnableMouse() if err := app.SetRoot(list, true).Run(); err != nil { panic(err) } diff --git a/list.go b/list.go index b36ef78..05c68d7 100644 --- a/list.go +++ b/list.go @@ -554,7 +554,17 @@ func (l *List) MouseHandler() func(event EventMouse) { atX, atY := event.Position() index := l.indexAtPoint(atX, atY) if index != -1 { - l.SetCurrentItem(index) + item := l.items[index] + if item.Selected != nil { + item.Selected() + } + if l.selected != nil { + l.selected(index, item.MainText, item.SecondaryText, item.Shortcut) + } + if index != l.currentItem && l.changed != nil { + l.changed(index, item.MainText, item.SecondaryText, item.Shortcut) + } + l.currentItem = index } } }) From 871ddc57d7570779f83524095e8cd49479c2c9b3 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 5 Nov 2019 01:11:21 +0000 Subject: [PATCH 05/24] DropDown mouse support --- dropdown.go | 134 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/dropdown.go b/dropdown.go index d406504..43b7eb6 100644 --- a/dropdown.go +++ b/dropdown.go @@ -394,22 +394,6 @@ func (d *DropDown) Draw(screen tcell.Screen) { // InputHandler returns the handler for this primitive. func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { - // A helper function which selects an item in the drop-down list based on - // the current prefix. - evalPrefix := func() { - if len(d.prefix) > 0 { - for index, option := range d.options { - if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) { - d.list.SetCurrentItem(index) - return - } - } - // Prefix does not match any item. Remove last rune. - r := []rune(d.prefix) - d.prefix = string(r[:len(r)-1]) - } - } - // Process key event. switch key := event.Key(); key { case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: @@ -418,45 +402,10 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // If the first key was a letter already, it becomes part of the prefix. if r := event.Rune(); key == tcell.KeyRune && r != ' ' { d.prefix += string(r) - evalPrefix() + d.evalPrefix() } - // Hand control over to the list. - d.open = true - optionBefore := d.currentOption - d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { - // An option was selected. Close the list again. - d.open = false - setFocus(d) - d.currentOption = index - - // Trigger "selected" event. - if d.selected != nil { - d.selected(d.options[d.currentOption].Text, d.currentOption) - } - if d.options[d.currentOption].Selected != nil { - d.options[d.currentOption].Selected() - } - }).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyRune { - d.prefix += string(event.Rune()) - evalPrefix() - } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 { - if len(d.prefix) > 0 { - r := []rune(d.prefix) - d.prefix = string(r[:len(r)-1]) - } - evalPrefix() - } else if event.Key() == tcell.KeyEscape { - d.open = false - d.currentOption = optionBefore - setFocus(d) - } else { - d.prefix = "" - } - return event - }) - setFocus(d.list) + d.openList(setFocus, nil) case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: if d.done != nil { d.done(key) @@ -468,6 +417,76 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr }) } +// A helper function which selects an item in the drop-down list based on +// the current prefix. +func (d *DropDown) evalPrefix() { + if len(d.prefix) > 0 { + for index, option := range d.options { + if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) { + d.list.SetCurrentItem(index) + return + } + } + // Prefix does not match any item. Remove last rune. + r := []rune(d.prefix) + d.prefix = string(r[:len(r)-1]) + } +} + +// Hand control over to the list. +func (d *DropDown) openList(setFocus func(Primitive), app *Application) { + d.open = true + optionBefore := d.currentOption + d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + // An option was selected. Close the list again. + d.currentOption = index + d.closeList(setFocus) + + // Trigger "selected" event. + if d.selected != nil { + d.selected(d.options[d.currentOption].Text, d.currentOption) + } + if d.options[d.currentOption].Selected != nil { + d.options[d.currentOption].Selected() + } + }).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyRune { + d.prefix += string(event.Rune()) + d.evalPrefix() + } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 { + if len(d.prefix) > 0 { + r := []rune(d.prefix) + d.prefix = string(r[:len(r)-1]) + } + d.evalPrefix() + } else if event.Key() == tcell.KeyEscape { + d.currentOption = optionBefore + d.closeList(setFocus) + } else { + d.prefix = "" + } + return event + }) + if app != nil { + app.SetMouseCapture(func(event EventMouse) EventMouse { + if d.open { + // Forward the mouse event to the list. + if handler := d.list.MouseHandler(); handler != nil { + handler(event) + return EventMouse{} // handled + } + } + return event + }) + } + setFocus(d.list) +} + +func (d *DropDown) closeList(setFocus func(Primitive)) { + d.open = false + setFocus(d) +} + // Focus is called by the application when the primitive receives focus. func (d *DropDown) Focus(delegate func(p Primitive)) { d.Box.Focus(delegate) @@ -489,8 +508,13 @@ func (d *DropDown) MouseHandler() func(event EventMouse) { return d.WrapMouseHandler(func(event EventMouse) { // Process mouse event. if event.Buttons()&tcell.Button1 != 0 { - //d.open = !d.open // FIXME: clicks go through the dropdown! - event.SetFocus(d) + //d.open = !d.open + //event.SetFocus(d) + if d.open { + d.closeList(event.SetFocus) + } else { + d.openList(event.SetFocus, event.Application) + } } }) } From 1d496acc2b87f58f2f8ea09d0fbe03de1ac72b7b Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 5 Nov 2019 01:38:47 +0000 Subject: [PATCH 06/24] Dismiss dropdown by click --- dropdown.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/dropdown.go b/dropdown.go index 43b7eb6..2c79f50 100644 --- a/dropdown.go +++ b/dropdown.go @@ -471,9 +471,21 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { app.SetMouseCapture(func(event EventMouse) EventMouse { if d.open { // Forward the mouse event to the list. - if handler := d.list.MouseHandler(); handler != nil { - handler(event) - return EventMouse{} // handled + atX, atY := event.Position() + x, y, w, h := d.list.GetInnerRect() + if atX >= x && atY >= y && atX < x+w && atY < y+h { + // Mouse is within the list. + if handler := d.list.MouseHandler(); handler != nil { + handler(event) + return EventMouse{} // handled + } + } else { + // Mouse not within the list. + if event.Buttons() != 0 { + // If a mouse button was pressed, cancel this capture. + app.SetMouseCapture(nil) + d.closeList(nil) // Close but don't focus. + } } } return event @@ -484,7 +496,9 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { func (d *DropDown) closeList(setFocus func(Primitive)) { d.open = false - setFocus(d) + if setFocus != nil { + setFocus(d) + } } // Focus is called by the application when the primitive receives focus. From e4d6f4f3eec86b380a38a8c9271f35f5381702ef Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 5 Nov 2019 01:43:35 +0000 Subject: [PATCH 07/24] Fix dropdown focus --- dropdown.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dropdown.go b/dropdown.go index 2c79f50..706beaf 100644 --- a/dropdown.go +++ b/dropdown.go @@ -484,7 +484,7 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { if event.Buttons() != 0 { // If a mouse button was pressed, cancel this capture. app.SetMouseCapture(nil) - d.closeList(nil) // Close but don't focus. + d.closeList(event.SetFocus) } } } @@ -496,7 +496,7 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { func (d *DropDown) closeList(setFocus func(Primitive)) { d.open = false - if setFocus != nil { + if d.list.HasFocus() { setFocus(d) } } From 93c0edc4a8bdf94c565fec0d565f55a25c20c580 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 5 Nov 2019 06:03:04 +0000 Subject: [PATCH 08/24] Get mouse actions rather than just mouse state --- application.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++--- button.go | 2 +- checkbox.go | 2 +- dropdown.go | 9 ++++++-- inputfield.go | 2 +- list.go | 2 +- 6 files changed, 64 insertions(+), 9 deletions(-) diff --git a/application.go b/application.go index 9d8a51f..bf1b59c 100644 --- a/application.go +++ b/application.go @@ -70,6 +70,10 @@ type Application struct { // event to be forwarded to the default mouse handler (nil if nothing should // be forwarded). mouseCapture func(event EventMouse) EventMouse + + lastMouseX, lastMouseY int + lastMouseBtn tcell.ButtonMask + lastMouseTarget Primitive // nil if none } // EventKey is the key input event info. @@ -77,11 +81,22 @@ type Application struct { // even though it's just an alias to *tcell.EventKey for backwards compatibility. type EventKey = *tcell.EventKey +// MouseAction are bit flags indicating what the mouse is logically doing. +type MouseAction int + +const ( + MouseDown MouseAction = 1 << iota + MouseUp + MouseClick // Button1 only. + MouseMove // The mouse position changed. +) + // EventMouse is the mouse event info. type EventMouse struct { *tcell.EventMouse Target Primitive Application *Application + Action MouseAction } // IsZero returns true if this is a zero object. @@ -304,11 +319,46 @@ EventLoop: a.draw() case *tcell.EventMouse: atX, atY := event.Position() - ptarget := a.GetPrimitiveAtPoint(atX, atY) // p under mouse. + btn := event.Buttons() + + var ptarget Primitive + if a.lastMouseBtn != 0 { + // While a button is down, the same primitive gets events. + ptarget = a.lastMouseTarget + } if ptarget == nil { - ptarget = p // Fallback to focused. + ptarget = a.GetPrimitiveAtPoint(atX, atY) // p under mouse. + if ptarget == nil { + ptarget = p // Fallback to focused. + } } - event2 := EventMouse{event, ptarget, a} + a.lastMouseTarget = ptarget + + // Calculate mouse actions. + var act MouseAction + if atX != a.lastMouseX || atY != a.lastMouseY { + act |= MouseMove + a.lastMouseX = atX + a.lastMouseY = atY + } + btnDiff := btn ^ a.lastMouseBtn + if btnDiff != 0 { + if btn&btnDiff != 0 { + act |= MouseDown + } + if a.lastMouseBtn&btnDiff != 0 { + act |= MouseUp + } + if a.lastMouseBtn == tcell.Button1 && btn == 0 { + if ptarget == a.GetPrimitiveAtPoint(atX, atY) { + // Only if Button1 and mouse up over same p. + act |= MouseClick + } + } + a.lastMouseBtn = btn + } + + event2 := EventMouse{event, ptarget, a, act} // Intercept event. if mouseCapture != nil { diff --git a/button.go b/button.go index 339f01b..d2f08ac 100644 --- a/button.go +++ b/button.go @@ -140,7 +140,7 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim func (b *Button) MouseHandler() func(event EventMouse) { return b.WrapMouseHandler(func(event EventMouse) { // Process mouse event. - if event.Buttons()&tcell.Button1 != 0 { + if event.Action&MouseClick != 0 { if b.selected != nil { b.selected() } diff --git a/checkbox.go b/checkbox.go index 5092b3d..0ce09df 100644 --- a/checkbox.go +++ b/checkbox.go @@ -206,7 +206,7 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr func (c *Checkbox) MouseHandler() func(event EventMouse) { return c.WrapMouseHandler(func(event EventMouse) { // Process mouse event. - if event.Buttons()&tcell.Button1 != 0 { + if event.Action&MouseClick != 0 { c.checked = !c.checked if c.changed != nil { c.changed(c.checked) diff --git a/dropdown.go b/dropdown.go index 706beaf..33a2dd4 100644 --- a/dropdown.go +++ b/dropdown.go @@ -476,12 +476,17 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { if atX >= x && atY >= y && atX < x+w && atY < y+h { // Mouse is within the list. if handler := d.list.MouseHandler(); handler != nil { + if event.Action&MouseUp != 0 { + // Treat mouse up as click here. + // This allows you to expand and select in one go. + event.Action |= MouseClick + } handler(event) return EventMouse{} // handled } } else { // Mouse not within the list. - if event.Buttons() != 0 { + if event.Action&MouseDown != 0 { // If a mouse button was pressed, cancel this capture. app.SetMouseCapture(nil) d.closeList(event.SetFocus) @@ -521,7 +526,7 @@ func (d *DropDown) HasFocus() bool { func (d *DropDown) MouseHandler() func(event EventMouse) { return d.WrapMouseHandler(func(event EventMouse) { // Process mouse event. - if event.Buttons()&tcell.Button1 != 0 { + if event.Action&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 { //d.open = !d.open //event.SetFocus(d) if d.open { diff --git a/inputfield.go b/inputfield.go index 992f7ab..4f8feb0 100644 --- a/inputfield.go +++ b/inputfield.go @@ -596,7 +596,7 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p func (i *InputField) MouseHandler() func(event EventMouse) { return i.WrapMouseHandler(func(event EventMouse) { // Process mouse event. - if event.Buttons()&tcell.Button1 != 0 { + if event.Action&MouseDown != 0 { event.SetFocus(i) } }) diff --git a/list.go b/list.go index 05c68d7..f118223 100644 --- a/list.go +++ b/list.go @@ -550,7 +550,7 @@ func (l *List) indexAtPoint(atX, atY int) int { func (l *List) MouseHandler() func(event EventMouse) { return l.WrapMouseHandler(func(event EventMouse) { // Process mouse event. - if event.Buttons()&tcell.Button1 != 0 { + if event.Action&MouseClick != 0 { atX, atY := event.Position() index := l.indexAtPoint(atX, atY) if index != -1 { From cd3c60e6d1ea916ff5c9a907d87013e50354fc7d Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 5 Nov 2019 09:19:40 +0000 Subject: [PATCH 09/24] Refactor EventMouse --- application.go | 43 +++++------------------------------------- box.go | 14 +++++++------- button.go | 6 +++--- checkbox.go | 6 +++--- dropdown.go | 20 +++++++++++--------- events.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ inputfield.go | 6 +++--- list.go | 6 +++--- primitive.go | 4 ++-- 9 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 events.go diff --git a/application.go b/application.go index bf1b59c..707d36d 100644 --- a/application.go +++ b/application.go @@ -69,46 +69,13 @@ type Application struct { // An optional capture function which receives a mouse event and returns the // event to be forwarded to the default mouse handler (nil if nothing should // be forwarded). - mouseCapture func(event EventMouse) EventMouse + mouseCapture func(event *EventMouse) *EventMouse lastMouseX, lastMouseY int lastMouseBtn tcell.ButtonMask lastMouseTarget Primitive // nil if none } -// EventKey is the key input event info. -// This exists for some consistency with EventMouse, -// even though it's just an alias to *tcell.EventKey for backwards compatibility. -type EventKey = *tcell.EventKey - -// MouseAction are bit flags indicating what the mouse is logically doing. -type MouseAction int - -const ( - MouseDown MouseAction = 1 << iota - MouseUp - MouseClick // Button1 only. - MouseMove // The mouse position changed. -) - -// EventMouse is the mouse event info. -type EventMouse struct { - *tcell.EventMouse - Target Primitive - Application *Application - Action MouseAction -} - -// IsZero returns true if this is a zero object. -func (e EventMouse) IsZero() bool { - return e == EventMouse{} -} - -// SetFocus will set focus to the primitive. -func (e EventMouse) SetFocus(p Primitive) { - e.Application.SetFocus(p) -} - // NewApplication creates and returns a new application. func NewApplication() *Application { return &Application{ @@ -143,14 +110,14 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event // This function can then choose to forward that event (or a // different one) by returning it or stop the event processing by returning // nil. -func (a *Application) SetMouseCapture(capture func(event EventMouse) EventMouse) *Application { +func (a *Application) SetMouseCapture(capture func(event *EventMouse) *EventMouse) *Application { a.mouseCapture = capture return a } // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. -func (a *Application) GetMouseCapture() func(event EventMouse) EventMouse { +func (a *Application) GetMouseCapture() func(event *EventMouse) *EventMouse { return a.mouseCapture } @@ -358,12 +325,12 @@ EventLoop: a.lastMouseBtn = btn } - event2 := EventMouse{event, ptarget, a, act} + event2 := NewEventMouse(event, ptarget, a, act) // Intercept event. if mouseCapture != nil { event2 = mouseCapture(event2) - if event2.IsZero() { + if event2 == nil { a.draw() continue // Don't forward event. } diff --git a/box.go b/box.go index ff11586..ef10174 100644 --- a/box.go +++ b/box.go @@ -62,7 +62,7 @@ type Box struct { // An optional capture function which receives a mouse event and returns the // event to be forwarded to the primitive's default mouse event handler (nil if // nothing should be forwarded). - mouseCapture func(event EventMouse) EventMouse + mouseCapture func(event *EventMouse) *EventMouse } // NewBox returns a Box without a border. @@ -202,19 +202,19 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { // on to the provided (default) event handler. // // This is only meant to be used by subclassing primitives. -func (b *Box) WrapMouseHandler(mouseHandler func(EventMouse)) func(EventMouse) { - return func(event EventMouse) { +func (b *Box) WrapMouseHandler(mouseHandler func(*EventMouse)) func(*EventMouse) { + return func(event *EventMouse) { if b.mouseCapture != nil { event = b.mouseCapture(event) } - if !event.IsZero() && mouseHandler != nil { + if event != nil && mouseHandler != nil { mouseHandler(event) } } } // MouseHandler returns nil. -func (b *Box) MouseHandler() func(event EventMouse) { +func (b *Box) MouseHandler() func(event *EventMouse) { return b.WrapMouseHandler(nil) } @@ -225,14 +225,14 @@ func (b *Box) MouseHandler() func(event EventMouse) { // be called. // // Providing a nil handler will remove a previously existing handler. -func (b *Box) SetMouseCapture(capture func(EventMouse) EventMouse) *Box { +func (b *Box) SetMouseCapture(capture func(*EventMouse) *EventMouse) *Box { b.mouseCapture = capture return b } // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. -func (b *Box) GetMouseCapture() func(EventMouse) EventMouse { +func (b *Box) GetMouseCapture() func(*EventMouse) *EventMouse { return b.mouseCapture } diff --git a/button.go b/button.go index d2f08ac..be2c516 100644 --- a/button.go +++ b/button.go @@ -137,10 +137,10 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim } // MouseHandler returns the mouse handler for this primitive. -func (b *Button) MouseHandler() func(event EventMouse) { - return b.WrapMouseHandler(func(event EventMouse) { +func (b *Button) MouseHandler() func(event *EventMouse) { + return b.WrapMouseHandler(func(event *EventMouse) { // Process mouse event. - if event.Action&MouseClick != 0 { + if event.Action()&MouseClick != 0 { if b.selected != nil { b.selected() } diff --git a/checkbox.go b/checkbox.go index 0ce09df..a6d6fae 100644 --- a/checkbox.go +++ b/checkbox.go @@ -203,10 +203,10 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } // MouseHandler returns the mouse handler for this primitive. -func (c *Checkbox) MouseHandler() func(event EventMouse) { - return c.WrapMouseHandler(func(event EventMouse) { +func (c *Checkbox) MouseHandler() func(event *EventMouse) { + return c.WrapMouseHandler(func(event *EventMouse) { // Process mouse event. - if event.Action&MouseClick != 0 { + if event.Action()&MouseClick != 0 { c.checked = !c.checked if c.changed != nil { c.changed(c.checked) diff --git a/dropdown.go b/dropdown.go index 33a2dd4..6542d87 100644 --- a/dropdown.go +++ b/dropdown.go @@ -468,7 +468,7 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { return event }) if app != nil { - app.SetMouseCapture(func(event EventMouse) EventMouse { + app.SetMouseCapture(func(event *EventMouse) *EventMouse { if d.open { // Forward the mouse event to the list. atX, atY := event.Position() @@ -476,17 +476,19 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { if atX >= x && atY >= y && atX < x+w && atY < y+h { // Mouse is within the list. if handler := d.list.MouseHandler(); handler != nil { - if event.Action&MouseUp != 0 { + if event.Action()&MouseUp != 0 { // Treat mouse up as click here. // This allows you to expand and select in one go. - event.Action |= MouseClick + event = NewEventMouse(event.EventMouse, + event.Target(), event.Application(), + event.Action()|MouseClick) } handler(event) - return EventMouse{} // handled + return nil // handled } } else { // Mouse not within the list. - if event.Action&MouseDown != 0 { + if event.Action()&MouseDown != 0 { // If a mouse button was pressed, cancel this capture. app.SetMouseCapture(nil) d.closeList(event.SetFocus) @@ -523,16 +525,16 @@ func (d *DropDown) HasFocus() bool { } // MouseHandler returns the mouse handler for this primitive. -func (d *DropDown) MouseHandler() func(event EventMouse) { - return d.WrapMouseHandler(func(event EventMouse) { +func (d *DropDown) MouseHandler() func(event *EventMouse) { + return d.WrapMouseHandler(func(event *EventMouse) { // Process mouse event. - if event.Action&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 { + if event.Action()&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 { //d.open = !d.open //event.SetFocus(d) if d.open { d.closeList(event.SetFocus) } else { - d.openList(event.SetFocus, event.Application) + d.openList(event.SetFocus, event.Application()) } } }) diff --git a/events.go b/events.go new file mode 100644 index 0000000..c7e66d7 --- /dev/null +++ b/events.go @@ -0,0 +1,51 @@ +package tview + +import "github.com/gdamore/tcell" + +// EventKey is the key input event info. +// This exists for some consistency with EventMouse, +// even though it's just an alias to tcell.EventKey for backwards compatibility. +type EventKey = tcell.EventKey + +// MouseAction are bit flags indicating what the mouse is logically doing. +type MouseAction int + +const ( + MouseDown MouseAction = 1 << iota + MouseUp + MouseClick // Button1 only. + MouseMove // The mouse position changed. +) + +// EventMouse is the mouse event info. +type EventMouse struct { + *tcell.EventMouse + target Primitive + app *Application + action MouseAction +} + +// Target gets the target Primitive of the mouse event. +func (e *EventMouse) Target() Primitive { + return e.target +} + +// Application gets the event originating *Application. +func (e *EventMouse) Application() *Application { + return e.app +} + +// MouseAction gets the mouse action of this event. +func (e *EventMouse) Action() MouseAction { + return e.action +} + +// SetFocus will set focus to the primitive. +func (e *EventMouse) SetFocus(p Primitive) { + e.app.SetFocus(p) +} + +// NewEventMouse creates a new mouse event. +func NewEventMouse(base *tcell.EventMouse, target Primitive, app *Application, action MouseAction) *EventMouse { + return &EventMouse{base, target, app, action} +} diff --git a/inputfield.go b/inputfield.go index 4f8feb0..485b39c 100644 --- a/inputfield.go +++ b/inputfield.go @@ -593,10 +593,10 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p } // MouseHandler returns the mouse handler for this primitive. -func (i *InputField) MouseHandler() func(event EventMouse) { - return i.WrapMouseHandler(func(event EventMouse) { +func (i *InputField) MouseHandler() func(event *EventMouse) { + return i.WrapMouseHandler(func(event *EventMouse) { // Process mouse event. - if event.Action&MouseDown != 0 { + if event.Action()&MouseDown != 0 { event.SetFocus(i) } }) diff --git a/list.go b/list.go index f118223..56180f0 100644 --- a/list.go +++ b/list.go @@ -547,10 +547,10 @@ func (l *List) indexAtPoint(atX, atY int) int { } // MouseHandler returns the mouse handler for this primitive. -func (l *List) MouseHandler() func(event EventMouse) { - return l.WrapMouseHandler(func(event EventMouse) { +func (l *List) MouseHandler() func(event *EventMouse) { + return l.WrapMouseHandler(func(event *EventMouse) { // Process mouse event. - if event.Action&MouseClick != 0 { + if event.Action()&MouseClick != 0 { atX, atY := event.Position() index := l.indexAtPoint(atX, atY) if index != -1 { diff --git a/primitive.go b/primitive.go index 6c7a6c7..60e89e1 100644 --- a/primitive.go +++ b/primitive.go @@ -50,10 +50,10 @@ type Primitive interface { // MouseHandler returns a handler which receives mouse events. // It is called by the Application class. // - // A zero value of EventMouse{} may also be returned to stop propagation. + // A value of nil may also be returned to stop propagation. // // The Box class provides functionality to intercept mouse events. If you // subclass from Box, it is recommended that you wrap your handler using // Box.WrapMouseHandler() so you inherit that functionality. - MouseHandler() func(event EventMouse) + MouseHandler() func(event *EventMouse) } From 9598ca25197aecfbe3fb91507f64725812363739 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Fri, 24 Jan 2020 20:40:34 +0000 Subject: [PATCH 10/24] New mouse design capture bool not finalized --- application.go | 100 ++++++++++++++--------------------------------- box.go | 28 +++++++------- button.go | 10 +++-- checkbox.go | 10 +++-- dropdown.go | 77 +++++++++++++++++------------------- events.go | 51 ------------------------ flex.go | 21 +++++++--- form.go | 33 ++++++++++------ frame.go | 15 ++++++- grid.go | 21 +++++++--- inputfield.go | 12 ++++-- list.go | 10 +++-- modal.go | 15 ++++++- mouse.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ pages.go | 25 +++++++----- primitive.go | 5 +-- 16 files changed, 304 insertions(+), 232 deletions(-) delete mode 100644 events.go create mode 100644 mouse.go diff --git a/application.go b/application.go index 707d36d..3671372 100644 --- a/application.go +++ b/application.go @@ -69,11 +69,12 @@ type Application struct { // An optional capture function which receives a mouse event and returns the // event to be forwarded to the default mouse handler (nil if nothing should // be forwarded). - mouseCapture func(event *EventMouse) *EventMouse + mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) - lastMouseX, lastMouseY int + lastMouseX, lastMouseY int16 // track last mouse pos + mouseDownX, mouseDownY int16 // track last mouse down pos + lastMouseAct MouseAction lastMouseBtn tcell.ButtonMask - lastMouseTarget Primitive // nil if none } // NewApplication creates and returns a new application. @@ -110,14 +111,14 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event // This function can then choose to forward that event (or a // different one) by returning it or stop the event processing by returning // nil. -func (a *Application) SetMouseCapture(capture func(event *EventMouse) *EventMouse) *Application { +func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Application { a.mouseCapture = capture return a } // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. -func (a *Application) GetMouseCapture() func(event *EventMouse) *EventMouse { +func (a *Application) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) { return a.mouseCapture } @@ -251,6 +252,7 @@ EventLoop: inputCapture := a.inputCapture mouseCapture := a.mouseCapture screen := a.screen + root := a.root a.RUnlock() switch event := event.(type) { @@ -288,56 +290,38 @@ EventLoop: atX, atY := event.Position() btn := event.Buttons() - var ptarget Primitive - if a.lastMouseBtn != 0 { - // While a button is down, the same primitive gets events. - ptarget = a.lastMouseTarget - } - if ptarget == nil { - ptarget = a.GetPrimitiveAtPoint(atX, atY) // p under mouse. - if ptarget == nil { - ptarget = p // Fallback to focused. - } - } - a.lastMouseTarget = ptarget - // Calculate mouse actions. - var act MouseAction - if atX != a.lastMouseX || atY != a.lastMouseY { - act |= MouseMove - a.lastMouseX = atX - a.lastMouseY = atY + var action MouseAction + if atX != int(a.lastMouseX) || atY != int(a.lastMouseY) { + action |= MouseMove + a.lastMouseX = int16(atX) + a.lastMouseY = int16(atY) } - btnDiff := btn ^ a.lastMouseBtn - if btnDiff != 0 { - if btn&btnDiff != 0 { - act |= MouseDown - } - if a.lastMouseBtn&btnDiff != 0 { - act |= MouseUp - } - if a.lastMouseBtn == tcell.Button1 && btn == 0 { - if ptarget == a.GetPrimitiveAtPoint(atX, atY) { - // Only if Button1 and mouse up over same p. - act |= MouseClick - } - } - a.lastMouseBtn = btn + action |= getMouseButtonAction(a.lastMouseBtn, btn) + if atX == int(a.mouseDownX) && atY == int(a.mouseDownY) { + action |= getMouseClickAction(a.lastMouseAct, action) + } + a.lastMouseAct = action + a.lastMouseBtn = btn + if action&(MouseLeftDown|MouseMiddleDown|MouseRightDown) != 0 { + a.mouseDownX = int16(atX) + a.mouseDownY = int16(atY) } - - event2 := NewEventMouse(event, ptarget, a, act) // Intercept event. if mouseCapture != nil { - event2 = mouseCapture(event2) - if event2 == nil { + event, action = mouseCapture(event, action) + if event == nil { a.draw() continue // Don't forward event. } } - if handler := ptarget.MouseHandler(); handler != nil { - handler(event2) + if handler := root.MouseHandler(); handler != nil { + //consumed, capture := + handler(event, action, func(p Primitive) { + a.SetFocus(p) + }) a.draw() } } @@ -355,34 +339,6 @@ EventLoop: return nil } -func findAtPoint(atX, atY int, p Primitive) Primitive { - x, y, w, h := p.GetRect() - if atX < x || atY < y { - return nil - } - if atX >= x+w || atY >= y+h { - return nil - } - bestp := p - for _, pchild := range p.GetChildren() { - x := findAtPoint(atX, atY, pchild) - if x != nil { - // Always overwrite if we find another one, - // this is because if any overlap, the last one is "on top". - bestp = x - } - } - return bestp -} - -// GetPrimitiveAtPoint returns the Primitive at the specified point, or nil. -// Note that this only works with a valid hierarchy of primitives (children) -func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive { - a.RLock() - defer a.RUnlock() - return findAtPoint(atX, atY, a.root) -} - // Stop stops the application, causing Run() to return. func (a *Application) Stop() { a.Lock() diff --git a/box.go b/box.go index ef10174..a2b2867 100644 --- a/box.go +++ b/box.go @@ -62,7 +62,7 @@ type Box struct { // An optional capture function which receives a mouse event and returns the // event to be forwarded to the primitive's default mouse event handler (nil if // nothing should be forwarded). - mouseCapture func(event *EventMouse) *EventMouse + mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) } // NewBox returns a Box without a border. @@ -202,19 +202,20 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { // on to the provided (default) event handler. // // This is only meant to be used by subclassing primitives. -func (b *Box) WrapMouseHandler(mouseHandler func(*EventMouse)) func(*EventMouse) { - return func(event *EventMouse) { +func (b *Box) WrapMouseHandler(mouseHandler func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool)) func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { if b.mouseCapture != nil { - event = b.mouseCapture(event) + event, action = b.mouseCapture(event, action) } if event != nil && mouseHandler != nil { - mouseHandler(event) + consumed, capture = mouseHandler(event, action, setFocus) } + return } } // MouseHandler returns nil. -func (b *Box) MouseHandler() func(event *EventMouse) { +func (b *Box) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { return b.WrapMouseHandler(nil) } @@ -225,14 +226,20 @@ func (b *Box) MouseHandler() func(event *EventMouse) { // be called. // // Providing a nil handler will remove a previously existing handler. -func (b *Box) SetMouseCapture(capture func(*EventMouse) *EventMouse) *Box { +func (b *Box) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Box { b.mouseCapture = capture return b } +func (b *Box) InRect(atX, atY int) bool { + x, y, w, h := b.GetRect() + return atX >= x && atX < x+w && + atY >= y && atY < y+h +} + // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. -func (b *Box) GetMouseCapture() func(*EventMouse) *EventMouse { +func (b *Box) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) { return b.mouseCapture } @@ -397,8 +404,3 @@ func (b *Box) HasFocus() bool { func (b *Box) GetFocusable() Focusable { return b.focus } - -// GetChildren gets the children. -func (b *Box) GetChildren() []Primitive { - return nil -} diff --git a/button.go b/button.go index be2c516..1844df6 100644 --- a/button.go +++ b/button.go @@ -137,13 +137,17 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim } // MouseHandler returns the mouse handler for this primitive. -func (b *Button) MouseHandler() func(event *EventMouse) { - return b.WrapMouseHandler(func(event *EventMouse) { +func (b *Button) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return b.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !b.InRect(event.Position()) { + return false, false + } // Process mouse event. - if event.Action()&MouseClick != 0 { + if action&MouseLeftClick != 0 { if b.selected != nil { b.selected() } } + return true, false }) } diff --git a/checkbox.go b/checkbox.go index a6d6fae..0db0f82 100644 --- a/checkbox.go +++ b/checkbox.go @@ -203,14 +203,18 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } // MouseHandler returns the mouse handler for this primitive. -func (c *Checkbox) MouseHandler() func(event *EventMouse) { - return c.WrapMouseHandler(func(event *EventMouse) { +func (c *Checkbox) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return c.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !c.InRect(event.Position()) { + return false, false + } // Process mouse event. - if event.Action()&MouseClick != 0 { + if action&MouseLeftClick != 0 { c.checked = !c.checked if c.changed != nil { c.changed(c.checked) } } + return true, false }) } diff --git a/dropdown.go b/dropdown.go index 6542d87..f920f8b 100644 --- a/dropdown.go +++ b/dropdown.go @@ -405,7 +405,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr d.evalPrefix() } - d.openList(setFocus, nil) + d.openList(setFocus) case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: if d.done != nil { d.done(key) @@ -434,7 +434,7 @@ func (d *DropDown) evalPrefix() { } // Hand control over to the list. -func (d *DropDown) openList(setFocus func(Primitive), app *Application) { +func (d *DropDown) openList(setFocus func(Primitive)) { d.open = true optionBefore := d.currentOption d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { @@ -467,37 +467,6 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) { } return event }) - if app != nil { - app.SetMouseCapture(func(event *EventMouse) *EventMouse { - if d.open { - // Forward the mouse event to the list. - atX, atY := event.Position() - x, y, w, h := d.list.GetInnerRect() - if atX >= x && atY >= y && atX < x+w && atY < y+h { - // Mouse is within the list. - if handler := d.list.MouseHandler(); handler != nil { - if event.Action()&MouseUp != 0 { - // Treat mouse up as click here. - // This allows you to expand and select in one go. - event = NewEventMouse(event.EventMouse, - event.Target(), event.Application(), - event.Action()|MouseClick) - } - handler(event) - return nil // handled - } - } else { - // Mouse not within the list. - if event.Action()&MouseDown != 0 { - // If a mouse button was pressed, cancel this capture. - app.SetMouseCapture(nil) - d.closeList(event.SetFocus) - } - } - } - return event - }) - } setFocus(d.list) } @@ -524,18 +493,42 @@ func (d *DropDown) HasFocus() bool { return d.hasFocus } +func (d *DropDown) listClick(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + atX, atY := event.Position() + x, y, w, h := d.list.GetRect() + if atX >= x && atY >= y && atX < x+w && atY < y+h { + // Mouse is within the list. + if handler := d.list.MouseHandler(); handler != nil { + // Treat mouse up as click here. + // This allows you to expand and select in one go. + return handler(event, MouseLeftUp|MouseLeftClick, setFocus) + } + return true, false + } + return false, false +} + // MouseHandler returns the mouse handler for this primitive. -func (d *DropDown) MouseHandler() func(event *EventMouse) { - return d.WrapMouseHandler(func(event *EventMouse) { +func (d *DropDown) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return d.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + inRect := d.InRect(event.Position()) + if !d.open && !inRect { + return false, false + } // Process mouse event. - if event.Action()&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 { - //d.open = !d.open - //event.SetFocus(d) - if d.open { - d.closeList(event.SetFocus) - } else { - d.openList(event.SetFocus, event.Application()) + if d.open && action&(MouseLeftDown|MouseLeftUp) != 0 { // Close it: + consumed, capture = d.listClick(event, action, setFocus) + if consumed { + d.closeList(setFocus) + return consumed, capture + } + if inRect && action&MouseLeftClick == 0 { + d.closeList(setFocus) } + } else if !d.open && inRect && action&MouseLeftDown != 0 { // Open it: + d.openList(setFocus) + return true, true // capture } + return true, false }) } diff --git a/events.go b/events.go deleted file mode 100644 index c7e66d7..0000000 --- a/events.go +++ /dev/null @@ -1,51 +0,0 @@ -package tview - -import "github.com/gdamore/tcell" - -// EventKey is the key input event info. -// This exists for some consistency with EventMouse, -// even though it's just an alias to tcell.EventKey for backwards compatibility. -type EventKey = tcell.EventKey - -// MouseAction are bit flags indicating what the mouse is logically doing. -type MouseAction int - -const ( - MouseDown MouseAction = 1 << iota - MouseUp - MouseClick // Button1 only. - MouseMove // The mouse position changed. -) - -// EventMouse is the mouse event info. -type EventMouse struct { - *tcell.EventMouse - target Primitive - app *Application - action MouseAction -} - -// Target gets the target Primitive of the mouse event. -func (e *EventMouse) Target() Primitive { - return e.target -} - -// Application gets the event originating *Application. -func (e *EventMouse) Application() *Application { - return e.app -} - -// MouseAction gets the mouse action of this event. -func (e *EventMouse) Action() MouseAction { - return e.action -} - -// SetFocus will set focus to the primitive. -func (e *EventMouse) SetFocus(p Primitive) { - e.app.SetFocus(p) -} - -// NewEventMouse creates a new mouse event. -func NewEventMouse(base *tcell.EventMouse, target Primitive, app *Application, action MouseAction) *EventMouse { - return &EventMouse{base, target, app, action} -} diff --git a/flex.go b/flex.go index 660d67d..17e3d22 100644 --- a/flex.go +++ b/flex.go @@ -196,10 +196,19 @@ func (f *Flex) HasFocus() bool { return false } -func (f *Flex) GetChildren() []Primitive { - children := make([]Primitive, len(f.items)) - for i, item := range f.items { - children[i] = item.Item - } - return children +// MouseHandler returns the mouse handler for this primitive. +func (f *Flex) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return f.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !f.InRect(event.Position()) { + return false, false + } + // Process mouse event. + for _, item := range f.items { + consumed, capture = item.Item.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + } + return true, false + }) } diff --git a/form.go b/form.go index 47051cb..d3d614c 100644 --- a/form.go +++ b/form.go @@ -601,16 +601,25 @@ func (f *Form) focusIndex() int { return -1 } -func (f *Form) GetChildren() []Primitive { - children := make([]Primitive, len(f.items)+len(f.buttons)) - i := 0 - for _, item := range f.items { - children[i] = item - i++ - } - for _, button := range f.buttons { - children[i] = button - i++ - } - return children +// MouseHandler returns the mouse handler for this primitive. +func (f *Form) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return f.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !f.InRect(event.Position()) { + return false, false + } + // Process mouse event. + for _, item := range f.items { + consumed, capture = item.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + } + for _, button := range f.buttons { + consumed, capture = button.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + } + return true, false + }) } diff --git a/frame.go b/frame.go index 99d092a..3266eb3 100644 --- a/frame.go +++ b/frame.go @@ -156,6 +156,17 @@ func (f *Frame) HasFocus() bool { return false } -func (f *Frame) GetChildren() []Primitive { - return []Primitive{f.primitive} +// MouseHandler returns the mouse handler for this primitive. +func (f *Frame) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return f.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !f.InRect(event.Position()) { + return false, false + } + // Process mouse event. + consumed, capture = f.primitive.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + return true, false + }) } diff --git a/grid.go b/grid.go index 70d0303..97e3067 100644 --- a/grid.go +++ b/grid.go @@ -661,10 +661,19 @@ func (g *Grid) Draw(screen tcell.Screen) { } } -func (g *Grid) GetChildren() []Primitive { - children := make([]Primitive, len(g.items)) - for i, item := range g.items { - children[i] = item.Item - } - return children +// MouseHandler returns the mouse handler for this primitive. +func (g *Grid) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return g.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !g.InRect(event.Position()) { + return false, false + } + // Process mouse event. + for _, item := range g.items { + consumed, capture = item.Item.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + } + return true, false + }) } diff --git a/inputfield.go b/inputfield.go index 485b39c..cc371c4 100644 --- a/inputfield.go +++ b/inputfield.go @@ -593,11 +593,15 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p } // MouseHandler returns the mouse handler for this primitive. -func (i *InputField) MouseHandler() func(event *EventMouse) { - return i.WrapMouseHandler(func(event *EventMouse) { +func (i *InputField) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return i.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !i.InRect(event.Position()) { + return false, false + } // Process mouse event. - if event.Action()&MouseDown != 0 { - event.SetFocus(i) + if action&MouseLeftDown != 0 { + setFocus(i) } + return true, false }) } diff --git a/list.go b/list.go index 56180f0..fcdc60c 100644 --- a/list.go +++ b/list.go @@ -547,10 +547,13 @@ func (l *List) indexAtPoint(atX, atY int) int { } // MouseHandler returns the mouse handler for this primitive. -func (l *List) MouseHandler() func(event *EventMouse) { - return l.WrapMouseHandler(func(event *EventMouse) { +func (l *List) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return l.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !l.InRect(event.Position()) { + return false, false + } // Process mouse event. - if event.Action()&MouseClick != 0 { + if action&MouseLeftClick != 0 { atX, atY := event.Position() index := l.indexAtPoint(atX, atY) if index != -1 { @@ -567,5 +570,6 @@ func (l *List) MouseHandler() func(event *EventMouse) { l.currentItem = index } } + return true, false }) } diff --git a/modal.go b/modal.go index c0707b1..e65537c 100644 --- a/modal.go +++ b/modal.go @@ -170,6 +170,17 @@ func (m *Modal) Draw(screen tcell.Screen) { m.frame.Draw(screen) } -func (m *Modal) GetChildren() []Primitive { - return []Primitive{m.frame} +// MouseHandler returns the mouse handler for this primitive. +func (m *Modal) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return m.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !m.InRect(event.Position()) { + return false, false + } + // Process mouse event. + consumed, capture = m.frame.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + return true, false + }) } diff --git a/mouse.go b/mouse.go new file mode 100644 index 0000000..4595c48 --- /dev/null +++ b/mouse.go @@ -0,0 +1,103 @@ +package tview + +import ( + "github.com/gdamore/tcell" +) + +// MouseAction are bit flags indicating what the mouse is logically doing. +type MouseAction int32 + +const ( + MouseMove MouseAction = 1 << iota + MouseLeftDown + MouseLeftUp + MouseLeftClick + MouseLeftDoubleClick + MouseMiddleDown + MouseMiddleUp + MouseMiddleClick + MouseMiddleDoubleClick + MouseRightDown + MouseRightUp + MouseRightClick + MouseRightDoubleClick + WheelUp + WheelDown + WheelLeft + WheelRight +) + +// Does not set MouseMove or *Click actions. +func getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { + btnDiff := btn ^ lastBtn + var action MouseAction + + if btnDiff&tcell.Button1 != 0 { + if btn&tcell.Button1 != 0 { + action |= MouseLeftDown + } else { + action |= MouseLeftUp + } + } + + if btnDiff&tcell.Button2 != 0 { + if btn&tcell.Button2 != 0 { + action |= MouseMiddleDown + } else { + action |= MouseMiddleUp + } + } + + if btnDiff&tcell.Button3 != 0 { + if btn&tcell.Button3 != 0 { + action |= MouseRightDown + } else { + action |= MouseRightUp + } + } + + if btn&tcell.WheelUp != 0 { + action |= WheelUp + } + if btn&tcell.WheelDown != 0 { + action |= WheelDown + } + if btn&tcell.WheelLeft != 0 { + action |= WheelLeft + } + if btn&tcell.WheelRight != 0 { + action |= WheelRight + } + + return action +} + +// Do not call if the mouse moved. +// Sets the *Click, including *DoubleClick. +// This should be called last, after setting all the other flags. +func getMouseClickAction(lastAct, action MouseAction) MouseAction { + if action&MouseMove == 0 { + if action&MouseLeftUp != 0 { + if lastAct&(MouseLeftClick&MouseLeftDoubleClick) == 0 { + action |= MouseLeftClick + } else if lastAct&MouseLeftDoubleClick == 0 { + action |= MouseLeftDoubleClick + } + } + if action&MouseMiddleUp != 0 { + if lastAct&(MouseMiddleClick&MouseMiddleDoubleClick) == 0 { + action |= MouseMiddleClick + } else if lastAct&MouseMiddleDoubleClick == 0 { + action |= MouseMiddleDoubleClick + } + } + if action&MouseRightUp != 0 { + if lastAct&(MouseRightClick&MouseRightDoubleClick) == 0 { + action |= MouseRightClick + } else if lastAct&MouseRightDoubleClick == 0 { + action |= MouseRightDoubleClick + } + } + } + return action +} diff --git a/pages.go b/pages.go index 35a1d9d..0d455dd 100644 --- a/pages.go +++ b/pages.go @@ -279,14 +279,21 @@ func (p *Pages) Draw(screen tcell.Screen) { } } -func (p *Pages) GetChildren() []Primitive { - var children []Primitive - for _, page := range p.pages { - // Considering invisible pages as not children. - // Even though we track all the pages, not all are "children" currently. - if page.Visible { - children = append(children, page.Item) +// MouseHandler returns the mouse handler for this primitive. +func (p *Pages) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { + return p.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { + if !p.InRect(event.Position()) { + return false, false } - } - return children + // Process mouse event. + for _, page := range p.pages { + if page.Visible { + consumed, capture = page.Item.MouseHandler()(event, action, setFocus) + if consumed { + return consumed, capture + } + } + } + return true, false + }) } diff --git a/primitive.go b/primitive.go index 60e89e1..e36efc4 100644 --- a/primitive.go +++ b/primitive.go @@ -44,9 +44,6 @@ type Primitive interface { // GetFocusable returns the item's Focusable. GetFocusable() Focusable - // GetChildren gets the children. - GetChildren() []Primitive - // MouseHandler returns a handler which receives mouse events. // It is called by the Application class. // @@ -55,5 +52,5 @@ type Primitive interface { // The Box class provides functionality to intercept mouse events. If you // subclass from Box, it is recommended that you wrap your handler using // Box.WrapMouseHandler() so you inherit that functionality. - MouseHandler() func(event *EventMouse) + MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) } From 080a8e624e24ead3ee3dd058ee156b76f317f7df Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Fri, 24 Jan 2020 23:58:34 +0000 Subject: [PATCH 11/24] Fix operator --- mouse.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mouse.go b/mouse.go index 4595c48..1871f46 100644 --- a/mouse.go +++ b/mouse.go @@ -78,21 +78,21 @@ func getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { func getMouseClickAction(lastAct, action MouseAction) MouseAction { if action&MouseMove == 0 { if action&MouseLeftUp != 0 { - if lastAct&(MouseLeftClick&MouseLeftDoubleClick) == 0 { + if lastAct&(MouseLeftClick|MouseLeftDoubleClick) == 0 { action |= MouseLeftClick } else if lastAct&MouseLeftDoubleClick == 0 { action |= MouseLeftDoubleClick } } if action&MouseMiddleUp != 0 { - if lastAct&(MouseMiddleClick&MouseMiddleDoubleClick) == 0 { + if lastAct&(MouseMiddleClick|MouseMiddleDoubleClick) == 0 { action |= MouseMiddleClick } else if lastAct&MouseMiddleDoubleClick == 0 { action |= MouseMiddleDoubleClick } } if action&MouseRightUp != 0 { - if lastAct&(MouseRightClick&MouseRightDoubleClick) == 0 { + if lastAct&(MouseRightClick|MouseRightDoubleClick) == 0 { action |= MouseRightClick } else if lastAct&MouseRightDoubleClick == 0 { action |= MouseRightDoubleClick From 8abbf770ec774243edc0bf2f839e141f975ea1c4 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Fri, 14 Feb 2020 02:09:09 +0000 Subject: [PATCH 12/24] Change mouse handler func --- application.go | 37 ++++++++++++++++++++++++++----------- box.go | 16 ++++++++-------- button.go | 8 ++++---- checkbox.go | 8 ++++---- dropdown.go | 26 ++++++++++++++++---------- flex.go | 10 +++++----- form.go | 12 ++++++------ frame.go | 10 +++++----- grid.go | 10 +++++----- inputfield.go | 8 ++++---- list.go | 8 ++++---- modal.go | 10 +++++----- mouse.go | 5 ++--- pages.go | 10 +++++----- primitive.go | 2 +- 15 files changed, 100 insertions(+), 80 deletions(-) diff --git a/application.go b/application.go index 3671372..67bd867 100644 --- a/application.go +++ b/application.go @@ -71,8 +71,11 @@ type Application struct { // be forwarded). mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) - lastMouseX, lastMouseY int16 // track last mouse pos - mouseDownX, mouseDownY int16 // track last mouse down pos + // An optional mouse capture Primitive returned from the MouseHandler. + mouseHandlerCapture Primitive + + lastMouseX, lastMouseY int // track last mouse pos + mouseDownX, mouseDownY int // track last mouse down pos lastMouseAct MouseAction lastMouseBtn tcell.ButtonMask } @@ -251,6 +254,7 @@ EventLoop: p := a.focus inputCapture := a.inputCapture mouseCapture := a.mouseCapture + mouseHandlerCapture := a.mouseHandlerCapture screen := a.screen root := a.root a.RUnlock() @@ -294,18 +298,18 @@ EventLoop: var action MouseAction if atX != int(a.lastMouseX) || atY != int(a.lastMouseY) { action |= MouseMove - a.lastMouseX = int16(atX) - a.lastMouseY = int16(atY) + a.lastMouseX = atX + a.lastMouseY = atY } - action |= getMouseButtonAction(a.lastMouseBtn, btn) + action = action.getMouseButtonAction(a.lastMouseBtn, btn) if atX == int(a.mouseDownX) && atY == int(a.mouseDownY) { - action |= getMouseClickAction(a.lastMouseAct, action) + action = action.getMouseClickAction(a.lastMouseAct) } a.lastMouseAct = action a.lastMouseBtn = btn if action&(MouseLeftDown|MouseMiddleDown|MouseRightDown) != 0 { - a.mouseDownX = int16(atX) - a.mouseDownY = int16(atY) + a.mouseDownX = atX + a.mouseDownY = atY } // Intercept event. @@ -317,13 +321,24 @@ EventLoop: } } - if handler := root.MouseHandler(); handler != nil { - //consumed, capture := - handler(event, action, func(p Primitive) { + var newHandlerCapture Primitive = nil // Clear it by default. + if mouseHandlerCapture != nil { // Check if already captured. + if handler := mouseHandlerCapture.MouseHandler(); handler != nil { + _, newHandlerCapture = handler(action, event, func(p Primitive) { + a.SetFocus(p) + }) + a.draw() + } + } else if handler := root.MouseHandler(); handler != nil { + _, newHandlerCapture = handler(action, event, func(p Primitive) { a.SetFocus(p) }) a.draw() } + + a.Lock() + a.mouseHandlerCapture = newHandlerCapture + a.Unlock() } // If we have updates, now is the time to execute them. diff --git a/box.go b/box.go index a2b2867..685ea64 100644 --- a/box.go +++ b/box.go @@ -62,7 +62,7 @@ type Box struct { // An optional capture function which receives a mouse event and returns the // event to be forwarded to the primitive's default mouse event handler (nil if // nothing should be forwarded). - mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) + mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) } // NewBox returns a Box without a border. @@ -202,20 +202,20 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { // on to the provided (default) event handler. // // This is only meant to be used by subclassing primitives. -func (b *Box) WrapMouseHandler(mouseHandler func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool)) func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if b.mouseCapture != nil { - event, action = b.mouseCapture(event, action) + action, event = b.mouseCapture(action, event) } if event != nil && mouseHandler != nil { - consumed, capture = mouseHandler(event, action, setFocus) + consumed, capture = mouseHandler(action, event, setFocus) } return } } // MouseHandler returns nil. -func (b *Box) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { +func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return b.WrapMouseHandler(nil) } @@ -226,7 +226,7 @@ func (b *Box) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primiti // be called. // // Providing a nil handler will remove a previously existing handler. -func (b *Box) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Box { +func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box { b.mouseCapture = capture return b } @@ -239,7 +239,7 @@ func (b *Box) InRect(atX, atY int) bool { // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. -func (b *Box) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) { +func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) { return b.mouseCapture } diff --git a/button.go b/button.go index 1844df6..e483c54 100644 --- a/button.go +++ b/button.go @@ -137,10 +137,10 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim } // MouseHandler returns the mouse handler for this primitive. -func (b *Button) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return b.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !b.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. if action&MouseLeftClick != 0 { @@ -148,6 +148,6 @@ func (b *Button) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Prim b.selected() } } - return true, false + return true, nil }) } diff --git a/checkbox.go b/checkbox.go index 0db0f82..181b9f9 100644 --- a/checkbox.go +++ b/checkbox.go @@ -203,10 +203,10 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } // MouseHandler returns the mouse handler for this primitive. -func (c *Checkbox) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return c.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !c.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. if action&MouseLeftClick != 0 { @@ -215,6 +215,6 @@ func (c *Checkbox) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Pr c.changed(c.checked) } } - return true, false + return true, nil }) } diff --git a/dropdown.go b/dropdown.go index f920f8b..c7e3e94 100644 --- a/dropdown.go +++ b/dropdown.go @@ -493,7 +493,7 @@ func (d *DropDown) HasFocus() bool { return d.hasFocus } -func (d *DropDown) listClick(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (d *DropDown) listClick(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { atX, atY := event.Position() x, y, w, h := d.list.GetRect() if atX >= x && atY >= y && atX < x+w && atY < y+h { @@ -501,23 +501,23 @@ func (d *DropDown) listClick(event *tcell.EventMouse, action MouseAction, setFoc if handler := d.list.MouseHandler(); handler != nil { // Treat mouse up as click here. // This allows you to expand and select in one go. - return handler(event, MouseLeftUp|MouseLeftClick, setFocus) + handler(MouseLeftUp|MouseLeftClick, event, setFocus) } - return true, false + return true, d } - return false, false + return false, nil } // MouseHandler returns the mouse handler for this primitive. -func (d *DropDown) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return d.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { inRect := d.InRect(event.Position()) if !d.open && !inRect { - return false, false + return false, nil } // Process mouse event. if d.open && action&(MouseLeftDown|MouseLeftUp) != 0 { // Close it: - consumed, capture = d.listClick(event, action, setFocus) + consumed, capture = d.listClick(action, event, setFocus) if consumed { d.closeList(setFocus) return consumed, capture @@ -527,8 +527,14 @@ func (d *DropDown) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Pr } } else if !d.open && inRect && action&MouseLeftDown != 0 { // Open it: d.openList(setFocus) - return true, true // capture + return true, d // capture + } else if d.open { + // Non-click while list is open. + if handler := d.list.MouseHandler(); handler != nil { + handler(action, event, setFocus) + } + return true, d // capture } - return true, false + return true, nil }) } diff --git a/flex.go b/flex.go index 17e3d22..18a340d 100644 --- a/flex.go +++ b/flex.go @@ -197,18 +197,18 @@ func (f *Flex) HasFocus() bool { } // MouseHandler returns the mouse handler for this primitive. -func (f *Flex) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return f.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !f.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. for _, item := range f.items { - consumed, capture = item.Item.MouseHandler()(event, action, setFocus) + consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } } - return true, false + return true, nil }) } diff --git a/form.go b/form.go index d3d614c..9ea008a 100644 --- a/form.go +++ b/form.go @@ -602,24 +602,24 @@ func (f *Form) focusIndex() int { } // MouseHandler returns the mouse handler for this primitive. -func (f *Form) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return f.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !f.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. for _, item := range f.items { - consumed, capture = item.MouseHandler()(event, action, setFocus) + consumed, capture = item.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } } for _, button := range f.buttons { - consumed, capture = button.MouseHandler()(event, action, setFocus) + consumed, capture = button.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } } - return true, false + return true, nil }) } diff --git a/frame.go b/frame.go index 3266eb3..436b4e4 100644 --- a/frame.go +++ b/frame.go @@ -157,16 +157,16 @@ func (f *Frame) HasFocus() bool { } // MouseHandler returns the mouse handler for this primitive. -func (f *Frame) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return f.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !f.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. - consumed, capture = f.primitive.MouseHandler()(event, action, setFocus) + consumed, capture = f.primitive.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } - return true, false + return true, nil }) } diff --git a/grid.go b/grid.go index 97e3067..830b3a6 100644 --- a/grid.go +++ b/grid.go @@ -662,18 +662,18 @@ func (g *Grid) Draw(screen tcell.Screen) { } // MouseHandler returns the mouse handler for this primitive. -func (g *Grid) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return g.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !g.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. for _, item := range g.items { - consumed, capture = item.Item.MouseHandler()(event, action, setFocus) + consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } } - return true, false + return true, nil }) } diff --git a/inputfield.go b/inputfield.go index cc371c4..c4cec52 100644 --- a/inputfield.go +++ b/inputfield.go @@ -593,15 +593,15 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p } // MouseHandler returns the mouse handler for this primitive. -func (i *InputField) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return i.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !i.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. if action&MouseLeftDown != 0 { setFocus(i) } - return true, false + return true, nil }) } diff --git a/list.go b/list.go index fcdc60c..0899eb2 100644 --- a/list.go +++ b/list.go @@ -547,10 +547,10 @@ func (l *List) indexAtPoint(atX, atY int) int { } // MouseHandler returns the mouse handler for this primitive. -func (l *List) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return l.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !l.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. if action&MouseLeftClick != 0 { @@ -570,6 +570,6 @@ func (l *List) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primit l.currentItem = index } } - return true, false + return true, nil }) } diff --git a/modal.go b/modal.go index e65537c..7009e15 100644 --- a/modal.go +++ b/modal.go @@ -171,16 +171,16 @@ func (m *Modal) Draw(screen tcell.Screen) { } // MouseHandler returns the mouse handler for this primitive. -func (m *Modal) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return m.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !m.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. - consumed, capture = m.frame.MouseHandler()(event, action, setFocus) + consumed, capture = m.frame.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } - return true, false + return true, nil }) } diff --git a/mouse.go b/mouse.go index 1871f46..cf7dda9 100644 --- a/mouse.go +++ b/mouse.go @@ -28,9 +28,8 @@ const ( ) // Does not set MouseMove or *Click actions. -func getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { +func (action MouseAction) getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { btnDiff := btn ^ lastBtn - var action MouseAction if btnDiff&tcell.Button1 != 0 { if btn&tcell.Button1 != 0 { @@ -75,7 +74,7 @@ func getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { // Do not call if the mouse moved. // Sets the *Click, including *DoubleClick. // This should be called last, after setting all the other flags. -func getMouseClickAction(lastAct, action MouseAction) MouseAction { +func (action MouseAction) getMouseClickAction(lastAct MouseAction) MouseAction { if action&MouseMove == 0 { if action&MouseLeftUp != 0 { if lastAct&(MouseLeftClick|MouseLeftDoubleClick) == 0 { diff --git a/pages.go b/pages.go index 0d455dd..9854658 100644 --- a/pages.go +++ b/pages.go @@ -280,20 +280,20 @@ func (p *Pages) Draw(screen tcell.Screen) { } // MouseHandler returns the mouse handler for this primitive. -func (p *Pages) MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) { - return p.WrapMouseHandler(func(event *tcell.EventMouse, action MouseAction, setFocus func(p Primitive)) (consumed, capture bool) { +func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return p.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !p.InRect(event.Position()) { - return false, false + return false, nil } // Process mouse event. for _, page := range p.pages { if page.Visible { - consumed, capture = page.Item.MouseHandler()(event, action, setFocus) + consumed, capture = page.Item.MouseHandler()(action, event, setFocus) if consumed { return consumed, capture } } } - return true, false + return true, nil }) } diff --git a/primitive.go b/primitive.go index e36efc4..e226149 100644 --- a/primitive.go +++ b/primitive.go @@ -52,5 +52,5 @@ type Primitive interface { // The Box class provides functionality to intercept mouse events. If you // subclass from Box, it is recommended that you wrap your handler using // Box.WrapMouseHandler() so you inherit that functionality. - MouseHandler() func(*tcell.EventMouse, MouseAction, func(p Primitive)) (bool, bool) + MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) } From 5ef51540da62e30205fd45bb80da98c077a7b1e2 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Fri, 14 Feb 2020 02:45:13 +0000 Subject: [PATCH 13/24] Double click interval --- application.go | 12 +++++------- mouse.go | 24 +++++++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/application.go b/application.go index 67bd867..d8b8d79 100644 --- a/application.go +++ b/application.go @@ -2,6 +2,7 @@ package tview import ( "sync" + "time" "github.com/gdamore/tcell" ) @@ -77,6 +78,7 @@ type Application struct { lastMouseX, lastMouseY int // track last mouse pos mouseDownX, mouseDownY int // track last mouse down pos lastMouseAct MouseAction + lastClickTime time.Time lastMouseBtn tcell.ButtonMask } @@ -254,7 +256,6 @@ EventLoop: p := a.focus inputCapture := a.inputCapture mouseCapture := a.mouseCapture - mouseHandlerCapture := a.mouseHandlerCapture screen := a.screen root := a.root a.RUnlock() @@ -303,7 +304,7 @@ EventLoop: } action = action.getMouseButtonAction(a.lastMouseBtn, btn) if atX == int(a.mouseDownX) && atY == int(a.mouseDownY) { - action = action.getMouseClickAction(a.lastMouseAct) + action = action.getMouseClickAction(a.lastMouseAct, &a.lastClickTime) } a.lastMouseAct = action a.lastMouseBtn = btn @@ -322,8 +323,8 @@ EventLoop: } var newHandlerCapture Primitive = nil // Clear it by default. - if mouseHandlerCapture != nil { // Check if already captured. - if handler := mouseHandlerCapture.MouseHandler(); handler != nil { + if a.mouseHandlerCapture != nil { // Check if already captured. + if handler := a.mouseHandlerCapture.MouseHandler(); handler != nil { _, newHandlerCapture = handler(action, event, func(p Primitive) { a.SetFocus(p) }) @@ -335,10 +336,7 @@ EventLoop: }) a.draw() } - - a.Lock() a.mouseHandlerCapture = newHandlerCapture - a.Unlock() } // If we have updates, now is the time to execute them. diff --git a/mouse.go b/mouse.go index cf7dda9..5a1017b 100644 --- a/mouse.go +++ b/mouse.go @@ -1,6 +1,8 @@ package tview import ( + "time" + "github.com/gdamore/tcell" ) @@ -27,6 +29,8 @@ const ( WheelRight ) +var DoubleClickInterval = 500 * time.Millisecond + // Does not set MouseMove or *Click actions. func (action MouseAction) getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { btnDiff := btn ^ lastBtn @@ -74,27 +78,33 @@ func (action MouseAction) getMouseButtonAction(lastBtn, btn tcell.ButtonMask) Mo // Do not call if the mouse moved. // Sets the *Click, including *DoubleClick. // This should be called last, after setting all the other flags. -func (action MouseAction) getMouseClickAction(lastAct MouseAction) MouseAction { +func (action MouseAction) getMouseClickAction(lastAct MouseAction, lastClickTime *time.Time) MouseAction { if action&MouseMove == 0 { if action&MouseLeftUp != 0 { - if lastAct&(MouseLeftClick|MouseLeftDoubleClick) == 0 { + if (*lastClickTime).Add(DoubleClickInterval).Before(time.Now()) { action |= MouseLeftClick - } else if lastAct&MouseLeftDoubleClick == 0 { + *lastClickTime = time.Now() + } else { action |= MouseLeftDoubleClick + *lastClickTime = time.Time{} // reset } } if action&MouseMiddleUp != 0 { - if lastAct&(MouseMiddleClick|MouseMiddleDoubleClick) == 0 { + if (*lastClickTime).Add(DoubleClickInterval).Before(time.Now()) { action |= MouseMiddleClick - } else if lastAct&MouseMiddleDoubleClick == 0 { + *lastClickTime = time.Now() + } else { action |= MouseMiddleDoubleClick + *lastClickTime = time.Time{} // reset } } if action&MouseRightUp != 0 { - if lastAct&(MouseRightClick|MouseRightDoubleClick) == 0 { + if (*lastClickTime).Add(DoubleClickInterval).Before(time.Now()) { action |= MouseRightClick - } else if lastAct&MouseRightDoubleClick == 0 { + *lastClickTime = time.Now() + } else { action |= MouseRightDoubleClick + *lastClickTime = time.Time{} // reset } } } From 0daf286122cf0a69cd699d77912bbeff98947f97 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Fri, 14 Feb 2020 03:06:14 +0000 Subject: [PATCH 14/24] Improve dropdown mouse handling --- dropdown.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dropdown.go b/dropdown.go index c7e3e94..db6bb99 100644 --- a/dropdown.go +++ b/dropdown.go @@ -494,16 +494,14 @@ func (d *DropDown) HasFocus() bool { } func (d *DropDown) listClick(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - atX, atY := event.Position() - x, y, w, h := d.list.GetRect() - if atX >= x && atY >= y && atX < x+w && atY < y+h { + if d.list.InRect(event.Position()) { // Mouse is within the list. if handler := d.list.MouseHandler(); handler != nil { // Treat mouse up as click here. // This allows you to expand and select in one go. handler(MouseLeftUp|MouseLeftClick, event, setFocus) } - return true, d + return true, d // capture } return false, nil } @@ -519,17 +517,21 @@ func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMou if d.open && action&(MouseLeftDown|MouseLeftUp) != 0 { // Close it: consumed, capture = d.listClick(action, event, setFocus) if consumed { + // The list click was processed. d.closeList(setFocus) return consumed, capture } if inRect && action&MouseLeftClick == 0 { + // Close the list if mouse down/up is not a click. + d.closeList(setFocus) + } else if !inRect && action&MouseLeftDown != 0 { + // Close the list if not in the list and mouse is down. d.closeList(setFocus) } } else if !d.open && inRect && action&MouseLeftDown != 0 { // Open it: d.openList(setFocus) return true, d // capture - } else if d.open { - // Non-click while list is open. + } else if d.open { // Non-click while list is open: if handler := d.list.MouseHandler(); handler != nil { handler(action, event, setFocus) } From 1fb9862c0046cdebbe851f755f874dbfdd4c78a3 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Fri, 28 Feb 2020 01:19:36 +0000 Subject: [PATCH 15/24] Separate action events --- application.go | 146 ++++++++++++++++++++++++++++++++++++------------- button.go | 2 +- checkbox.go | 2 +- dropdown.go | 51 +++++++---------- inputfield.go | 2 +- list.go | 2 +- mouse.go | 88 +---------------------------- 7 files changed, 136 insertions(+), 157 deletions(-) diff --git a/application.go b/application.go index d8b8d79..46e50f7 100644 --- a/application.go +++ b/application.go @@ -77,7 +77,6 @@ type Application struct { lastMouseX, lastMouseY int // track last mouse pos mouseDownX, mouseDownY int // track last mouse down pos - lastMouseAct MouseAction lastClickTime time.Time lastMouseBtn tcell.ButtonMask } @@ -292,51 +291,47 @@ EventLoop: screen.Clear() a.draw() case *tcell.EventMouse: - atX, atY := event.Position() - btn := event.Buttons() - - // Calculate mouse actions. - var action MouseAction - if atX != int(a.lastMouseX) || atY != int(a.lastMouseY) { - action |= MouseMove - a.lastMouseX = atX - a.lastMouseY = atY - } - action = action.getMouseButtonAction(a.lastMouseBtn, btn) - if atX == int(a.mouseDownX) && atY == int(a.mouseDownY) { - action = action.getMouseClickAction(a.lastMouseAct, &a.lastClickTime) - } - a.lastMouseAct = action - a.lastMouseBtn = btn - if action&(MouseLeftDown|MouseMiddleDown|MouseRightDown) != 0 { - a.mouseDownX = atX - a.mouseDownY = atY - } + isMouseDownAct := false + // Fire a mouse action. + mouseEv := func(action MouseAction) { + switch action { + case MouseLeftDown, MouseMiddleDown, MouseRightDown: + isMouseDownAct = true + } - // Intercept event. - if mouseCapture != nil { - event, action = mouseCapture(event, action) - if event == nil { - a.draw() - continue // Don't forward event. + // Intercept event. + if mouseCapture != nil { + event, action = mouseCapture(event, action) + if event == nil { + a.draw() + return // Don't forward event. + } } - } - var newHandlerCapture Primitive = nil // Clear it by default. - if a.mouseHandlerCapture != nil { // Check if already captured. - if handler := a.mouseHandlerCapture.MouseHandler(); handler != nil { + var newHandlerCapture Primitive = nil // Clear it by default. + if a.mouseHandlerCapture != nil { // Check if already captured. + if handler := a.mouseHandlerCapture.MouseHandler(); handler != nil { + _, newHandlerCapture = handler(action, event, func(p Primitive) { + a.SetFocus(p) + }) + a.draw() + } + } else if handler := root.MouseHandler(); handler != nil { _, newHandlerCapture = handler(action, event, func(p Primitive) { a.SetFocus(p) }) a.draw() } - } else if handler := root.MouseHandler(); handler != nil { - _, newHandlerCapture = handler(action, event, func(p Primitive) { - a.SetFocus(p) - }) - a.draw() + a.mouseHandlerCapture = newHandlerCapture + } + + a.fireMouseActions(event, mouseEv) + + // Keep state: + a.lastMouseBtn = event.Buttons() + if isMouseDownAct { + a.mouseDownX, a.mouseDownY = event.Position() } - a.mouseHandlerCapture = newHandlerCapture } // If we have updates, now is the time to execute them. @@ -352,6 +347,83 @@ EventLoop: return nil } +func (a *Application) fireMouseActions(event *tcell.EventMouse, mouseEv func(MouseAction)) { + atX, atY := event.Position() + btn := event.Buttons() + clickMoved := atX != int(a.mouseDownX) || atY != int(a.mouseDownY) + btnDiff := btn ^ a.lastMouseBtn + + if atX != int(a.lastMouseX) || atY != int(a.lastMouseY) { + mouseEv(MouseMove) + a.lastMouseX = atX + a.lastMouseY = atY + } + + if btnDiff&tcell.Button1 != 0 { + if btn&tcell.Button1 != 0 { + mouseEv(MouseLeftDown) + } else { + mouseEv(MouseLeftUp) + if !clickMoved { + if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { + mouseEv(MouseLeftClick) + a.lastClickTime = time.Now() + } else { + mouseEv(MouseLeftDoubleClick) + a.lastClickTime = time.Time{} // reset + } + } + } + } + + if btnDiff&tcell.Button2 != 0 { + if btn&tcell.Button2 != 0 { + mouseEv(MouseMiddleDown) + } else { + mouseEv(MouseMiddleUp) + if !clickMoved { + if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { + mouseEv(MouseMiddleClick) + a.lastClickTime = time.Now() + } else { + mouseEv(MouseMiddleDoubleClick) + a.lastClickTime = time.Time{} // reset + } + } + } + } + + if btnDiff&tcell.Button3 != 0 { + if btn&tcell.Button3 != 0 { + mouseEv(MouseRightDown) + } else { + mouseEv(MouseRightUp) + if !clickMoved { + if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { + mouseEv(MouseRightClick) + a.lastClickTime = time.Now() + } else { + mouseEv(MouseRightDoubleClick) + a.lastClickTime = time.Time{} // reset + } + } + } + } + + if btn&tcell.WheelUp != 0 { + mouseEv(WheelUp) + } + if btn&tcell.WheelDown != 0 { + mouseEv(WheelDown) + } + if btn&tcell.WheelLeft != 0 { + mouseEv(WheelLeft) + } + if btn&tcell.WheelRight != 0 { + mouseEv(WheelRight) + } +} + // Stop stops the application, causing Run() to return. func (a *Application) Stop() { a.Lock() diff --git a/button.go b/button.go index e483c54..153c814 100644 --- a/button.go +++ b/button.go @@ -143,7 +143,7 @@ func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse return false, nil } // Process mouse event. - if action&MouseLeftClick != 0 { + if action == MouseLeftClick { if b.selected != nil { b.selected() } diff --git a/checkbox.go b/checkbox.go index 181b9f9..bd8286d 100644 --- a/checkbox.go +++ b/checkbox.go @@ -209,7 +209,7 @@ func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMou return false, nil } // Process mouse event. - if action&MouseLeftClick != 0 { + if action == MouseLeftClick { c.checked = !c.checked if c.changed != nil { c.changed(c.checked) diff --git a/dropdown.go b/dropdown.go index db6bb99..d67aa84 100644 --- a/dropdown.go +++ b/dropdown.go @@ -493,50 +493,39 @@ func (d *DropDown) HasFocus() bool { return d.hasFocus } -func (d *DropDown) listClick(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if d.list.InRect(event.Position()) { - // Mouse is within the list. - if handler := d.list.MouseHandler(); handler != nil { - // Treat mouse up as click here. - // This allows you to expand and select in one go. - handler(MouseLeftUp|MouseLeftClick, event, setFocus) - } - return true, d // capture +func (d *DropDown) listClick(event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool) { + if !d.list.InRect(event.Position()) { + return false + } + // Mouse is within the list. + if handler := d.list.MouseHandler(); handler != nil { + consumed, _ := handler(MouseLeftClick, event, setFocus) + return consumed } - return false, nil + return false } // MouseHandler returns the mouse handler for this primitive. func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - inRect := d.InRect(event.Position()) + inRect := d.InRect(event.Position()) // mouse in the dropdown box itself, not the list. if !d.open && !inRect { return false, nil } // Process mouse event. - if d.open && action&(MouseLeftDown|MouseLeftUp) != 0 { // Close it: - consumed, capture = d.listClick(action, event, setFocus) - if consumed { - // The list click was processed. - d.closeList(setFocus) - return consumed, capture - } - if inRect && action&MouseLeftClick == 0 { - // Close the list if mouse down/up is not a click. - d.closeList(setFocus) - } else if !inRect && action&MouseLeftDown != 0 { - // Close the list if not in the list and mouse is down. + if action == MouseLeftClick { + if !d.open { // not open + d.openList(setFocus) + } else { // if d.open + if !inRect { + d.listClick(event, setFocus) + } d.closeList(setFocus) } - } else if !d.open && inRect && action&MouseLeftDown != 0 { // Open it: - d.openList(setFocus) - return true, d // capture - } else if d.open { // Non-click while list is open: - if handler := d.list.MouseHandler(); handler != nil { - handler(action, event, setFocus) - } + } + if d.open { return true, d // capture } - return true, nil + return inRect, nil }) } diff --git a/inputfield.go b/inputfield.go index c4cec52..856b150 100644 --- a/inputfield.go +++ b/inputfield.go @@ -599,7 +599,7 @@ func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventM return false, nil } // Process mouse event. - if action&MouseLeftDown != 0 { + if action == MouseLeftDown { setFocus(i) } return true, nil diff --git a/list.go b/list.go index 0899eb2..9156231 100644 --- a/list.go +++ b/list.go @@ -553,7 +553,7 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, return false, nil } // Process mouse event. - if action&MouseLeftClick != 0 { + if action == MouseLeftClick { atX, atY := event.Position() index := l.indexAtPoint(atX, atY) if index != -1 { diff --git a/mouse.go b/mouse.go index 5a1017b..713b24c 100644 --- a/mouse.go +++ b/mouse.go @@ -2,15 +2,13 @@ package tview import ( "time" - - "github.com/gdamore/tcell" ) -// MouseAction are bit flags indicating what the mouse is logically doing. -type MouseAction int32 +// MouseAction indicates one of the actions the mouse is logically doing. +type MouseAction int16 const ( - MouseMove MouseAction = 1 << iota + MouseMove MouseAction = iota MouseLeftDown MouseLeftUp MouseLeftClick @@ -30,83 +28,3 @@ const ( ) var DoubleClickInterval = 500 * time.Millisecond - -// Does not set MouseMove or *Click actions. -func (action MouseAction) getMouseButtonAction(lastBtn, btn tcell.ButtonMask) MouseAction { - btnDiff := btn ^ lastBtn - - if btnDiff&tcell.Button1 != 0 { - if btn&tcell.Button1 != 0 { - action |= MouseLeftDown - } else { - action |= MouseLeftUp - } - } - - if btnDiff&tcell.Button2 != 0 { - if btn&tcell.Button2 != 0 { - action |= MouseMiddleDown - } else { - action |= MouseMiddleUp - } - } - - if btnDiff&tcell.Button3 != 0 { - if btn&tcell.Button3 != 0 { - action |= MouseRightDown - } else { - action |= MouseRightUp - } - } - - if btn&tcell.WheelUp != 0 { - action |= WheelUp - } - if btn&tcell.WheelDown != 0 { - action |= WheelDown - } - if btn&tcell.WheelLeft != 0 { - action |= WheelLeft - } - if btn&tcell.WheelRight != 0 { - action |= WheelRight - } - - return action -} - -// Do not call if the mouse moved. -// Sets the *Click, including *DoubleClick. -// This should be called last, after setting all the other flags. -func (action MouseAction) getMouseClickAction(lastAct MouseAction, lastClickTime *time.Time) MouseAction { - if action&MouseMove == 0 { - if action&MouseLeftUp != 0 { - if (*lastClickTime).Add(DoubleClickInterval).Before(time.Now()) { - action |= MouseLeftClick - *lastClickTime = time.Now() - } else { - action |= MouseLeftDoubleClick - *lastClickTime = time.Time{} // reset - } - } - if action&MouseMiddleUp != 0 { - if (*lastClickTime).Add(DoubleClickInterval).Before(time.Now()) { - action |= MouseMiddleClick - *lastClickTime = time.Now() - } else { - action |= MouseMiddleDoubleClick - *lastClickTime = time.Time{} // reset - } - } - if action&MouseRightUp != 0 { - if (*lastClickTime).Add(DoubleClickInterval).Before(time.Now()) { - action |= MouseRightClick - *lastClickTime = time.Now() - } else { - action |= MouseRightDoubleClick - *lastClickTime = time.Time{} // reset - } - } - } - return action -} From 15700e712940a2f675c69b503e5a2bff0b2bf52d Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 3 Mar 2020 16:16:56 +0000 Subject: [PATCH 16/24] Minor changes --- application.go | 41 ++++++++++++++++++++++++++++++++++------- mouse.go | 30 ------------------------------ 2 files changed, 34 insertions(+), 37 deletions(-) delete mode 100644 mouse.go diff --git a/application.go b/application.go index 46e50f7..3ef4e29 100644 --- a/application.go +++ b/application.go @@ -291,12 +291,12 @@ EventLoop: screen.Clear() a.draw() case *tcell.EventMouse: - isMouseDownAct := false + isMouseDownAction := false // Fire a mouse action. mouseEv := func(action MouseAction) { switch action { case MouseLeftDown, MouseMiddleDown, MouseRightDown: - isMouseDownAct = true + isMouseDownAction = true } // Intercept event. @@ -308,8 +308,8 @@ EventLoop: } } - var newHandlerCapture Primitive = nil // Clear it by default. - if a.mouseHandlerCapture != nil { // Check if already captured. + var newHandlerCapture Primitive // None by default. + if a.mouseHandlerCapture != nil { // Check if already captured. if handler := a.mouseHandlerCapture.MouseHandler(); handler != nil { _, newHandlerCapture = handler(action, event, func(p Primitive) { a.SetFocus(p) @@ -329,7 +329,7 @@ EventLoop: // Keep state: a.lastMouseBtn = event.Buttons() - if isMouseDownAct { + if isMouseDownAction { a.mouseDownX, a.mouseDownY = event.Position() } } @@ -347,13 +347,15 @@ EventLoop: return nil } +// fireMouseActions determines each mouse action from mouse events +// and fires the appropriate mouse handlers and mouse captures. func (a *Application) fireMouseActions(event *tcell.EventMouse, mouseEv func(MouseAction)) { atX, atY := event.Position() btn := event.Buttons() - clickMoved := atX != int(a.mouseDownX) || atY != int(a.mouseDownY) + clickMoved := atX != a.mouseDownX || atY != a.mouseDownY btnDiff := btn ^ a.lastMouseBtn - if atX != int(a.lastMouseX) || atY != int(a.lastMouseY) { + if atX != a.lastMouseX || atY != a.lastMouseY { mouseEv(MouseMove) a.lastMouseX = atX a.lastMouseY = atY @@ -666,3 +668,28 @@ func (a *Application) QueueEvent(event tcell.Event) *Application { a.events <- event return a } + +// MouseAction indicates one of the actions the mouse is logically doing. +type MouseAction int16 + +const ( + MouseMove MouseAction = iota + MouseLeftDown + MouseLeftUp + MouseLeftClick + MouseLeftDoubleClick + MouseMiddleDown + MouseMiddleUp + MouseMiddleClick + MouseMiddleDoubleClick + MouseRightDown + MouseRightUp + MouseRightClick + MouseRightDoubleClick + WheelUp + WheelDown + WheelLeft + WheelRight +) + +var DoubleClickInterval = 500 * time.Millisecond diff --git a/mouse.go b/mouse.go deleted file mode 100644 index 713b24c..0000000 --- a/mouse.go +++ /dev/null @@ -1,30 +0,0 @@ -package tview - -import ( - "time" -) - -// MouseAction indicates one of the actions the mouse is logically doing. -type MouseAction int16 - -const ( - MouseMove MouseAction = iota - MouseLeftDown - MouseLeftUp - MouseLeftClick - MouseLeftDoubleClick - MouseMiddleDown - MouseMiddleUp - MouseMiddleClick - MouseMiddleDoubleClick - MouseRightDown - MouseRightUp - MouseRightClick - MouseRightDoubleClick - WheelUp - WheelDown - WheelLeft - WheelRight -) - -var DoubleClickInterval = 500 * time.Millisecond From 55cdc84e256998332426db1fccdd05bc058bee67 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 3 Mar 2020 16:24:50 +0000 Subject: [PATCH 17/24] EnableMouse to take bool and enable/disable directly --- application.go | 13 ++++++++++--- demos/list/main.go | 2 +- demos/presentation/main.go | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/application.go b/application.go index 3ef4e29..14823f3 100644 --- a/application.go +++ b/application.go @@ -155,10 +155,17 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application { } // EnableMouse enables mouse events. -func (a *Application) EnableMouse() *Application { +func (a *Application) EnableMouse(enable bool) *Application { a.Lock() - a.enableMouse = true - a.Unlock() + defer a.Unlock() + if enable != a.enableMouse && a.screen != nil { + if enable { + a.screen.EnableMouse() + } else { + a.screen.DisableMouse() + } + } + a.enableMouse = enable return a } diff --git a/demos/list/main.go b/demos/list/main.go index 3ca90eb..07afa28 100644 --- a/demos/list/main.go +++ b/demos/list/main.go @@ -15,7 +15,7 @@ func main() { AddItem("Quit", "Press to exit", 'q', func() { app.Stop() }) - app.EnableMouse() + app.EnableMouse(true) if err := app.SetRoot(list, true).Run(); err != nil { panic(err) } diff --git a/demos/presentation/main.go b/demos/presentation/main.go index f72687b..1861780 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -91,7 +91,7 @@ func main() { return event }) - app.EnableMouse() + app.EnableMouse(true) // Start the application. if err := app.SetRoot(layout, true).Run(); err != nil { From 12f94f688ab76fd4a9b5aff9246917532cd904eb Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 3 Mar 2020 17:45:32 +0000 Subject: [PATCH 18/24] Document DoubleClickInterval --- application.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application.go b/application.go index 14823f3..e983406 100644 --- a/application.go +++ b/application.go @@ -699,4 +699,6 @@ const ( WheelRight ) +// DoubleClickInterval specifies the maximum time between clicks +// to register a double click rather than click. var DoubleClickInterval = 500 * time.Millisecond From 55c590d97f11f0d94803c8802ec58b8d23268feb Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Tue, 3 Mar 2020 18:07:39 +0000 Subject: [PATCH 19/24] Refactor fireMouseActions --- application.go | 188 +++++++++++++++++++++++-------------------------- 1 file changed, 87 insertions(+), 101 deletions(-) diff --git a/application.go b/application.go index e983406..e1fbe78 100644 --- a/application.go +++ b/application.go @@ -258,16 +258,13 @@ EventLoop: break EventLoop } - a.RLock() - p := a.focus - inputCapture := a.inputCapture - mouseCapture := a.mouseCapture - screen := a.screen - root := a.root - a.RUnlock() - switch event := event.(type) { case *tcell.EventKey: + a.RLock() + p := a.focus + inputCapture := a.inputCapture + a.RUnlock() + // Intercept keys. if inputCapture != nil { event = inputCapture(event) @@ -292,48 +289,19 @@ EventLoop: } } case *tcell.EventResize: + a.RLock() + screen := a.screen + a.RUnlock() if screen == nil { continue } screen.Clear() a.draw() case *tcell.EventMouse: - isMouseDownAction := false - // Fire a mouse action. - mouseEv := func(action MouseAction) { - switch action { - case MouseLeftDown, MouseMiddleDown, MouseRightDown: - isMouseDownAction = true - } - - // Intercept event. - if mouseCapture != nil { - event, action = mouseCapture(event, action) - if event == nil { - a.draw() - return // Don't forward event. - } - } - - var newHandlerCapture Primitive // None by default. - if a.mouseHandlerCapture != nil { // Check if already captured. - if handler := a.mouseHandlerCapture.MouseHandler(); handler != nil { - _, newHandlerCapture = handler(action, event, func(p Primitive) { - a.SetFocus(p) - }) - a.draw() - } - } else if handler := root.MouseHandler(); handler != nil { - _, newHandlerCapture = handler(action, event, func(p Primitive) { - a.SetFocus(p) - }) - a.draw() - } - a.mouseHandlerCapture = newHandlerCapture + consumed, isMouseDownAction := a.fireMouseActions(event) + if consumed { + a.draw() } - - a.fireMouseActions(event, mouseEv) - // Keep state: a.lastMouseBtn = event.Buttons() if isMouseDownAction { @@ -356,81 +324,99 @@ EventLoop: // fireMouseActions determines each mouse action from mouse events // and fires the appropriate mouse handlers and mouse captures. -func (a *Application) fireMouseActions(event *tcell.EventMouse, mouseEv func(MouseAction)) { - atX, atY := event.Position() - btn := event.Buttons() - clickMoved := atX != a.mouseDownX || atY != a.mouseDownY - btnDiff := btn ^ a.lastMouseBtn +func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) { + a.RLock() + root := a.root + mouseCapture := a.mouseCapture + a.RUnlock() - if atX != a.lastMouseX || atY != a.lastMouseY { - mouseEv(MouseMove) - a.lastMouseX = atX - a.lastMouseY = atY - } + // Fire a mouse action. + mouseEv := func(action MouseAction) { + switch action { + case MouseLeftDown, MouseMiddleDown, MouseRightDown: + isMouseDownAction = true + } - if btnDiff&tcell.Button1 != 0 { - if btn&tcell.Button1 != 0 { - mouseEv(MouseLeftDown) - } else { - mouseEv(MouseLeftUp) - if !clickMoved { - if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { - mouseEv(MouseLeftClick) - a.lastClickTime = time.Now() - } else { - mouseEv(MouseLeftDoubleClick) - a.lastClickTime = time.Time{} // reset - } + // Intercept event. + if mouseCapture != nil { + event, action = mouseCapture(event, action) + if event == nil { + consumed = true + return // Don't forward event. } } - } - if btnDiff&tcell.Button2 != 0 { - if btn&tcell.Button2 != 0 { - mouseEv(MouseMiddleDown) + var handlerTarget Primitive + if a.mouseHandlerCapture != nil { // Check if already captured. + handlerTarget = a.mouseHandlerCapture } else { - mouseEv(MouseMiddleUp) - if !clickMoved { - if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { - mouseEv(MouseMiddleClick) - a.lastClickTime = time.Now() - } else { - mouseEv(MouseMiddleDoubleClick) - a.lastClickTime = time.Time{} // reset + handlerTarget = root + } + + var newHandlerCapture Primitive // None by default. + if handlerTarget != nil { + if handler := handlerTarget.MouseHandler(); handler != nil { + hconsumed := false + hconsumed, newHandlerCapture = handler(action, event, func(p Primitive) { + a.SetFocus(p) + }) + if hconsumed { + consumed = true } } } + a.mouseHandlerCapture = newHandlerCapture } - if btnDiff&tcell.Button3 != 0 { - if btn&tcell.Button3 != 0 { - mouseEv(MouseRightDown) - } else { - mouseEv(MouseRightUp) - if !clickMoved { - if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { - mouseEv(MouseRightClick) - a.lastClickTime = time.Now() - } else { - mouseEv(MouseRightDoubleClick) - a.lastClickTime = time.Time{} // reset + atX, atY := event.Position() + ebuttons := event.Buttons() + clickMoved := atX != a.mouseDownX || atY != a.mouseDownY + btnDiff := ebuttons ^ a.lastMouseBtn + + if atX != a.lastMouseX || atY != a.lastMouseY { + mouseEv(MouseMove) + a.lastMouseX = atX + a.lastMouseY = atY + } + + for _, x := range []struct { + button tcell.ButtonMask + down, up, click, dclick MouseAction + }{ + {tcell.Button1, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick}, + {tcell.Button2, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick}, + {tcell.Button3, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick}, + } { + if btnDiff&x.button != 0 { + if ebuttons&x.button != 0 { + mouseEv(x.down) + } else { + mouseEv(x.up) + if !clickMoved { + if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { + mouseEv(x.click) + a.lastClickTime = time.Now() + } else { + mouseEv(x.dclick) + a.lastClickTime = time.Time{} // reset + } } } } } - if btn&tcell.WheelUp != 0 { - mouseEv(WheelUp) - } - if btn&tcell.WheelDown != 0 { - mouseEv(WheelDown) - } - if btn&tcell.WheelLeft != 0 { - mouseEv(WheelLeft) - } - if btn&tcell.WheelRight != 0 { - mouseEv(WheelRight) + for _, x := range []struct { + button tcell.ButtonMask + action MouseAction + }{ + {tcell.WheelUp, WheelUp}, {tcell.WheelDown, WheelDown}, + {tcell.WheelLeft, WheelLeft}, {tcell.WheelRight, WheelRight}} { + if ebuttons&x.button != 0 { + mouseEv(x.action) + } } + + return consumed, isMouseDownAction } // Stop stops the application, causing Run() to return. From 9af6826328c361dc3b8ae0c4213cd541ee2a47de Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 27 Mar 2020 18:41:44 +0100 Subject: [PATCH 20/24] Added/fixed comments, some structural changes/bugfixes for mouse support. Table, TextView, and TreeView still open. Closes #363 --- application.go | 174 ++++++++++++++++++------------------- box.go | 36 +++++--- button.go | 6 +- checkbox.go | 12 ++- demos/button/main.go | 2 +- demos/checkbox/main.go | 2 +- demos/dropdown/main.go | 2 +- demos/flex/main.go | 2 +- demos/form/main.go | 2 +- demos/frame/main.go | 2 +- demos/grid/main.go | 2 +- demos/inputfield/main.go | 2 +- demos/list/main.go | 3 +- demos/modal/main.go | 2 +- demos/pages/main.go | 2 +- demos/presentation/main.go | 4 +- dropdown.go | 77 +++++++++------- flex.go | 8 +- form.go | 21 ++++- frame.go | 13 ++- grid.go | 8 +- inputfield.go | 30 +++++-- list.go | 26 +++--- modal.go | 16 ++-- pages.go | 13 +-- primitive.go | 3 +- 26 files changed, 270 insertions(+), 200 deletions(-) diff --git a/application.go b/application.go index 470ef1a..bbf51b4 100644 --- a/application.go +++ b/application.go @@ -15,6 +15,34 @@ const ( redrawPause = 50 * time.Millisecond ) +// DoubleClickInterval specifies the maximum time between clicks to register a +// double click rather than click. +var DoubleClickInterval = 500 * time.Millisecond + +// MouseAction indicates one of the actions the mouse is logically doing. +type MouseAction int16 + +// Available mouse actions. +const ( + MouseMove MouseAction = iota + MouseLeftDown + MouseLeftUp + MouseLeftClick + MouseLeftDoubleClick + MouseMiddleDown + MouseMiddleUp + MouseMiddleClick + MouseMiddleDoubleClick + MouseRightDown + MouseRightUp + MouseRightClick + MouseRightDoubleClick + WheelUp + WheelDown + WheelLeft + WheelRight +) + // queuedUpdate represented the execution of f queued by // Application.QueueUpdate(). The "done" channel receives exactly one element // after f has executed. @@ -52,7 +80,7 @@ type Application struct { // Whether or not the application resizes the root primitive. rootFullscreen bool - // Enable mouse events? + // Set to true if mouse events are enabled. enableMouse bool // An optional capture function which receives a key event and returns the @@ -85,13 +113,11 @@ type Application struct { // be forwarded). mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) - // An optional mouse capture Primitive returned from the MouseHandler. - mouseHandlerCapture Primitive - - lastMouseX, lastMouseY int // track last mouse pos - mouseDownX, mouseDownY int // track last mouse down pos - lastClickTime time.Time - lastMouseBtn tcell.ButtonMask + mouseCapturingPrimitive Primitive // A Primitive returned by a MouseHandler which will capture future mouse events. + lastMouseX, lastMouseY int // The last position of the mouse. + mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed. + lastMouseClick time.Time // The time when a mouse button was last clicked. + lastMouseButtons tcell.ButtonMask // The last mouse button state. } // NewApplication creates and returns a new application. @@ -123,11 +149,11 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event return a.inputCapture } -// SetMouseCapture sets a function which captures mouse events before they are -// forwarded to the appropriate mouse event handler. -// This function can then choose to forward that event (or a -// different one) by returning it or stop the event processing by returning -// nil. +// SetMouseCapture sets a function which captures mouse events (consisting of +// the original tcell mouse event and the semantic mouse action) before they are +// forwarded to the appropriate mouse event handler. This function can then +// choose to forward that event (or a different one) by returning it or stop +// the event processing by returning a nil mouse event. func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Application { a.mouseCapture = capture return a @@ -334,8 +360,7 @@ EventLoop: if consumed { a.draw() } - // Keep state: - a.lastMouseBtn = event.Buttons() + a.lastMouseButtons = event.Buttons() if isMouseDownAction { a.mouseDownX, a.mouseDownY = event.Position() } @@ -355,64 +380,64 @@ EventLoop: return nil } -// fireMouseActions determines each mouse action from mouse events -// and fires the appropriate mouse handlers and mouse captures. +// fireMouseActions analyzes the provided mouse event, derives mouse actions +// from it and then forwards them to the corresponding primitives. func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) { - a.RLock() - root := a.root - mouseCapture := a.mouseCapture - a.RUnlock() + // We want to relay follow-up events to the same target primitive. + var targetPrimitive Primitive - // Fire a mouse action. - mouseEv := func(action MouseAction) { + // Helper function to fire a mouse action. + fire := func(action MouseAction) { switch action { case MouseLeftDown, MouseMiddleDown, MouseRightDown: isMouseDownAction = true } // Intercept event. - if mouseCapture != nil { - event, action = mouseCapture(event, action) + if a.mouseCapture != nil { + event, action = a.mouseCapture(event, action) if event == nil { consumed = true return // Don't forward event. } } - var handlerTarget Primitive - if a.mouseHandlerCapture != nil { // Check if already captured. - handlerTarget = a.mouseHandlerCapture + // Determine the target primitive. + var primitive, capturingPrimitive Primitive + if a.mouseCapturingPrimitive != nil { + primitive = a.mouseCapturingPrimitive + targetPrimitive = a.mouseCapturingPrimitive + } else if targetPrimitive != nil { + primitive = targetPrimitive } else { - handlerTarget = root + primitive = a.root } - - var newHandlerCapture Primitive // None by default. - if handlerTarget != nil { - if handler := handlerTarget.MouseHandler(); handler != nil { - hconsumed := false - hconsumed, newHandlerCapture = handler(action, event, func(p Primitive) { + if primitive != nil { + if handler := primitive.MouseHandler(); handler != nil { + var wasConsumed bool + wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) { a.SetFocus(p) }) - if hconsumed { + if wasConsumed { consumed = true } } } - a.mouseHandlerCapture = newHandlerCapture + a.mouseCapturingPrimitive = capturingPrimitive } - atX, atY := event.Position() - ebuttons := event.Buttons() - clickMoved := atX != a.mouseDownX || atY != a.mouseDownY - btnDiff := ebuttons ^ a.lastMouseBtn + x, y := event.Position() + buttons := event.Buttons() + clickMoved := x != a.mouseDownX || y != a.mouseDownY + buttonChanges := buttons ^ a.lastMouseButtons - if atX != a.lastMouseX || atY != a.lastMouseY { - mouseEv(MouseMove) - a.lastMouseX = atX - a.lastMouseY = atY + if x != a.lastMouseX || y != a.lastMouseY { + fire(MouseMove) + a.lastMouseX = x + a.lastMouseY = y } - for _, x := range []struct { + for _, buttonEvent := range []struct { button tcell.ButtonMask down, up, click, dclick MouseAction }{ @@ -420,32 +445,34 @@ func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMou {tcell.Button2, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick}, {tcell.Button3, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick}, } { - if btnDiff&x.button != 0 { - if ebuttons&x.button != 0 { - mouseEv(x.down) + if buttonChanges&buttonEvent.button != 0 { + if buttons&buttonEvent.button != 0 { + fire(buttonEvent.down) } else { - mouseEv(x.up) + fire(buttonEvent.up) if !clickMoved { - if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { - mouseEv(x.click) - a.lastClickTime = time.Now() + if a.lastMouseClick.Add(DoubleClickInterval).Before(time.Now()) { + fire(buttonEvent.click) + a.lastMouseClick = time.Now() } else { - mouseEv(x.dclick) - a.lastClickTime = time.Time{} // reset + fire(buttonEvent.dclick) + a.lastMouseClick = time.Time{} // reset } } } } } - for _, x := range []struct { + for _, wheelEvent := range []struct { button tcell.ButtonMask action MouseAction }{ - {tcell.WheelUp, WheelUp}, {tcell.WheelDown, WheelDown}, - {tcell.WheelLeft, WheelLeft}, {tcell.WheelRight, WheelRight}} { - if ebuttons&x.button != 0 { - mouseEv(x.action) + {tcell.WheelUp, WheelUp}, + {tcell.WheelDown, WheelDown}, + {tcell.WheelLeft, WheelLeft}, + {tcell.WheelRight, WheelRight}} { + if buttons&wheelEvent.button != 0 { + fire(wheelEvent.action) } } @@ -699,30 +726,3 @@ func (a *Application) QueueEvent(event tcell.Event) *Application { a.events <- event return a } - -// MouseAction indicates one of the actions the mouse is logically doing. -type MouseAction int16 - -const ( - MouseMove MouseAction = iota - MouseLeftDown - MouseLeftUp - MouseLeftClick - MouseLeftDoubleClick - MouseMiddleDown - MouseMiddleUp - MouseMiddleClick - MouseMiddleDoubleClick - MouseRightDown - MouseRightUp - MouseRightClick - MouseRightDoubleClick - WheelUp - WheelDown - WheelLeft - WheelRight -) - -// DoubleClickInterval specifies the maximum time between clicks -// to register a double click rather than click. -var DoubleClickInterval = 500 * time.Millisecond diff --git a/box.go b/box.go index cede8a6..1870597 100644 --- a/box.go +++ b/box.go @@ -61,8 +61,8 @@ type Box struct { draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) // An optional capture function which receives a mouse event and returns the - // event to be forwarded to the primitive's default mouse event handler (nil if - // nothing should be forwarded). + // event to be forwarded to the primitive's default mouse event handler (at + // least one nil if nothing should be forwarded). mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) } @@ -199,8 +199,8 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { } // WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the -// functionality to capture input (see SetMouseCapture()) before passing it -// on to the provided (default) event handler. +// functionality to capture mouse events (see SetMouseCapture()) before passing +// them on to the provided (default) event handler. // // This is only meant to be used by subclassing primitives. func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { @@ -217,14 +217,21 @@ func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, // MouseHandler returns nil. func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - return b.WrapMouseHandler(nil) + return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + if action == MouseLeftClick && b.InRect(event.Position()) { + setFocus(b) + consumed = true + } + return + }) } -// SetMouseCapture installs a function which captures events before they are -// forwarded to the primitive's default event handler. This function can -// then choose to forward that event (or a different one) to the default -// handler by returning it. If nil is returned, the default handler will not -// be called. +// SetMouseCapture sets a function which captures mouse events (consisting of +// the original tcell mouse event and the semantic mouse action) before they are +// forwarded to the primitive's default mouse event handler. This function can +// then choose to forward that event (or a different one) by returning it or +// returning a nil mouse event, in which case the default handler will not be +// called. // // Providing a nil handler will remove a previously existing handler. func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box { @@ -232,10 +239,11 @@ func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.Even return b } -func (b *Box) InRect(atX, atY int) bool { - x, y, w, h := b.GetRect() - return atX >= x && atX < x+w && - atY >= y && atY < y+h +// InRect returns true if the given coordinate is within the bounds of the box's +// rectangle. +func (b *Box) InRect(x, y int) bool { + rectX, rectY, width, height := b.GetRect() + return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height } // GetMouseCapture returns the function installed with SetMouseCapture() or nil diff --git a/button.go b/button.go index 153c814..fd7c234 100644 --- a/button.go +++ b/button.go @@ -142,12 +142,16 @@ func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse if !b.InRect(event.Position()) { return false, nil } + // Process mouse event. if action == MouseLeftClick { + setFocus(b) if b.selected != nil { b.selected() } + consumed = true } - return true, nil + + return }) } diff --git a/checkbox.go b/checkbox.go index bd8286d..7c4b505 100644 --- a/checkbox.go +++ b/checkbox.go @@ -205,16 +205,22 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // MouseHandler returns the mouse handler for this primitive. func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if !c.InRect(event.Position()) { + x, y := event.Position() + _, rectY, _, _ := c.GetInnerRect() + if !c.InRect(x, y) { return false, nil } + // Process mouse event. - if action == MouseLeftClick { + if action == MouseLeftClick && y == rectY { + setFocus(c) c.checked = !c.checked if c.changed != nil { c.changed(c.checked) } + consumed = true } - return true, nil + + return }) } diff --git a/demos/button/main.go b/demos/button/main.go index 429b5be..10e52f9 100644 --- a/demos/button/main.go +++ b/demos/button/main.go @@ -9,7 +9,7 @@ func main() { app.Stop() }) button.SetBorder(true).SetRect(0, 0, 22, 3) - if err := app.SetRoot(button, false).Run(); err != nil { + if err := app.SetRoot(button, false).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/checkbox/main.go b/demos/checkbox/main.go index ef9ef8e..0a6bc70 100644 --- a/demos/checkbox/main.go +++ b/demos/checkbox/main.go @@ -6,7 +6,7 @@ import "github.com/rivo/tview" func main() { app := tview.NewApplication() checkbox := tview.NewCheckbox().SetLabel("Hit Enter to check box: ") - if err := app.SetRoot(checkbox, true).Run(); err != nil { + if err := app.SetRoot(checkbox, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/dropdown/main.go b/demos/dropdown/main.go index be9db66..3d9510b 100644 --- a/demos/dropdown/main.go +++ b/demos/dropdown/main.go @@ -8,7 +8,7 @@ func main() { dropdown := tview.NewDropDown(). SetLabel("Select an option (hit Enter): "). SetOptions([]string{"First", "Second", "Third", "Fourth", "Fifth"}, nil) - if err := app.SetRoot(dropdown, true).Run(); err != nil { + if err := app.SetRoot(dropdown, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/flex/main.go b/demos/flex/main.go index 1c22172..2df2466 100644 --- a/demos/flex/main.go +++ b/demos/flex/main.go @@ -14,7 +14,7 @@ func main() { AddItem(tview.NewBox().SetBorder(true).SetTitle("Middle (3 x height of Top)"), 0, 3, false). AddItem(tview.NewBox().SetBorder(true).SetTitle("Bottom (5 rows)"), 5, 1, false), 0, 2, false). AddItem(tview.NewBox().SetBorder(true).SetTitle("Right (20 cols)"), 20, 1, false) - if err := app.SetRoot(flex, true).Run(); err != nil { + if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/form/main.go b/demos/form/main.go index e64614f..1b50c13 100644 --- a/demos/form/main.go +++ b/demos/form/main.go @@ -18,7 +18,7 @@ func main() { app.Stop() }) form.SetBorder(true).SetTitle("Enter some data").SetTitleAlign(tview.AlignLeft) - if err := app.SetRoot(form, true).Run(); err != nil { + if err := app.SetRoot(form, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/frame/main.go b/demos/frame/main.go index 9b03e64..db6e184 100644 --- a/demos/frame/main.go +++ b/demos/frame/main.go @@ -16,7 +16,7 @@ func main() { AddText("Header second middle", true, tview.AlignCenter, tcell.ColorRed). AddText("Footer middle", false, tview.AlignCenter, tcell.ColorGreen). AddText("Footer second middle", false, tview.AlignCenter, tcell.ColorGreen) - if err := app.SetRoot(frame, true).Run(); err != nil { + if err := app.SetRoot(frame, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/grid/main.go b/demos/grid/main.go index cbdb1df..57efe1e 100644 --- a/demos/grid/main.go +++ b/demos/grid/main.go @@ -32,7 +32,7 @@ func main() { AddItem(main, 1, 1, 1, 1, 0, 100, false). AddItem(sideBar, 1, 2, 1, 1, 0, 100, false) - if err := tview.NewApplication().SetRoot(grid, true).Run(); err != nil { + if err := tview.NewApplication().SetRoot(grid, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/inputfield/main.go b/demos/inputfield/main.go index b5ccd77..923bf2d 100644 --- a/demos/inputfield/main.go +++ b/demos/inputfield/main.go @@ -16,7 +16,7 @@ func main() { SetDoneFunc(func(key tcell.Key) { app.Stop() }) - if err := app.SetRoot(inputField, true).Run(); err != nil { + if err := app.SetRoot(inputField, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/list/main.go b/demos/list/main.go index 07afa28..402703a 100644 --- a/demos/list/main.go +++ b/demos/list/main.go @@ -15,8 +15,7 @@ func main() { AddItem("Quit", "Press to exit", 'q', func() { app.Stop() }) - app.EnableMouse(true) - if err := app.SetRoot(list, true).Run(); err != nil { + if err := app.SetRoot(list, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/modal/main.go b/demos/modal/main.go index 893230d..e7ef1f8 100644 --- a/demos/modal/main.go +++ b/demos/modal/main.go @@ -15,7 +15,7 @@ func main() { app.Stop() } }) - if err := app.SetRoot(modal, false).Run(); err != nil { + if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/pages/main.go b/demos/pages/main.go index 8eb67ab..67c06e0 100644 --- a/demos/pages/main.go +++ b/demos/pages/main.go @@ -29,7 +29,7 @@ func main() { page == 0) }(page) } - if err := app.SetRoot(pages, true).Run(); err != nil { + if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/presentation/main.go b/demos/presentation/main.go index 1861780..7ae2eb9 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -91,10 +91,8 @@ func main() { return event }) - app.EnableMouse(true) - // Start the application. - if err := app.SetRoot(layout, true).Run(); err != nil { + if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/dropdown.go b/dropdown.go index d67aa84..12d04ed 100644 --- a/dropdown.go +++ b/dropdown.go @@ -79,6 +79,8 @@ type DropDown struct { // A callback function which is called when the user changes the drop-down's // selection. selected func(text string, index int) + + dragging bool // Set to true when mouse dragging is in progress. } // NewDropDown returns a new drop-down. @@ -417,8 +419,8 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr }) } -// A helper function which selects an item in the drop-down list based on -// the current prefix. +// evalPrefix is selects an item in the drop-down list based on the current +// prefix. func (d *DropDown) evalPrefix() { if len(d.prefix) > 0 { for index, option := range d.options { @@ -427,17 +429,23 @@ func (d *DropDown) evalPrefix() { return } } - // Prefix does not match any item. Remove last rune. + + // Prefix does not match any item. Remove last rune. TODO: Use uniseg here. r := []rune(d.prefix) d.prefix = string(r[:len(r)-1]) } } -// Hand control over to the list. +// openList hands control over to the embedded List primitive. func (d *DropDown) openList(setFocus func(Primitive)) { d.open = true optionBefore := d.currentOption + d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + if d.dragging { + return // If we're dragging the mouse, we don't want to trigger any events. + } + // An option was selected. Close the list again. d.currentOption = index d.closeList(setFocus) @@ -465,11 +473,15 @@ func (d *DropDown) openList(setFocus func(Primitive)) { } else { d.prefix = "" } + return event }) + setFocus(d.list) } +// closeList closes the embedded List element by hiding it and removing focus +// from it. func (d *DropDown) closeList(setFocus func(Primitive)) { d.open = false if d.list.HasFocus() { @@ -493,39 +505,44 @@ func (d *DropDown) HasFocus() bool { return d.hasFocus } -func (d *DropDown) listClick(event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool) { - if !d.list.InRect(event.Position()) { - return false - } - // Mouse is within the list. - if handler := d.list.MouseHandler(); handler != nil { - consumed, _ := handler(MouseLeftClick, event, setFocus) - return consumed - } - return false -} - // MouseHandler returns the mouse handler for this primitive. func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - inRect := d.InRect(event.Position()) // mouse in the dropdown box itself, not the list. + // Was the mouse event in the drop-down box itself (or on its label)? + x, y := event.Position() + _, rectY, _, _ := d.GetInnerRect() + inRect := y == rectY if !d.open && !inRect { - return false, nil + return d.InRect(x, y), nil // No, and it's not expanded either. Ignore. } - // Process mouse event. - if action == MouseLeftClick { - if !d.open { // not open + + // Handle dragging. Clicks are implicitly handled by this logic. + switch action { + case MouseLeftDown: + consumed = d.open || inRect + capture = d + if !d.open { d.openList(setFocus) - } else { // if d.open - if !inRect { - d.listClick(event, setFocus) - } - d.closeList(setFocus) + d.dragging = true + } else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed { + d.closeList(setFocus) // Close drop-down if clicked outside of it. + } + case MouseMove: + if d.dragging { + // We pretend it's a left click so we can see the selection during + // dragging. Because we don't act upon it, it's not a problem. + d.list.MouseHandler()(MouseLeftClick, event, setFocus) + consumed = true + capture = d + } + case MouseLeftUp: + if d.dragging { + d.dragging = false + d.list.MouseHandler()(MouseLeftClick, event, setFocus) + consumed = true } } - if d.open { - return true, d // capture - } - return inRect, nil + + return }) } diff --git a/flex.go b/flex.go index e991b4f..108b5b0 100644 --- a/flex.go +++ b/flex.go @@ -208,13 +208,15 @@ func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !f.InRect(event.Position()) { return false, nil } - // Process mouse event. + + // Pass mouse events along to the first child item that takes it. for _, item := range f.items { consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + return } } - return true, nil + + return }) } diff --git a/form.go b/form.go index 6bdd6bc..ca123bf 100644 --- a/form.go +++ b/form.go @@ -626,19 +626,32 @@ func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !f.InRect(event.Position()) { return false, nil } - // Process mouse event. + + // Determine items to pass mouse events to. for _, item := range f.items { consumed, capture = item.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + return } } for _, button := range f.buttons { consumed, capture = button.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + return + } + } + + // A mouse click anywhere else will return the focus to the last selected + // element. + if action == MouseLeftClick { + if f.focusedElement < len(f.items) { + setFocus(f.items[f.focusedElement]) + } else if f.focusedElement < len(f.items)+len(f.buttons) { + setFocus(f.buttons[f.focusedElement-len(f.items)]) } + consumed = true } - return true, nil + + return }) } diff --git a/frame.go b/frame.go index 436b4e4..1ea9d6e 100644 --- a/frame.go +++ b/frame.go @@ -12,8 +12,8 @@ type frameText struct { Color tcell.Color // The text color. } -// Frame is a wrapper which adds a border around another primitive. The top area -// (header) and the bottom area (footer) may also contain text. +// Frame is a wrapper which adds space around another primitive. In addition, +// the top area (header) and the bottom area (footer) may also contain text. // // See https://github.com/rivo/tview/wiki/Frame for an example. type Frame struct { @@ -162,11 +162,8 @@ func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !f.InRect(event.Position()) { return false, nil } - // Process mouse event. - consumed, capture = f.primitive.MouseHandler()(action, event, setFocus) - if consumed { - return consumed, capture - } - return true, nil + + // Pass mouse events on to contained primitive. + return f.primitive.MouseHandler()(action, event, setFocus) }) } diff --git a/grid.go b/grid.go index 5f24e4f..b814d35 100644 --- a/grid.go +++ b/grid.go @@ -667,13 +667,15 @@ func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !g.InRect(event.Position()) { return false, nil } - // Process mouse event. + + // Pass mouse events along to the first child item that takes it. for _, item := range g.items { consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + return } } - return true, nil + + return }) } diff --git a/inputfield.go b/inputfield.go index 856b150..784a06b 100644 --- a/inputfield.go +++ b/inputfield.go @@ -69,9 +69,6 @@ type InputField struct { // The cursor position as a byte index into the text string. cursorPos int - // The number of bytes of the text string skipped ahead while drawing. - offset int - // An optional autocomplete function which receives the current text of the // input field and returns a slice of strings to be displayed in a drop-down // selection. @@ -96,6 +93,9 @@ type InputField struct { // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) + + fieldX int // The x-coordinate of the input field as determined during the last call to Draw(). + offset int // The number of bytes of the text string skipped ahead while drawing. } // NewInputField returns a new input field. @@ -326,6 +326,7 @@ func (i *InputField) Draw(screen tcell.Screen) { } // Draw input area. + i.fieldX = x fieldWidth := i.fieldWidth if fieldWidth == 0 { fieldWidth = math.MaxInt32 @@ -595,13 +596,30 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p // MouseHandler returns the mouse handler for this primitive. func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if !i.InRect(event.Position()) { + x, y := event.Position() + _, rectY, _, _ := i.GetInnerRect() + if !i.InRect(x, y) { return false, nil } + // Process mouse event. - if action == MouseLeftDown { + if action == MouseLeftClick && y == rectY { + // Determine where to place the cursor. + if x >= i.fieldX { + if !iterateString(i.text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool { + if x-i.fieldX < screenPos+screenWidth { + i.cursorPos = textPos + return true + } + return false + }) { + i.cursorPos = len(i.text) + } + } setFocus(i) + consumed = true } - return true, nil + + return }) } diff --git a/list.go b/list.go index 600dc72..474477b 100644 --- a/list.go +++ b/list.go @@ -559,22 +559,23 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit }) } -// returns -1 if not found. -func (l *List) indexAtPoint(atX, atY int) int { - _, y, _, h := l.GetInnerRect() - if atY < y || atY >= y+h { +// indexAtPoint returns the index of the list item found at the given position +// or -1 if there is no such list item. +func (l *List) indexAtPoint(x, y int) int { + rectX, rectY, width, height := l.GetInnerRect() + if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height { return -1 } - n := atY - y + index := y - rectY if l.showSecondaryText { - n /= 2 + index /= 2 } - if n >= len(l.items) { + if index >= len(l.items) { return -1 } - return n + return index } // MouseHandler returns the mouse handler for this primitive. @@ -583,10 +584,11 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !l.InRect(event.Position()) { return false, nil } + // Process mouse event. if action == MouseLeftClick { - atX, atY := event.Position() - index := l.indexAtPoint(atX, atY) + setFocus(l) + index := l.indexAtPoint(event.Position()) if index != -1 { item := l.items[index] if item.Selected != nil { @@ -600,7 +602,9 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } l.currentItem = index } + consumed = true } - return true, nil + + return }) } diff --git a/modal.go b/modal.go index 4256cdf..f534113 100644 --- a/modal.go +++ b/modal.go @@ -12,7 +12,7 @@ import ( type Modal struct { *Box - // The framed embedded in the modal. + // The frame embedded in the modal. frame *Frame // The form embedded in the modal's frame. @@ -179,14 +179,12 @@ func (m *Modal) Draw(screen tcell.Screen) { // MouseHandler returns the mouse handler for this primitive. func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if !m.InRect(event.Position()) { - return false, nil + // Pass mouse events on to the form. + consumed, capture = m.form.MouseHandler()(action, event, setFocus) + if !consumed && action == MouseLeftClick && m.InRect(event.Position()) { + setFocus(m) + consumed = true } - // Process mouse event. - consumed, capture = m.frame.MouseHandler()(action, event, setFocus) - if consumed { - return consumed, capture - } - return true, nil + return }) } diff --git a/pages.go b/pages.go index 9854658..0955045 100644 --- a/pages.go +++ b/pages.go @@ -20,7 +20,7 @@ type page struct { type Pages struct { *Box - // The contained pages. + // The contained pages. (Visible) pages are drawn from back to front. pages []*page // We keep a reference to the function which allows us to set the focus to @@ -285,15 +285,18 @@ func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !p.InRect(event.Position()) { return false, nil } - // Process mouse event. - for _, page := range p.pages { + + // Pass mouse events along to the last visible page item that takes it. + for index := len(p.pages) - 1; index >= 0; index-- { + page := p.pages[index] if page.Visible { consumed, capture = page.Item.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + return } } } - return true, nil + + return }) } diff --git a/primitive.go b/primitive.go index e226149..416d708 100644 --- a/primitive.go +++ b/primitive.go @@ -47,7 +47,8 @@ type Primitive interface { // MouseHandler returns a handler which receives mouse events. // It is called by the Application class. // - // A value of nil may also be returned to stop propagation. + // A value of nil may also be returned to stop the downward propagation of + // mouse events. // // The Box class provides functionality to intercept mouse events. If you // subclass from Box, it is recommended that you wrap your handler using From 5f5b79b00ecadbc4755705f15a2230793436dda2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 27 Mar 2020 21:13:03 +0100 Subject: [PATCH 21/24] Added mouse support for Table and TreeView. --- README.md | 2 +- demos/table/main.go | 2 +- demos/treeview/main.go | 2 +- dropdown.go | 5 ++- table.go | 82 +++++++++++++++++++++++++++++++++++++++--- treeview.go | 32 +++++++++++++++++ 6 files changed, 115 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ddceec7..8ce62bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Rich Interactive Widgets for Terminal UIs -[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/rivo/tview) +[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/rivo/tview) [![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/tview) This Go package provides commonly needed components for terminal based user interfaces. diff --git a/demos/table/main.go b/demos/table/main.go index 38cb0c1..906ed56 100644 --- a/demos/table/main.go +++ b/demos/table/main.go @@ -39,7 +39,7 @@ func main() { table.GetCell(row, column).SetTextColor(tcell.ColorRed) table.SetSelectable(false, false) }) - if err := app.SetRoot(table, true).Run(); err != nil { + if err := app.SetRoot(table, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/treeview/main.go b/demos/treeview/main.go index bce3cba..5a6805d 100644 --- a/demos/treeview/main.go +++ b/demos/treeview/main.go @@ -56,7 +56,7 @@ func main() { } }) - if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + if err := tview.NewApplication().SetRoot(tree, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/dropdown.go b/dropdown.go index 12d04ed..89d50c1 100644 --- a/dropdown.go +++ b/dropdown.go @@ -419,8 +419,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr }) } -// evalPrefix is selects an item in the drop-down list based on the current -// prefix. +// evalPrefix selects an item in the drop-down list based on the current prefix. func (d *DropDown) evalPrefix() { if len(d.prefix) > 0 { for index, option := range d.options { @@ -430,7 +429,7 @@ func (d *DropDown) evalPrefix() { } } - // Prefix does not match any item. Remove last rune. TODO: Use uniseg here. + // Prefix does not match any item. Remove last rune. r := []rune(d.prefix) d.prefix = string(r[:len(r)-1]) } diff --git a/table.go b/table.go index 04fe448..05923b7 100644 --- a/table.go +++ b/table.go @@ -251,6 +251,13 @@ type Table struct { // The number of visible rows the last time the table was drawn. visibleRows int + // The indices of the visible columns as of the last time the table was drawn. + visibleColumnIndices []int + + // The net widths of the visible columns as of the last time the table was + // drawn. + visibleColumnWidths []int + // The style of the selected rows. If this value is 0, selected rows are // simply inverted. selectedStyle tcell.Style @@ -360,8 +367,8 @@ func (t *Table) GetSelection() (row, column int) { // Select sets the selected cell. Depending on the selection settings // specified via SetSelectable(), this may be an entire row or column, or even // ignored completely. The "selection changed" event is fired if such a callback -// is available (even if the selection ends up being the same as before, even if -// cells are not selectable). +// is available (even if the selection ends up being the same as before and even +// if cells are not selectable). func (t *Table) Select(row, column int) *Table { t.selectedRow, t.selectedColumn = row, column if t.selectionChanged != nil { @@ -537,6 +544,49 @@ func (t *Table) GetColumnCount() int { return t.lastColumn + 1 } +// cellAt returns the row and column located at the given screen coordinates. +// Each returned value may be negative if there is no row and/or cell. This +// function will also process coordinates outside the table's inner rectangle so +// callers will need to check for bounds themselves. +func (t *Table) cellAt(x, y int) (row, column int) { + rectX, rectY, _, _ := t.GetInnerRect() + + // Determine row as seen on screen. + if t.borders { + row = (y - rectY - 1) / 2 + } else { + row = y - rectY + } + + // Respect fixed rows and row offset. + if row >= 0 { + if row >= t.fixedRows { + row += t.rowOffset + } + if row >= len(t.cells) { + row = -1 + } + } + + // Saerch for the clicked column. + column = -1 + if x >= rectX { + columnX := rectX + if t.borders { + columnX++ + } + for index, width := range t.visibleColumnWidths { + columnX += width + 1 + if x < columnX { + column = t.visibleColumnIndices[index] + break + } + } + } + + return +} + // ScrollToBeginning scrolls the table to the beginning to that the top left // corner of the table is shown. Note that this position may be corrected if // there is a selection. @@ -824,8 +874,8 @@ ColumnLoop: cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes)) if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { - _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY) - printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) + _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) + printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) } } @@ -964,6 +1014,9 @@ ColumnLoop: } } } + + // Remember column infos. + t.visibleColumnIndices, t.visibleColumnWidths = columns, widths } // InputHandler returns the handler for this primitive. @@ -1173,3 +1226,24 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi } }) } + +// MouseHandler returns the mouse handler for this primitive. +func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + switch action { + case MouseLeftClick: + if t.rowsSelectable || t.columnsSelectable { + t.Select(t.cellAt(x, y)) + } + consumed = true + setFocus(t) + } + + return + }) +} diff --git a/treeview.go b/treeview.go index 2a8c16c..95ababa 100644 --- a/treeview.go +++ b/treeview.go @@ -730,3 +730,35 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr t.process() }) } + +// MouseHandler returns the mouse handler for this primitive. +func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + switch action { + case MouseLeftClick: + _, rectY, _, _ := t.GetInnerRect() + y -= rectY + if y >= 0 && y < len(t.nodes) { + node := t.nodes[y] + if node.selectable { + if t.currentNode != node && t.changed != nil { + t.changed(node) + } + if t.selected != nil { + t.selected(node) + } + t.currentNode = node + } + } + consumed = true + setFocus(t) + } + + return + }) +} From 2505a942a1b56886518cfddfd258fdd1f3f25b27 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 29 Mar 2020 20:20:05 +0200 Subject: [PATCH 22/24] Added mouse selection of regions in TextViews. --- README.md | 48 ------------ demos/textview/main.go | 2 +- textview.go | 162 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 151 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 8ce62bd..3a1b1cd 100644 --- a/README.md +++ b/README.md @@ -60,51 +60,3 @@ This package is based on [github.com/gdamore/tcell](https://github.com/gdamore/t ## Your Feedback Add your issue here on GitHub. Feel free to get in touch if you have any questions. - -## Version History - -(There are no corresponding tags in the project. I only keep such a history in this README.) - -- v0.20 (2019-07-08) - - Added autocomplete functionality to `InputField`. -- v0.19 (2018-10-28) - - Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines. -- v0.18 (2018-10-18) - - `InputField` elements can now be navigated freely. -- v0.17 (2018-06-20) - - Added `TreeView`. -- v0.15 (2018-05-02) - - `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example. -- v0.14 (2018-04-13) - - Added an `Escape()` function which keep strings like color or region tags from being recognized as such. - - Added `ANSIWriter()` and `TranslateANSI()` which convert ANSI escape sequences to `tview` color tags. -- v0.13 (2018-04-01) - - Added background colors and text attributes to color tags. -- v0.12 (2018-03-13) - - Added "suspended mode" to `Application`. -- v0.11 (2018-03-02) - - Added a `RemoveItem()` function to `Grid` and `Flex`. -- v0.10 (2018-02-22) - - Direct access to the `screen` object through callback in `Box` (i.e. for all primitives). -- v0.9 (2018-02-20) - - Introduced `Grid` layout. - - Direct access to the `screen` object through callbacks in `Application`. -- v0.8 (2018-01-17) - - Color tags can now be used almost everywhere. -- v0.7 (2018-01-16) - - Forms can now also have a horizontal layout. -- v0.6 (2018-01-14) - - All primitives can now intercept all key events when they have focus. - - Key events can also be intercepted globally (changed to a more general, consistent handling) -- v0.5 (2018-01-13) - - `TextView` now has word wrapping and text alignment -- v0.4 (2018-01-12) - - `TextView` now accepts color tags with any W3C color (including RGB hex values). - - Support for wide unicode characters. -- v0.3 (2018-01-11) - - Added masking to `InputField` and password entry to `Form`. -- v0.2 (2018-01-10) - - Added `Styles` variable with default colors for primitives. - - Completed some missing InputField functions. -- v0.1 (2018-01-06) - - First Release. diff --git a/demos/textview/main.go b/demos/textview/main.go index 66cc45e..701a9c1 100644 --- a/demos/textview/main.go +++ b/demos/textview/main.go @@ -63,7 +63,7 @@ func main() { } }) textView.SetBorder(true) - if err := app.SetRoot(textView, true).Run(); err != nil { + if err := app.SetRoot(textView, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/textview.go b/textview.go index 38876b9..4095454 100644 --- a/textview.go +++ b/textview.go @@ -36,6 +36,16 @@ type textViewIndex struct { Region string // The starting region ID. } +// textViewRegion contains information about a region. +type textViewRegion struct { + // The region ID. + ID string + + // The starting and end screen position of the region as determined the last + // time Draw() was called. A negative value indicates out-of-rect positions. + FromX, FromY, ToX, ToY int +} + // TextView is a box which displays text. It implements the io.Writer interface // so you can stream text to it. This does not trigger a redraw automatically // but if a handler is installed via SetChangedFunc(), you can cause it to be @@ -106,6 +116,9 @@ type TextView struct { // The text alignment, one of AlignLeft, AlignCenter, or AlignRight. align int + // Information about visible regions as of the last call to Draw(). + regionInfos []*textViewRegion + // Indices into the "index" slice which correspond to the first line of the // first highlight and the last line of the last highlight. This is calculated // during re-indexing. Set to -1 if there is no current highlight. @@ -163,6 +176,10 @@ type TextView struct { // highlight(s) into the visible screen. scrollToHighlights bool + // If true, setting new highlights will be a XOR instead of an overwrite + // operation. + toggleHighlights bool + // An optional function which is called when the content of the text view has // changed. changed func() @@ -170,6 +187,10 @@ type TextView struct { // An optional function which is called when the user presses one of the // following keys: Escape, Enter, Tab, Backtab. done func(tcell.Key) + + // An optional function which is called when one or more regions were + // highlighted. + highlighted func([]string, []string) } // NewTextView returns a new text view. @@ -325,6 +346,14 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView { return t } +// SetHighlightedFunc sets a handler which is called when the list of currently +// highlighted regions change. It receives a list of region IDs which were newly +// highlighted as well as those that are not highlighted anymore. +func (t *TextView) SetHighlightedFunc(handler func(addedRegionIDs, removedRedionIDs []string)) *TextView { + t.highlighted = handler + return t +} + // ScrollTo scrolls to the specified row and column (both starting with 0). func (t *TextView) ScrollTo(row, column int) *TextView { if !t.scrollable { @@ -374,23 +403,65 @@ func (t *TextView) Clear() *TextView { return t } -// Highlight specifies which regions should be highlighted. See class -// description for details on regions. Empty region strings are ignored. +// Highlight specifies which regions should be highlighted. If highlight +// toggling is set to true (see SetToggleHighlights()), the highlight of the +// provided regions is toggled (highlighted regions are un-highlighted and vice +// versa). If toggling is set to false, the provided regions are highlighted and +// all other regions will not be highlighted (you may also provide nil to turn +// off all highlights). +// +// For more information on regions, see class description. Empty region strings +// are ignored. // // Text in highlighted regions will be drawn inverted, i.e. with their // background and foreground colors swapped. -// -// Calling this function will remove any previous highlights. To remove all -// highlights, call this function without any arguments. func (t *TextView) Highlight(regionIDs ...string) *TextView { - t.highlights = make(map[string]struct{}) - for _, id := range regionIDs { - if id == "" { - continue + // Determine added and removed regions. + var added, removed []string + if t.highlighted != nil { + highlights := make(map[string]struct{}) + for regionID, highlight := range t.highlights { + highlights[regionID] = highlight + } + for _, regionID := range regionIDs { + if _, ok := highlights[regionID]; ok { + added = append(added, regionID) + delete(highlights, regionID) + } + } + for regionID := range highlights { + removed = append(removed, regionID) } - t.highlights[id] = struct{}{} } - t.index = nil + + // Make new selection. + if t.toggleHighlights { + for _, id := range regionIDs { + if id == "" { + continue + } + if _, ok := t.highlights[id]; ok { + delete(t.highlights, id) + } else { + t.highlights[id] = struct{}{} + } + } + } else { + t.highlights = make(map[string]struct{}) + for _, id := range regionIDs { + if id == "" { + continue + } + t.highlights[id] = struct{}{} + } + t.index = nil + } + + // Notify. + if t.highlighted != nil { + t.highlighted(added, removed) + } + return t } @@ -402,6 +473,15 @@ func (t *TextView) GetHighlights() (regionIDs []string) { return } +// SetToggleHighlights sets a flag to determine how regions are highlighted. +// When set to true, the Highlight() function (or a mouse click) will toggle the +// provided/selected regions. When set to false, Highlight() (or a mouse click) +// will simply highlight the provided regions. +func (t *TextView) SetToggleHighlights(toggle bool) *TextView { + t.toggleHighlights = toggle + return t +} + // ScrollToHighlight will cause the visible area to be scrolled so that the // highlighted regions appear in the visible area of the text view. This // repositioning happens the next time the text view is drawn. It happens only @@ -766,6 +846,9 @@ func (t *TextView) Draw(screen tcell.Screen) { // Re-index. t.reindexBuffer(width) + if t.regions { + t.regionInfos = nil + } // If we don't have an index, there's nothing to draw. if t.index == nil { @@ -850,6 +933,15 @@ func (t *TextView) Draw(screen tcell.Screen) { backgroundColor := index.BackgroundColor attributes := index.Attributes regionID := index.Region + if t.regions && regionID != "" && (len(t.regionInfos) == 0 || t.regionInfos[len(t.regionInfos)-1].ID != regionID) { + t.regionInfos = append(t.regionInfos, &textViewRegion{ + ID: regionID, + FromX: x, + FromY: y + line - t.lineOffset, + ToX: -1, + ToY: -1, + }) + } // Process tags. colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions) @@ -881,7 +973,22 @@ func (t *TextView) Draw(screen tcell.Screen) { colorPos++ } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] { // Get the region. + if regionID != "" && len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID == regionID { + // End last region. + t.regionInfos[len(t.regionInfos)-1].ToX = x + posX + t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset + } regionID = regions[regionPos][1] + if regionID != "" { + // Start new region. + t.regionInfos = append(t.regionInfos, &textViewRegion{ + ID: regionID, + FromX: x + posX, + FromY: y + line - t.lineOffset, + ToX: -1, + ToY: -1, + }) + } tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0] regionPos++ } else { @@ -902,7 +1009,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Do we highlight this character? var highlighted bool - if len(regionID) > 0 { + if regionID != "" { if _, ok := t.highlights[regionID]; ok { highlighted = true } @@ -1022,3 +1129,34 @@ func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } }) } + +// MouseHandler returns the mouse handler for this primitive. +func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + switch action { + case MouseLeftClick: + if t.regions { + // Find a region to highlight. + for _, region := range t.regionInfos { + if y == region.FromY && x < region.FromX || + y == region.ToY && x >= region.ToX || + region.FromY >= 0 && y < region.FromY || + region.ToY >= 0 && y > region.ToY { + continue + } + t.Highlight(region.ID) + break + } + } + consumed = true + setFocus(t) + } + + return + }) +} From b3dc389cb44efe145b43ae1a73a7a5cc70d2098a Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 29 Mar 2020 20:53:28 +0200 Subject: [PATCH 23/24] Added mouse support to presentation demo. TextView "highlighted" callback also receives remaining highlights. --- demos/presentation/cover.go | 4 +- demos/presentation/main.go | 25 +++++++------ textview.go | 73 +++++++++++++++++++++---------------- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/demos/presentation/cover.go b/demos/presentation/cover.go index 2c06e5c..6027561 100644 --- a/demos/presentation/cover.go +++ b/demos/presentation/cover.go @@ -20,6 +20,7 @@ const logo = ` const ( subtitle = `tview - Rich Widgets for Terminal UIs` navigation = `Ctrl-N: Next slide Ctrl-P: Previous slide Ctrl-C: Exit` + mouse = `(or use your mouse)` ) // Cover returns the cover page. @@ -45,7 +46,8 @@ func Cover(nextSlide func()) (title string, content tview.Primitive) { SetBorders(0, 0, 0, 0, 0, 0). AddText(subtitle, true, tview.AlignCenter, tcell.ColorWhite). AddText("", true, tview.AlignCenter, tcell.ColorWhite). - AddText(navigation, true, tview.AlignCenter, tcell.ColorDarkMagenta) + AddText(navigation, true, tview.AlignCenter, tcell.ColorDarkMagenta). + AddText(mouse, true, tview.AlignCenter, tcell.ColorDarkMagenta) // Create a Flex layout that centers the logo and subtitle. flex := tview.NewFlex(). diff --git a/demos/presentation/main.go b/demos/presentation/main.go index 7ae2eb9..065d798 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -47,33 +47,36 @@ func main() { End, } + pages := tview.NewPages() + // The bottom row has some info on where we are. info := tview.NewTextView(). SetDynamicColors(true). SetRegions(true). - SetWrap(false) + SetWrap(false). + SetHighlightedFunc(func(added, removed, remaining []string) { + pages.SwitchToPage(added[0]) + }) // Create the pages for all slides. - currentSlide := 0 - info.Highlight(strconv.Itoa(currentSlide)) - pages := tview.NewPages() previousSlide := func() { - currentSlide = (currentSlide - 1 + len(slides)) % len(slides) - info.Highlight(strconv.Itoa(currentSlide)). + slide, _ := strconv.Atoi(info.GetHighlights()[0]) + slide = (slide - 1 + len(slides)) % len(slides) + info.Highlight(strconv.Itoa(slide)). ScrollToHighlight() - pages.SwitchToPage(strconv.Itoa(currentSlide)) } nextSlide := func() { - currentSlide = (currentSlide + 1) % len(slides) - info.Highlight(strconv.Itoa(currentSlide)). + slide, _ := strconv.Atoi(info.GetHighlights()[0]) + slide = (slide + 1) % len(slides) + info.Highlight(strconv.Itoa(slide)). ScrollToHighlight() - pages.SwitchToPage(strconv.Itoa(currentSlide)) } for index, slide := range slides { title, primitive := slide(nextSlide) - pages.AddPage(strconv.Itoa(index), primitive, true, index == currentSlide) + pages.AddPage(strconv.Itoa(index), primitive, true, index == 0) fmt.Fprintf(info, `%d ["%d"][darkcyan]%s[white][""] `, index+1, index, title) } + info.Highlight("0") // Create the main layout. layout := tview.NewFlex(). diff --git a/textview.go b/textview.go index 4095454..e911c5f 100644 --- a/textview.go +++ b/textview.go @@ -190,7 +190,7 @@ type TextView struct { // An optional function which is called when one or more regions were // highlighted. - highlighted func([]string, []string) + highlighted func(added, removed, remaining []string) } // NewTextView returns a new text view. @@ -348,8 +348,12 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView { // SetHighlightedFunc sets a handler which is called when the list of currently // highlighted regions change. It receives a list of region IDs which were newly -// highlighted as well as those that are not highlighted anymore. -func (t *TextView) SetHighlightedFunc(handler func(addedRegionIDs, removedRedionIDs []string)) *TextView { +// highlighted, those that are not highlighted anymore, and those that remain +// highlighted. +// +// Note that because regions are only determined during drawing, this function +// can only fire for regions that have existed during the last call to Draw(). +func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) *TextView { t.highlighted = handler return t } @@ -416,50 +420,55 @@ func (t *TextView) Clear() *TextView { // Text in highlighted regions will be drawn inverted, i.e. with their // background and foreground colors swapped. func (t *TextView) Highlight(regionIDs ...string) *TextView { + // Toggle highlights. + if t.toggleHighlights { + var newIDs []string + HighlightLoop: + for regionID := range t.highlights { + for _, id := range regionIDs { + if regionID == id { + continue HighlightLoop + } + } + newIDs = append(newIDs, regionID) + } + for _, regionID := range regionIDs { + if _, ok := t.highlights[regionID]; !ok { + newIDs = append(newIDs, regionID) + } + } + regionIDs = newIDs + } // Now we have a list of region IDs that end up being highlighted. + // Determine added and removed regions. - var added, removed []string + var added, removed, remaining []string if t.highlighted != nil { - highlights := make(map[string]struct{}) - for regionID, highlight := range t.highlights { - highlights[regionID] = highlight - } for _, regionID := range regionIDs { - if _, ok := highlights[regionID]; ok { + if _, ok := t.highlights[regionID]; ok { + remaining = append(remaining, regionID) + delete(t.highlights, regionID) + } else { added = append(added, regionID) - delete(highlights, regionID) } } - for regionID := range highlights { + for regionID := range t.highlights { removed = append(removed, regionID) } } // Make new selection. - if t.toggleHighlights { - for _, id := range regionIDs { - if id == "" { - continue - } - if _, ok := t.highlights[id]; ok { - delete(t.highlights, id) - } else { - t.highlights[id] = struct{}{} - } + t.highlights = make(map[string]struct{}) + for _, id := range regionIDs { + if id == "" { + continue } - } else { - t.highlights = make(map[string]struct{}) - for _, id := range regionIDs { - if id == "" { - continue - } - t.highlights[id] = struct{}{} - } - t.index = nil + t.highlights[id] = struct{}{} } + t.index = nil // Notify. - if t.highlighted != nil { - t.highlighted(added, removed) + if t.highlighted != nil && len(added) > 0 || len(removed) > 0 { + t.highlighted(added, removed, remaining) } return t From f395cf6e33d5c9b1534cb4c0b910a1c4f382593e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 29 Mar 2020 21:36:06 +0200 Subject: [PATCH 24/24] Added mouse scrolling to List, Table, TextView, and TreeView. --- application.go | 16 ++++++++-------- list.go | 20 ++++++++++++++++++-- table.go | 7 +++++++ textview.go | 7 +++++++ treeview.go | 6 ++++++ 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/application.go b/application.go index bbf51b4..67ef6dd 100644 --- a/application.go +++ b/application.go @@ -37,10 +37,10 @@ const ( MouseRightUp MouseRightClick MouseRightDoubleClick - WheelUp - WheelDown - WheelLeft - WheelRight + MouseScrollUp + MouseScrollDown + MouseScrollLeft + MouseScrollRight ) // queuedUpdate represented the execution of f queued by @@ -467,10 +467,10 @@ func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMou button tcell.ButtonMask action MouseAction }{ - {tcell.WheelUp, WheelUp}, - {tcell.WheelDown, WheelDown}, - {tcell.WheelLeft, WheelLeft}, - {tcell.WheelRight, WheelRight}} { + {tcell.WheelUp, MouseScrollUp}, + {tcell.WheelDown, MouseScrollDown}, + {tcell.WheelLeft, MouseScrollLeft}, + {tcell.WheelRight, MouseScrollRight}} { if buttons&wheelEvent.button != 0 { fire(wheelEvent.action) } diff --git a/list.go b/list.go index 474477b..e28b415 100644 --- a/list.go +++ b/list.go @@ -560,7 +560,7 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit } // indexAtPoint returns the index of the list item found at the given position -// or -1 if there is no such list item. +// or a negative value if there is no such list item. func (l *List) indexAtPoint(x, y int) int { rectX, rectY, width, height := l.GetInnerRect() if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height { @@ -571,6 +571,7 @@ func (l *List) indexAtPoint(x, y int) int { if l.showSecondaryText { index /= 2 } + index += l.offset if index >= len(l.items) { return -1 @@ -586,7 +587,8 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } // Process mouse event. - if action == MouseLeftClick { + switch action { + case MouseLeftClick: setFocus(l) index := l.indexAtPoint(event.Position()) if index != -1 { @@ -603,6 +605,20 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, l.currentItem = index } consumed = true + case MouseScrollUp: + if l.offset > 0 { + l.offset-- + } + consumed = true + case MouseScrollDown: + lines := len(l.items) - l.offset + if l.showSecondaryText { + lines *= 2 + } + if _, _, _, height := l.GetInnerRect(); lines > height { + l.offset++ + } + consumed = true } return diff --git a/table.go b/table.go index 05923b7..a6f1b3e 100644 --- a/table.go +++ b/table.go @@ -1242,6 +1242,13 @@ func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } consumed = true setFocus(t) + case MouseScrollUp: + t.trackEnd = false + t.rowOffset-- + consumed = true + case MouseScrollDown: + t.rowOffset++ + consumed = true } return diff --git a/textview.go b/textview.go index e911c5f..5b2944a 100644 --- a/textview.go +++ b/textview.go @@ -1164,6 +1164,13 @@ func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMou } consumed = true setFocus(t) + case MouseScrollUp: + t.trackEnd = false + t.lineOffset-- + consumed = true + case MouseScrollDown: + t.lineOffset++ + consumed = true } return diff --git a/treeview.go b/treeview.go index 95ababa..ddefa62 100644 --- a/treeview.go +++ b/treeview.go @@ -757,6 +757,12 @@ func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMou } consumed = true setFocus(t) + case MouseScrollUp: + t.movement = treeUp + consumed = true + case MouseScrollDown: + t.movement = treeDown + consumed = true } return