Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Keyboard only operation #1074

Closed
wants to merge 11 commits into from
7 changes: 6 additions & 1 deletion cmd/fyne_demo/screens/window.go
Expand Up @@ -187,7 +187,12 @@ func loadWindowGroup() fyne.Widget {
}),
widget.NewButton("Fixed size window", func() {
w := fyne.CurrentApp().NewWindow("Fixed")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewLabel("Hello World!")))
w.SetContent(fyne.NewContainerWithLayout(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is demoing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was introduced to test keyboard operation in a second window. There was no extra windows with any focusable widgets, so I had to introduce some buttons to test it. Of cause it failed the first time I tested it, but should be working ok now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All windows should behave the same though?

layout.NewGridLayoutWithRows(3),
widget.NewLabel("Hello World!"),
widget.NewButton("Cancel", func() { w.Close() }),
widget.NewButton("OK", func() { w.Close() })),
)

w.Resize(fyne.NewSize(240, 180))
w.SetFixedSize(true)
Expand Down
8 changes: 8 additions & 0 deletions dialog/base.go
Expand Up @@ -38,6 +38,7 @@ type dialog struct {
content, label fyne.CanvasObject
dismiss *widget.Button
parent fyne.Window
oldFocus fyne.Focusable
}

// SetOnClosed allows to set a callback function that is called when
Expand Down Expand Up @@ -144,9 +145,16 @@ func newButtonList(buttons ...*widget.Button) fyne.CanvasObject {
func (d *dialog) Show() {
d.sendResponse = true
d.win.Show()
d.oldFocus = d.win.Canvas.Focused()
if d.dismiss != nil {
d.win.Canvas.Focus(d.dismiss)
} else {
d.win.Canvas.Focus(nil)
}
}

func (d *dialog) Hide() {
d.win.Canvas.Focus(d.oldFocus)
d.hideWithResponse(false)
}

Expand Down
13 changes: 11 additions & 2 deletions internal/app/focus.go
Expand Up @@ -13,7 +13,11 @@ type FocusManager struct {
func (f *FocusManager) nextInChain(current fyne.Focusable) fyne.Focusable {
var first, next fyne.Focusable
found := current == nil // if we have no starting point then pretend we matched already
driver.WalkVisibleObjectTree(f.canvas.Content(), func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
content := f.canvas.Overlays().Top()
if content == nil {
content = f.canvas.Content()
}
driver.WalkVisibleObjectTree(content, func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
if w, ok := obj.(fyne.Disableable); ok && w.Disabled() {
// disabled widget cannot receive focus
return false
Expand Down Expand Up @@ -48,7 +52,12 @@ func (f *FocusManager) nextInChain(current fyne.Focusable) fyne.Focusable {
func (f *FocusManager) previousInChain(current fyne.Focusable) fyne.Focusable {
var last, previous fyne.Focusable
found := false
driver.WalkVisibleObjectTree(f.canvas.Content(), func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {

content := f.canvas.Overlays().Top()
if content == nil {
content = f.canvas.Content()
}
driver.WalkVisibleObjectTree(content, func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
if w, ok := obj.(fyne.Disableable); ok && w.Disabled() {
// disabled widget cannot receive focus
return false
Expand Down
41 changes: 35 additions & 6 deletions internal/driver/glfw/menu_bar.go
Expand Up @@ -16,11 +16,41 @@ var _ fyne.Widget = (*MenuBar)(nil)
// MenuBar is a widget for displaying a fyne.MainMenu in a bar.
type MenuBar struct {
widget.Base
Items []fyne.CanvasObject

active bool
activeItem *menuBarItem
canvas fyne.Canvas
Items []fyne.CanvasObject
currentItem int
active bool
canvas fyne.Canvas
activeItem *menuBarItem
}

// handleKey will process keys pressed when menu is open
func (b *MenuBar) handleKey(key fyne.KeyName) {
if key == fyne.KeyEscape {
b.deactivate()
} else if key == fyne.KeyEnter || key == fyne.KeyReturn || key == fyne.KeySpace {
// Simulate tapping on menu entry
b.Items[b.currentItem].(*menuBarItem).Child().HandleEnterKey()
} else if key == fyne.KeyRight {
// Move to the menu item at right
if b.currentItem < len(b.Items)-1 {
b.Items[b.currentItem].(*menuBarItem).Tapped(nil)
b.currentItem++
b.Items[b.currentItem].(*menuBarItem).Tapped(nil)
}
} else if key == fyne.KeyLeft {
// Move to the menu item at left
if b.currentItem > 0 {
b.Items[b.currentItem].(*menuBarItem).Tapped(nil)
b.currentItem--
b.Items[b.currentItem].(*menuBarItem).Tapped(nil)
}
} else if key == fyne.KeyUp {
// Move focus to menu item above
b.Items[b.currentItem].(*menuBarItem).Child().HandleUpKey()
} else if key == fyne.KeyDown {
// Move focus to menu item below
b.Items[b.currentItem].(*menuBarItem).Child().HandleDownKey()
}
}

// NewMenuBar creates a menu bar populated with items from the passed main menu structure.
Expand Down Expand Up @@ -114,7 +144,6 @@ func (b *MenuBar) deactivate() {
if !b.active {
return
}

b.active = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dupe

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

if b.activeItem != nil {
defer b.activeItem.Child().Dismiss()
Expand Down
14 changes: 12 additions & 2 deletions internal/driver/glfw/menu_bar_item.go
Expand Up @@ -112,11 +112,22 @@ func (i *menuBarItem) Show() {
// Tapped toggles the activation state of the menu bar.
// It shows the item’s menu if the bar is activated and hides it if the bar is deactivated.
// Implements: fyne.Tappable
func (i *menuBarItem) Tapped(*fyne.PointEvent) {
func (i *menuBarItem) Tapped(pos *fyne.PointEvent) {
if i.Parent.active {
i.Parent.deactivate()
} else {
// Set then menu bar's index to the tapped items index and update focus
i.Parent.activateChild(i)
for j := 0; j < len(i.Parent.Items); j++ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this loop...
i.Parent.Items[i.Parent.index] is i so the item that has just been activated is being defocussed (or "upped").
Possibly this is because I don't understand what index is

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the name "index" to "currentItem", as it indicates the menu item selected. There is unfortunately a mix of "focused". The focused item receiveing the keyboard data is the MenuBar, while the indicated (focus-colored) item is one of the menu items in the list. Perhaps each menu item should recieve keyboard input?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THat might be cleaner, would probably help with code encapsulation, that said left/right may still make sense at the top level.

if i.Parent.Items[j] == i {
i.Parent.currentItem = j
if pos == nil {
i.Parent.Items[i.Parent.currentItem].(*menuBarItem).Child().HandleUpKey()
} else {
i.Parent.Items[i.Parent.currentItem].(*menuBarItem).Child().Defocus()
}
}
}
}
}

Expand All @@ -130,7 +141,6 @@ func (r *menuBarItemRenderer) BackgroundColor() color.Color {
if r.i.hovered || (r.i.child != nil && r.i.child.Visible()) {
return theme.HoverColor()
}

return color.Transparent
}

Expand Down
6 changes: 4 additions & 2 deletions internal/driver/glfw/menu_bar_test.go
Expand Up @@ -65,7 +65,7 @@ func TestMenuBar(t *testing.T) {
}
themeCounter++
})

button.FocusLost()
container := fyne.NewContainer(button, menuBar)

w.SetContent(container)
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestMenuBar(t *testing.T) {
},
{
actions: []action{{"tap", buttonPos}},
wantImage: "menu_bar_hovered_content.png",
wantImage: "menu_bar_hovered_focused_content.png",
},
},
},
Expand Down Expand Up @@ -305,6 +305,8 @@ func TestMenuBar(t *testing.T) {
t.Run(name, func(t *testing.T) {
test.MoveMouse(c, fyne.NewPos(0, 0))
test.TapCanvas(c, fyne.NewPos(0, 0))
// Make sure that the button is unfocused at start of the test steps
c.Unfocus()
test.AssertImageMatches(t, "menu_bar_initial.png", c.Capture())
for i, s := range tt.steps {
t.Run("step "+strconv.Itoa(i+1), func(t *testing.T) {
Expand Down
Binary file modified internal/driver/glfw/testdata/menu_bar_hovered_content_dark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 28 additions & 9 deletions internal/driver/glfw/window.go
Expand Up @@ -15,6 +15,7 @@ import (
"fyne.io/fyne/internal/cache"
"fyne.io/fyne/internal/driver"
"fyne.io/fyne/internal/painter/gl"
"fyne.io/fyne/widget"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must not add dependencies from widget here.


"github.com/go-gl/glfw/v3.3/glfw"
)
Expand Down Expand Up @@ -622,7 +623,13 @@ func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw.
}

if layer != 1 { // 0 - overlay, 1 - menu, 2 - content
if wid, ok := co.(fyne.Focusable); ok {
if _, isToolbarButton := co.(*widget.ToolbarButton); isToolbarButton {
// Avoid changing focus when a toolbar button is tapped
w.canvas.Unfocus()
} else if _, isButton := co.(*widget.Button); isButton {
// Avoid changing focus when an ordinary button is tapped
w.canvas.Unfocus()
} else if wid, ok := co.(fyne.Focusable); ok {
w.canvas.Focus(wid)
} else {
w.canvas.Unfocus()
Expand Down Expand Up @@ -857,16 +864,29 @@ func (w *window) keyPressed(viewport *glfw.Window, key glfw.Key, scancode int, a
keyEvent := &fyne.KeyEvent{Name: keyName}
keyDesktopModifier := desktopModifier(mods)

if keyName == fyne.KeyTab {
if keyDesktopModifier == 0 {
if action != glfw.Release {
w.canvas.focusMgr.FocusNext(w.canvas.focused)
// Activate the menu when the Alt key is pressed
focused := w.canvas.Focused()
if menu, ok := w.canvas.menu.(*MenuBar); ok {
if !menu.active {
if action == glfw.Press && (key == glfw.KeyLeftAlt || key == glfw.KeyRightAlt) &&
keyDesktopModifier == desktop.AltModifier {
mbi := menu.Items[0].(*menuBarItem)
mbi.Tapped(nil)
}
} else {
if action == glfw.Release {
menu.handleKey(keyName)
}
return
}
}

if keyName == fyne.KeyTab && action != glfw.Release {
if keyDesktopModifier == 0 {
w.canvas.focusMgr.FocusNext(w.canvas.focused)
return
} else if keyDesktopModifier == desktop.ShiftModifier {
if action != glfw.Release {
w.canvas.focusMgr.FocusPrevious(w.canvas.focused)
}
w.canvas.focusMgr.FocusPrevious(w.canvas.focused)
return
}
}
Expand Down Expand Up @@ -950,7 +970,6 @@ func (w *window) keyPressed(viewport *glfw.Window, key glfw.Key, scancode int, a
}

// No shortcut detected, pass down to TypedKey
focused := w.canvas.Focused()
if focused != nil {
w.queueEvent(func() { focused.TypedKey(keyEvent) })
} else if w.canvas.onTypedKey != nil {
Expand Down
50 changes: 49 additions & 1 deletion theme/theme.go
Expand Up @@ -61,6 +61,44 @@ func DarkTheme() fyne.Theme {
return theme
}

// Shade will darken a light color and lighten a light color by the given % value
// should use 4,8, 12 or 14 percent darken/lighten to implement google material design rules
// Note that the percent lightening is twice the percent given. See material.io/design/interaction/states.
func Shade(c color.Color, pct uint32) color.Color {
if pct == 0 {
return c
}
r, g, b, a := c.RGBA()
if pct > 50 {
pct = 50
}
if r+g+b < 3*0x8080 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like this might be important, should it be extracted into something like an isLight helper?

// Lighten by twice the percent given.
return color.NRGBA{
R: uint8((r + (0x10000-r)*pct/50) >> 8),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

percent / 50 is a strange seemting thing to do?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above about shades.

G: uint8((g + (0x10000-g)*pct/50) >> 8),
B: uint8((b + (0x10000-b)*pct/50) >> 8),
A: uint8(a >> 8),
}
}
// Darken by given percent
return color.NRGBA{
R: uint8((r * (100 - pct) / 100) >> 8),
G: uint8((g * (100 - pct) / 100) >> 8),
B: uint8((b * (100 - pct) / 100) >> 8),
A: uint8(a >> 8),
}
}

// PressedShade is the shade used for pressed buttons, in %
const PressedShade = 14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these percentages? should they be public API?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they are percentages. I belive they should be part of the customizable theme, but I do not see how to do it.


// HoveredShade is the shade used for hovered buttons, in %
const HoveredShade = 4

// FocusedShade is the shade used for focused buttons, in %.
const FocusedShade = 8

func (t *builtinTheme) BackgroundColor() color.Color {
return t.background
}
Expand Down Expand Up @@ -117,7 +155,7 @@ func (t *builtinTheme) HoverColor() color.Color {

// FocusColor returns the color used to highlight a focused widget
func (t *builtinTheme) FocusColor() color.Color {
return t.primary
return Shade(t.primary, FocusedShade)
}

// ScrollBarColor returns the color (and translucency) for a scrollBar
Expand Down Expand Up @@ -276,6 +314,16 @@ func HoverColor() color.Color {
return current().HoverColor()
}

// PressedColor returns the colour used for a pressed button
func PressedColor() color.Color {
return Shade(FocusColor(), PressedShade)
}

// HoverFocusedColor returns the colour used for a focused/primary hovered button
func HoverFocusedColor() color.Color {
return Shade(FocusColor(), HoveredShade)
}

// FocusColor returns the color used to highlight a focussed widget
func FocusColor() color.Color {
return current().FocusColor()
Expand Down
2 changes: 1 addition & 1 deletion theme/theme_test.go
Expand Up @@ -95,7 +95,7 @@ func Test_HoverColor(t *testing.T) {
func Test_FocusColor(t *testing.T) {
fyne.CurrentApp().Settings().SetTheme(DarkTheme())
c := FocusColor()
assert.Equal(t, PrimaryColor(), c, "wrong focus color")
assert.Equal(t, DarkTheme().FocusColor(), c, "wrong focus color")
}

func Test_ScrollBarColor(t *testing.T) {
Expand Down