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
Changes from 2 commits
ed04aa9
0b06b60
5bb4487
07c8cbf
c32a82c
c416a0d
c8052c9
b61755f
f6ec6d1
13ec8ff
0a75eb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,12 @@ 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 { | ||
//OBS driver.WalkVisibleObjectTree(f.cavas.Content(), func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OBS? try not to commit commented out code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may relate to bug #814 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I use //OBS to easily do a global search for comments that should be removed before commit. This fell through the cracks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. In the future it's good to have a bug fix in separate commit - possibly even PR - than a big feature addition |
||
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 | ||
|
@@ -48,7 +53,13 @@ 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 { | ||
//OBS driver.WalkVisibleObjectTree(f.canvas.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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
Items []fyne.CanvasObject | ||
index int | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. index is quite generic, not clear what this means There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. index is the currently focused item on the menu bar. Using arrow keys, the focus is moved and index is updated. This is also used on toolbars and tabs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just changed it to currentItem |
||
active bool | ||
activeItem *menuBarItem | ||
canvas fyne.Canvas | ||
activeItem *menuBarItem | ||
} | ||
|
||
// HandleKey will process keys pressed when menu is open | ||
func (b *MenuBar) HandleKey(key fyne.KeyName) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not be an exported API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By way of explanation here (sorry it was lacking before) we don't add a new exported API unless there is a solid use-case for developers. |
||
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.index].(*menuBarItem).Child().Do() | ||
} else if key == fyne.KeyRight { | ||
// Move to the menu item at right | ||
if b.index < len(b.Items)-1 { | ||
b.Items[b.index].(*menuBarItem).Tapped(nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tapped should not be called on an item with an invalid parameter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, Action() is not possible, I colud of cause duplicate the code or extract it into a new function, but I think the trick of using a nil position is fine for signaling that the event comes from the keyboard and not the mouse. I have used it several places. |
||
b.index++ | ||
b.Items[b.index].(*menuBarItem).Tapped(nil) | ||
} | ||
} else if key == fyne.KeyLeft { | ||
// Move to the menu item at left | ||
if b.index > 0 { | ||
b.Items[b.index].(*menuBarItem).Tapped(nil) | ||
b.index-- | ||
b.Items[b.index].(*menuBarItem).Tapped(nil) | ||
} | ||
} else if key == fyne.KeyUp { | ||
// Move focus to menu item above | ||
b.Items[b.index].(*menuBarItem).Child().Up() | ||
} else if key == fyne.KeyDown { | ||
// Move focus to menu item below | ||
b.Items[b.index].(*menuBarItem).Child().Down() | ||
} | ||
} | ||
|
||
// NewMenuBar creates a menu bar populated with items from the passed main menu structure. | ||
|
@@ -106,6 +136,7 @@ func (b *MenuBar) activateChild(item *menuBarItem) { | |
return | ||
} | ||
|
||
|
||
item.Child().Show() | ||
b.Refresh() | ||
} | ||
|
@@ -114,7 +145,7 @@ func (b *MenuBar) deactivate() { | |
if !b.active { | ||
return | ||
} | ||
|
||
b.active = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dupe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
b.active = false | ||
if b.activeItem != nil { | ||
defer b.activeItem.Child().Dismiss() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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++ { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this loop... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.index = j | ||
if pos==nil { | ||
i.Parent.Items[i.Parent.index].(*menuBarItem).Child().Up() | ||
} else { | ||
i.Parent.Items[i.Parent.index].(*menuBarItem).Child().Defocus() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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 | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
}, | ||
}, | ||
}, | ||
|
@@ -182,7 +182,7 @@ func TestMenuBar(t *testing.T) { | |
{ | ||
actions: []action{{"tap", fileNewPos}}, | ||
wantAction: "new", | ||
wantImage: "menu_bar_initial.png", | ||
wantImage: "menu_bar_initial1.png", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why postfixing numbers? it is not clear what the intended differences are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This relates to my trying to fix the tests. It did not suceed. The problem is the color changes when a button is focused. Previously the buttons did not change color when focused, as it was not possible to focus them. So the menu_bar_initial.png can not be used in every case. It will be different depending on the previous simulated operations. Further changes to the tests must be done later on, introducing new png images. This must wait until the coloring sheme is settled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use appropriate naming instead of adding a number - it should be clear of the image content/state based on the name. We can’t have failing tests waiting for another PR or external change, if this PR depends on another then we have to land that first before this can complete. |
||
}, | ||
}, | ||
}, | ||
|
@@ -198,7 +198,7 @@ func TestMenuBar(t *testing.T) { | |
{ | ||
actions: []action{{"tap", fileOpenPos}}, | ||
wantAction: "open", | ||
wantImage: "menu_bar_initial.png", | ||
wantImage: "menu_bar_initial2.png", | ||
}, | ||
}, | ||
}, | ||
|
@@ -222,7 +222,7 @@ func TestMenuBar(t *testing.T) { | |
{ | ||
actions: []action{{"tap", fileRecentOlderOld1Pos}}, | ||
wantAction: "old 1", | ||
wantImage: "menu_bar_initial.png", | ||
wantImage: "menu_bar_initial3.png", | ||
}, | ||
}, | ||
}, | ||
|
@@ -305,7 +305,7 @@ 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)) | ||
test.AssertImageMatches(t, "menu_bar_initial.png", c.Capture()) | ||
test.AssertImageMatches(t, "menu_bar_initial4.png", c.Capture()) | ||
for i, s := range tt.steps { | ||
t.Run("step "+strconv.Itoa(i+1), func(t *testing.T) { | ||
lastAction = "" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must not add dependencies from |
||
|
||
"github.com/go-gl/glfw/v3.3/glfw" | ||
) | ||
|
@@ -621,11 +622,19 @@ func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw. | |
} | ||
} | ||
|
||
needsfocus := false | ||
if layer != 1 { // 0 - overlay, 1 - menu, 2 - content | ||
if wid, ok := co.(fyne.Focusable); ok { | ||
w.canvas.Focus(wid) | ||
} else { | ||
w.canvas.Unfocus() | ||
needsfocus = true | ||
|
||
if wid := w.canvas.Focused(); wid != nil { | ||
if wid.(fyne.CanvasObject) != co { | ||
// Avoid changing focus when a toolbar button is pressed. | ||
if _, isToolbarButton := co.(*widget.ToolbarButton); !isToolbarButton { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is confusing - if you made a new ToolbButton so that it didn't accept focus like normal buttons then this should not be needed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunatly it cannot. The toolbar must be focusable by TAB key-presses, but should not be focused when tapped. On the other hand, the code can be simplified as I have now done. |
||
w.canvas.Unfocus() | ||
} | ||
} else { | ||
needsfocus = false | ||
} | ||
} | ||
} | ||
|
||
|
@@ -635,6 +644,13 @@ func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw. | |
w.mouseButton = 0 | ||
} | ||
|
||
git // we cannot switch here as objects may respond to multiple cases | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo I'm guessing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With two screens, it is not easy to know which window is focused... |
||
if wid, ok := co.(fyne.Focusable); ok && needsfocus { | ||
if dis, ok := wid.(fyne.Disableable); !ok || !dis.Disabled() { | ||
w.canvas.Focus(wid) | ||
} | ||
} | ||
|
||
// Check for double click/tap | ||
doubleTapped := false | ||
if action == glfw.Release && button == desktop.LeftMouseButton { | ||
|
@@ -857,7 +873,26 @@ 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 { | ||
// Activate the menu when the Alt key is pressed | ||
focused := w.canvas.Focused() | ||
isMenu := false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is reduntant - inside the ok the menu.active flag could apply to both blocks - the return in the second block means the later check on 895 is not needed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct - but the simplification was not easy - at least not for me. I an realy impressed that you spotted this so easily. |
||
menu, ok := w.canvas.menu.(*MenuBar) | ||
if ok { | ||
isMenu = menu.active | ||
if !isMenu && action == glfw.Press && (key == glfw.KeyLeftAlt || key == glfw.KeyRightAlt) && | ||
keyDesktopModifier == desktop.AltModifier { | ||
mbi := menu.Items[0].(*menuBarItem) | ||
mbi.Tapped(nil) | ||
} | ||
} | ||
if isMenu { | ||
if action == glfw.Release { | ||
menu.HandleKey(keyName) | ||
} | ||
return | ||
} | ||
|
||
if keyName == fyne.KeyTab && action != glfw.Release && !isMenu { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The release check appears duplicating 2 liones below? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. Will fix. |
||
if keyDesktopModifier == 0 { | ||
if action != glfw.Release { | ||
w.canvas.focusMgr.FocusNext(w.canvas.focused) | ||
|
@@ -950,7 +985,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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,39 @@ func DarkTheme() fyne.Theme { | |
return theme | ||
} | ||
|
||
// shade will darken a light color and lighten a light color by the given % value | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. % or pct should explain it's expected range better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It comes from the material design recomendations. The actual percents to lighten is twice the percent to darken. The shade for focus is 12% darken, but 24% lighten. In this way we need only one constant and the doubling is done in Shade(). I have improved the comments. |
||
// should use 4,8, 12 or 14 percent darken/lighten to implement google material design rules | ||
func Shade(c color.Color, pct uint32) color.Color { | ||
r, g, b, a := c.RGBA() | ||
if pct == 0 { | ||
return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. better would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct |
||
} | ||
if pct > 50 { | ||
pct = 50 | ||
} | ||
if r+g+b < 3*0x8080 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
// Lighten | ||
return color.NRGBA{ | ||
R: uint8((r + (0x10000-r)*pct/50) >> 8), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. percent / 50 is a strange seemting thing to do? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
} | ||
} else { | ||
// Darken | ||
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), | ||
} | ||
} | ||
} | ||
|
||
const PressedShade = 14 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these percentages? should they be public API? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
const HoveredShade = 4 | ||
const FocusedShade = 8 | ||
|
||
func (t *builtinTheme) BackgroundColor() color.Color { | ||
return t.background | ||
} | ||
|
@@ -117,7 +150,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 | ||
|
@@ -276,6 +309,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) | ||
} | ||
|
||
// HoverFocused 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() | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?