-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Make it possible to auto display a popup list with completion - Navigate with keyboard (up, down) and use Escape key to hide the menu - Entry is always editable while the menu is open Co-authored-by: Pablo Fuentes <[email protected]> Co-authored-by: Andy Williams <[email protected]>
- Loading branch information
1 parent
a37ac4f
commit a8a85d8
Showing
4 changed files
with
462 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
package widget | ||
|
||
import ( | ||
"fyne.io/fyne/v2" | ||
"fyne.io/fyne/v2/theme" | ||
"fyne.io/fyne/v2/widget" | ||
) | ||
|
||
// CompletionEntry is an Entry with options displayed in a PopUpMenu. | ||
type CompletionEntry struct { | ||
widget.Entry | ||
popupMenu *widget.PopUp | ||
navigableList *navigableList | ||
Options []string | ||
pause bool | ||
itemHeight float32 | ||
} | ||
|
||
// NewCompletionEntry creates a new CompletionEntry which creates a popup menu that responds to keystrokes to navigate through the items without losing the editing ability of the text input. | ||
func NewCompletionEntry(options []string) *CompletionEntry { | ||
c := &CompletionEntry{Options: options} | ||
c.ExtendBaseWidget(c) | ||
return c | ||
} | ||
|
||
// HideCompletion hides the completion menu. | ||
func (c *CompletionEntry) HideCompletion() { | ||
if c.popupMenu != nil { | ||
c.popupMenu.Hide() | ||
} | ||
} | ||
|
||
// Move changes the relative position of the select entry. | ||
// | ||
// Implements: fyne.Widget | ||
func (c *CompletionEntry) Move(pos fyne.Position) { | ||
c.Entry.Move(pos) | ||
if c.popupMenu != nil { | ||
c.popupMenu.Resize(c.maxSize()) | ||
c.popupMenu.Move(c.popUpPos()) | ||
} | ||
} | ||
|
||
// Refresh the list to update the options to display. | ||
func (c *CompletionEntry) Refresh() { | ||
c.Entry.Refresh() | ||
if c.navigableList != nil { | ||
c.navigableList.SetOptions(c.Options) | ||
} | ||
} | ||
|
||
// SetOptions set the completion list with itemList and update the view. | ||
func (c *CompletionEntry) SetOptions(itemList []string) { | ||
c.Options = itemList | ||
c.Refresh() | ||
} | ||
|
||
// ShowCompletion displays the completion menu | ||
func (c *CompletionEntry) ShowCompletion() { | ||
if c.pause { | ||
return | ||
} | ||
if len(c.Options) == 0 { | ||
c.HideCompletion() | ||
return | ||
} | ||
|
||
if c.navigableList == nil { | ||
c.navigableList = newNavigableList(c.Options, &c.Entry, c.setTextFromMenu, c.HideCompletion) | ||
} | ||
holder := fyne.CurrentApp().Driver().CanvasForObject(c) | ||
|
||
if c.popupMenu == nil { | ||
c.popupMenu = widget.NewPopUp(c.navigableList, holder) | ||
} | ||
c.popupMenu.Resize(c.maxSize()) | ||
c.popupMenu.ShowAtPosition(c.popUpPos()) | ||
holder.Focus(c.navigableList) | ||
} | ||
|
||
// calculate the max size to make the popup to cover everything below the entry | ||
func (c *CompletionEntry) maxSize() fyne.Size { | ||
cnv := fyne.CurrentApp().Driver().CanvasForObject(c) | ||
|
||
if c.itemHeight == 0 { | ||
// set item height to cache | ||
c.itemHeight = c.navigableList.CreateItem().MinSize().Height | ||
} | ||
|
||
listheight := float32(len(c.Options))*(c.itemHeight+2*theme.Padding()+theme.SeparatorThicknessSize()) + 2*theme.Padding() | ||
canvasSize := cnv.Size() | ||
entrySize := c.Size() | ||
if canvasSize.Height > listheight { | ||
return fyne.NewSize(entrySize.Width, listheight) | ||
} | ||
|
||
return fyne.NewSize( | ||
entrySize.Width, | ||
canvasSize.Height-c.Position().Y-entrySize.Height-theme.InputBorderSize()-theme.Padding()) | ||
} | ||
|
||
// calculate where the popup should appear | ||
func (c *CompletionEntry) popUpPos() fyne.Position { | ||
entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(c) | ||
return entryPos.Add(fyne.NewPos(0, c.Size().Height)) | ||
} | ||
|
||
// Prevent the menu to open when the user validate value from the menu. | ||
func (c *CompletionEntry) setTextFromMenu(s string) { | ||
c.pause = true | ||
c.Entry.SetText(s) | ||
c.Entry.CursorColumn = len([]rune(s)) | ||
c.Entry.Refresh() | ||
c.pause = false | ||
c.popupMenu.Hide() | ||
} | ||
|
||
type navigableList struct { | ||
widget.List | ||
entry *widget.Entry | ||
selected int | ||
setTextFromMenu func(string) | ||
hide func() | ||
navigating bool | ||
items []string | ||
} | ||
|
||
func newNavigableList(items []string, entry *widget.Entry, setTextFromMenu func(string), hide func()) *navigableList { | ||
n := &navigableList{ | ||
entry: entry, | ||
selected: -1, | ||
setTextFromMenu: setTextFromMenu, | ||
hide: hide, | ||
items: items, | ||
} | ||
|
||
n.List = widget.List{ | ||
Length: func() int { | ||
return len(n.items) | ||
}, | ||
CreateItem: func() fyne.CanvasObject { | ||
return widget.NewLabel("") | ||
}, | ||
UpdateItem: func(i widget.ListItemID, o fyne.CanvasObject) { | ||
o.(*widget.Label).SetText(n.items[i]) | ||
}, | ||
OnSelected: func(id widget.ListItemID) { | ||
if !n.navigating && id > -1 { | ||
setTextFromMenu(n.items[id]) | ||
} | ||
n.navigating = false | ||
}, | ||
} | ||
n.ExtendBaseWidget(n) | ||
return n | ||
} | ||
|
||
// Implements: fyne.Focusable | ||
func (n *navigableList) FocusGained() { | ||
} | ||
|
||
// Implements: fyne.Focusable | ||
func (n *navigableList) FocusLost() { | ||
} | ||
|
||
func (n *navigableList) SetOptions(items []string) { | ||
n.Unselect(n.selected) | ||
n.items = items | ||
n.Refresh() | ||
n.selected = -1 | ||
} | ||
|
||
func (n *navigableList) TypedKey(event *fyne.KeyEvent) { | ||
switch event.Name { | ||
case fyne.KeyDown: | ||
if n.selected < len(n.items)-1 { | ||
n.selected++ | ||
} else { | ||
n.selected = 0 | ||
} | ||
n.navigating = true | ||
n.Select(n.selected) | ||
|
||
case fyne.KeyUp: | ||
if n.selected > 0 { | ||
n.selected-- | ||
} else { | ||
n.selected = len(n.items) - 1 | ||
} | ||
n.navigating = true | ||
n.Select(n.selected) | ||
case fyne.KeyReturn, fyne.KeyEnter: | ||
if n.selected == -1 { // so the user want to submit the entry | ||
n.hide() | ||
n.entry.TypedKey(event) | ||
} else { | ||
n.navigating = false | ||
n.OnSelected(n.selected) | ||
} | ||
case fyne.KeyEscape: | ||
n.hide() | ||
default: | ||
n.entry.TypedKey(event) | ||
|
||
} | ||
} | ||
|
||
func (n *navigableList) TypedRune(r rune) { | ||
n.entry.TypedRune(r) | ||
} |
Oops, something went wrong.