Skip to content

Commit

Permalink
Add a completion entry (#11)
Browse files Browse the repository at this point in the history
- 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
3 people authored May 13, 2021
1 parent a37ac4f commit a8a85d8
Show file tree
Hide file tree
Showing 4 changed files with 462 additions and 0 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,49 @@ tree.Sorter = func(u1, u2 fyne.URI) bool {
<img src="img/widget-filetree.png" width="1024" height="880" alt="FileTree Widget" style="max-width: 100%" />
</p>

### CompletionEntry

An extension of widget.Entry for displaying a popup menu for completion. The "up" and "down" keys on the keyboard are used to navigate through the menu, the "Enter" key is used to confirm the selection. The options can also be selected with the mouse. The "Escape" key closes the selection list.

```go
entry := widget.NewCompletionEntry([]string{})

// When the use typed text, complete the list.
entry.OnChanged = func(s string) {
// completion start for text length >= 3
if len(s) < 3 {
entry.HideCompletion()
return
}

// Make a search on wikipedia
resp, err := http.Get(
"https://en.wikipedia.org/w/api.php?action=opensearch&search=" + entry.Text,
)
if err != nil {
entry.HideCompletion()
return
}

// Get the list of possible completion
var results [][]string
json.NewDecoder(resp.Body).Decode(&results)

// no results
if len(results) == 0 {
entry.HideCompletion()
return
}

// then show them
entry.SetOptions(results[1])
entry.ShowCompletion()
}
```

<p align="center" markdown="1" style="max-width: 100%">
<img src="img/widget-completion-entry.png" width="825" height="634" alt="CompletionEntry Widget" style="max-width: 100%" />
</p>

### 7-Segment ("Hex") Display

Expand Down
Binary file added img/widget-completion-entry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
210 changes: 210 additions & 0 deletions widget/completionentry.go
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)
}
Loading

0 comments on commit a8a85d8

Please sign in to comment.