add keyboard focus

This commit is contained in:
airstruck
2015-10-31 04:21:50 -04:00
parent b3b4f90b23
commit 1e668f8f09
13 changed files with 233 additions and 79 deletions

View File

@@ -17,6 +17,9 @@ local style = {
align = 'center middle',
width = 48,
},
toolButton_focused = {
slices = 'defer',
},
toolButton_not_hovered = {
slices = false,
},
@@ -62,7 +65,7 @@ local mainForm = { title = "Test window", id = 'mainWindow', type = 'panel',
{ type = 'sash', width = 4, },
{ type = 'panel', id = 'rightSideBox', width = 200,
{ type = 'panel', text = 'A slider', align = 'bottom', height = 24, padding = 4 },
{ type = 'slider', height = 32, margin = 4, id = 'slidey', },
{ type = 'slider', height = 32, margin = 4, id = 'slidey', value = 0 },
{ type = 'panel', text = 'A stepper', align = 'bottom', height = 24, padding = 4 },
{ type = 'stepper', height = 32, margin = 4, options = {
{ value = 1, text = 'Thing One' },
@@ -93,8 +96,8 @@ layout.leftSideBox:addChild {
align = 'middle right'
}
layout.slidey:onPressDrag(function (event)
layout.progressBar.value = layout.slidey.value
layout.slidey:onChange(function (event)
layout.progressBar:setValue(event.value)
layout.progressBar:reshape()
end)

View File

@@ -18,9 +18,18 @@ function Event:bind (target, callback)
end
local eventNames = {
'Reshape', 'Display', 'KeyPress', 'KeyRelease', 'TextInput', 'Move',
'Enter', 'Leave', 'PressEnter', 'PressLeave',
'PressStart', 'PressEnd', 'PressDrag', 'PressMove', 'Press',
'Reshape', -- widget's dimensions changed
'Display', -- widget is being drawn
'KeyPress', 'KeyRelease', -- keyboard key pressed/released
'TextInput', -- text is entered
'Move', -- cursor moves, no button pressed
'Enter', 'Leave', -- cursor enters/leaves widget, no button pressed
'PressEnter', 'PressLeave', -- cursor enters/leaves widget, button pressed
'PressStart', 'PressEnd', -- cursor or accelerator key press starts/ends
'PressDrag', -- pressed cursor moves, targets originating widget
'PressMove', -- pressed cursor moves, targets widget at cursor position
'Press', -- cursor is pressed and released on same widget
'Change', -- widget's value changed via Widget:setValue
}
local weakKeyMeta = { __mode = 'k' }

View File

@@ -14,15 +14,6 @@ function Input:constructor (layout)
self.passedWidgets = setmetatable({}, weakValueMeta)
end
function Input:bubbleEvent (eventName, widget, data)
local event = Event[eventName]
for ancestor in widget:getAncestors(true) do
local result = event:emit(ancestor, data)
if result ~= nil then return result end
end
return event:emit(self.layout, data)
end
function Input:handleDisplay ()
local root = self.layout.root
if root then Renderer:render(root) end
@@ -31,40 +22,23 @@ end
function Input:handleKeyPress (key, x, y)
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
local result = self:bubbleEvent('KeyPress', widget, {
target = widget,
local result = widget:bubbleEvent('KeyPress', {
key = key, x = x, y = y
})
if result ~= nil then return result end
local acceleratedWidget = self.layout.accelerators[key]
if acceleratedWidget then
acceleratedWidget.hovered = true
self:handlePressStart(key, x, y, acceleratedWidget, key)
end
end
function Input:handleKeyRelease (key, x, y)
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
local result = self:bubbleEvent('KeyRelease', widget, {
target = widget,
local result = widget:bubbleEvent('KeyRelease', {
key = key, x = x, y = y
})
if result ~= nil then return result end
local acceleratedWidget = self.layout.accelerators[key]
if acceleratedWidget then
acceleratedWidget.hovered = false
self:handlePressEnd(key, x, y, acceleratedWidget, key)
end
end
function Input:handleTextInput (text, x, y)
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
self:bubbleEvent('TextInput', widget, {
target = widget,
widget:bubbleEvent('TextInput', {
text = text, x = x, y = y
})
end
@@ -78,21 +52,18 @@ function Input:handleMove (x, y)
end
widget.hovered = true
end
self:bubbleEvent('Move', widget, {
target = widget,
widget:bubbleEvent('Move', {
oldTarget = previousWidget,
x = x, y = y
})
if widget ~= previousWidget then
if previousWidget then
self:bubbleEvent('Leave', previousWidget, {
target = previousWidget,
previousWidget:bubbleEvent('Leave', {
newTarget = widget,
x = x, y = y
})
end
self:bubbleEvent('Enter', widget, {
target = widget,
widget:bubbleEvent('Enter', {
oldTarget = previousWidget,
x = x, y = y
})
@@ -106,15 +77,13 @@ function Input:handlePressedMove (x, y)
local originWidget = self.pressedWidgets[button]
local passedWidget = self.passedWidgets[button]
if originWidget then
self:bubbleEvent('PressDrag', originWidget, {
target = originWidget,
originWidget:bubbleEvent('PressDrag', {
newTarget = widget,
button = button,
x = x, y = y
})
if (widget == passedWidget) then
self:bubbleEvent('PressMove', widget, {
target = widget,
widget:bubbleEvent('PressMove', {
origin = originWidget,
button = button,
x = x, y = y
@@ -122,16 +91,14 @@ function Input:handlePressedMove (x, y)
else
originWidget.pressed = (widget == originWidget) or nil
if passedWidget then
self:bubbleEvent('PressLeave', passedWidget, {
target = passedWidget,
passedWidget:bubbleEvent('PressLeave', {
newTarget = widget,
origin = originWidget,
button = button,
x = x, y = y
})
end
self:bubbleEvent('PressEnter', widget, {
target = widget,
widget:bubbleEvent('PressEnter', {
oldTarget = passedWidget,
origin = originWidget,
button = button,
@@ -148,8 +115,8 @@ function Input:handlePressStart (button, x, y, widget, accelerator)
widget.pressed = true
self.pressedWidgets[button] = widget
self.passedWidgets[button] = widget
self:bubbleEvent('PressStart', widget, {
target = widget,
self.layout:tryFocus(widget)
widget:bubbleEvent('PressStart', {
button = button,
accelerator = accelerator,
x = x, y = y
@@ -160,15 +127,13 @@ function Input:handlePressEnd (button, x, y, widget, accelerator)
local widget = widget or self.layout:getWidgetAt(x, y)
local originWidget = self.pressedWidgets[button]
originWidget.pressed = nil
self:bubbleEvent('PressEnd', widget, {
target = widget,
widget:bubbleEvent('PressEnd', {
origin = originWidget,
accelerator = accelerator,
button = button, x = x, y = y
})
if (widget == originWidget) then
self:bubbleEvent('Press', widget, {
target = widget,
widget:bubbleEvent('Press', {
button = button,
accelerator = accelerator,
x = x, y = y

View File

@@ -12,7 +12,6 @@ local Layout = Base:extend()
local weakValueMeta = { __mode = 'v' }
function Layout:constructor (data)
self.widgets = setmetatable({}, weakValueMeta)
self.accelerators = {}
self:setStyle()
self:setTheme()
@@ -21,8 +20,134 @@ function Layout:constructor (data)
self.isManagingInput = false
self.hooks = {}
self.root = Widget(self, data or {})
self:addDefaultHandlers ()
end
-- focus a widget if it's focusable, and return success
function Layout:tryFocus (widget)
if widget.canFocus then
if self.focusedWidget then
self.focusedWidget.focused = false
end
widget.focused = true
self.focusedWidget = widget
return true
end
end
-- get the next widget, cycling back around to root (depth first)
function Layout:getNextWidget (widget)
if #widget.children > 0 then
return widget.children[1]
end
for ancestor in widget:eachAncestor(true) do
local nextWidget = ancestor:getNext()
if nextWidget then return nextWidget end
end
return self.root
end
-- get the last child of the last child of the last child of the...
local function getGreatestDescendant (widget)
while #widget.children > 0 do
local children = widget.children
widget = children[#children]
end
return widget
end
-- get the previous widget, cycling back around to root (depth first)
function Layout:getPreviousWidget (widget)
if widget == self.root then
return getGreatestDescendant(widget)
end
for ancestor in widget:eachAncestor(true) do
local previousWidget = ancestor:getPrevious()
if previousWidget then
return getGreatestDescendant(previousWidget)
end
if ancestor ~= widget then return ancestor end
end
return self.root
end
-- focus next focusable widget (depth first)
function Layout:focusNextWidget ()
local widget = self.focusedWidget or self.root
local nextWidget = self:getNextWidget(widget)
while nextWidget ~= widget do
if self:tryFocus(nextWidget) then return end
nextWidget = self:getNextWidget(nextWidget)
end
end
-- focus previous focusable widget (depth first)
function Layout:focusPreviousWidget ()
local widget = self.focusedWidget or self.root
local previousWidget = self:getPreviousWidget(widget)
while previousWidget ~= widget do
if self:tryFocus(previousWidget) then return end
previousWidget = self:getPreviousWidget(previousWidget)
end
end
-- handlers for keyboard accelerators and tab focus
function Layout:addDefaultHandlers ()
self:onKeyPress(function (event)
-- tab / shift-tab cycles focused widget
if event.key == 'tab' then
if love.keyboard.isDown('lshift', 'rshift') then
self:focusPreviousWidget()
else
self:focusNextWidget()
end
return
end
-- space / enter presses focused widget
local widget = self.focusedWidget
if widget and (event.key == 'return' or event.key == 'space') then
self.input:handlePressStart(event.key, event.x, event.y,
widget, event.key)
return
end
-- accelerators
local acceleratedWidget = self.accelerators[event.key]
if acceleratedWidget then
acceleratedWidget.hovered = true
self.input:handlePressStart(event.key, event.x, event.y,
acceleratedWidget, event.key)
end
end)
self:onKeyRelease(function (event)
-- space / enter presses focused widget
local widget = self.focusedWidget
if widget and (event.key == 'return' or event.key == 'space') then
self.input:handlePressEnd(event.key, event.x, event.y,
widget, event.key)
return
end
-- accelerators
local acceleratedWidget = self.accelerators[event.key]
if acceleratedWidget then
acceleratedWidget.hovered = false
self.input:handlePressEnd(event.key, event.x, event.y,
acceleratedWidget, event.key)
end
end)
end
-- set the style from a definition table or function
function Layout:setStyle (rules)
if type(rules) == 'function' then
rules = rules()
@@ -30,6 +155,7 @@ function Layout:setStyle (rules)
self.style = Style(rules or {}, { 'id', 'style' })
end
-- set the theme from a definition table or function
function Layout:setTheme (rules)
if type(rules) == 'function' then
rules = rules()
@@ -37,6 +163,7 @@ function Layout:setTheme (rules)
self.theme = Style(rules or {}, { 'type' })
end
-- show the layout (hooks all appropriate love events and callbacks)
function Layout:show ()
local root = self.root
local width = root.width
@@ -55,6 +182,7 @@ function Layout:show ()
root:reshape()
end
-- hide the layout (unhooks love events and callbacks)
function Layout:hide ()
if not self.isManagingInput then
return
@@ -89,7 +217,6 @@ function Layout:addWidget (widget)
if widget.key then
self.accelerators[widget.key] = widget
end
table.insert(self.widgets, widget)
end
-- event stuff

View File

@@ -69,7 +69,7 @@ function Style:eachName (object)
end
return function ()
if not checkLookupProp() then return end
local specialName = getSpecialName { 'pressed', 'hovered' }
local specialName = getSpecialName { 'pressed', 'focused', 'hovered' }
if specialName then return specialName end
lookupPropIndex = lookupPropIndex + 1
return lookupProp[lookupPropIndex]

View File

@@ -9,29 +9,23 @@ return function (config)
local highlight = config.highlight or { 180, 180, 255 }
return {
panel = {
background = backColor,
},
button = {
align = 'center middle',
padding = 6,
slices = RESOURCE .. 'button.png',
minimumWidth = 24,
minimumHeight = 24
minimumHeight = 24,
canFocus = true
},
button_hovered = {
slices = RESOURCE .. 'button_hovered.png'
},
button_focused = {
slices = RESOURCE .. 'button_focused.png',
},
button_pressed = {
slices = RESOURCE .. 'button_pressed.png',
},
text = {
align = 'left middle',
slices = RESOURCE .. 'text.png',
padding = 6,
minimumWidth = 24,
minimumHeight = 24
},
sash = {
background = lineColor
},
@@ -44,6 +38,9 @@ return function (config)
minimumWidth = 24,
minimumHeight = 24
},
panel = {
background = backColor,
},
progress = {
slices = RESOURCE .. 'button_pressed.png',
padding = 0,
@@ -59,6 +56,17 @@ return function (config)
},
stepper = {
},
text = {
align = 'left middle',
slices = RESOURCE .. 'text.png',
padding = 6,
minimumWidth = 24,
minimumHeight = 24,
canFocus = true,
},
text_focused = {
slices = RESOURCE .. 'text_focused.png',
},
}
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -9,6 +9,7 @@ Event.injectBinders(Widget)
Widget.isWidget = true
Widget.typeDecorators = {
button = require(ROOT .. 'widget.button'),
progress = require(ROOT .. 'widget.progress'),
sash = require(ROOT .. 'widget.sash'),
slider = require(ROOT .. 'widget.slider'),
@@ -34,7 +35,7 @@ local function new (Widget, layout, self)
if value ~= nil then return value end
local style = self.layout.style
value = style and style:getProperty(self, property)
if value ~= nil then return value end
if value ~= nil and value ~= 'defer' then return value end
local theme = self.layout.theme
return theme and theme:getProperty(self, property)
end
@@ -56,7 +57,29 @@ local function new (Widget, layout, self)
return self
end
function Widget:bubbleEvent (eventName, data)
local event = Event[eventName]
data = data or {}
data.target = self
for ancestor in self:eachAncestor(true) do
local result = event:emit(ancestor, data)
if result ~= nil then return result end
end
return event:emit(self.layout, data)
end
function Widget:setValue (value)
local oldValue = self.value
self.value = value
self:bubbleEvent('Change', {
value = value,
oldValue = oldValue,
})
end
function Widget:getPrevious ()
if not self.parent then return end
local siblings = self.parent.children
for i, widget in ipairs(siblings) do
if widget == self then return siblings[i - 1] end
@@ -64,6 +87,7 @@ function Widget:getPrevious ()
end
function Widget:getNext ()
if not self.parent then return end
local siblings = self.parent.children
for i, widget in ipairs(siblings) do
if widget == self then return siblings[i + 1] end
@@ -76,7 +100,6 @@ function Widget:addChild (data)
table.insert(self.children, child)
child.parent = self
layout:addWidget(child)
return child
end
@@ -236,7 +259,7 @@ function Widget:isAt (x, y)
return (x1 < x) and (x2 > x) and (y1 < y) and (y2 > y)
end
function Widget:getAncestors (includeSelf)
function Widget:eachAncestor (includeSelf)
local instance = includeSelf and self or self.parent
return function()
local widget = instance

3
luigi/widget/button.lua Normal file
View File

@@ -0,0 +1,3 @@
return function (self)
end

View File

@@ -1,6 +1,11 @@
return function (self)
self.value = 0.5
local function clamp (value)
return value < 0 and 0 or value > 1 and 1 or value
end
self:setValue(clamp(self.value or 0.5))
self.step = self.step or 0.01
self.flow = 'x' -- TODO: support vertical slider
local spacer = self:addChild()
@@ -13,22 +18,32 @@ return function (self)
}
local function unpress ()
thumb.pressed = false
thumb.pressed = false -- don't make the thumb appear pushed in
return false -- don't press thumb on focused keyboard activation
end
thumb:onPressStart(unpress)
thumb:onPressEnter(unpress)
thumb:onKeyPress(function (event)
local key = event.key
if key == 'left' or key == 'down' then
self:setValue(clamp(self.value - self.step))
self:reshape()
elseif event.key == 'right' or key == 'up' then
self:setValue(clamp(self.value + self.step))
self:reshape()
end
end)
local function press (event)
local x1, y1, x2, y2 = self:getRectangle(true, true)
self.value = (event.x - x1) / (x2 - x1)
if self.value < 0 then self.value = 0 end
if self.value > 1 then self.value = 1 end
self:setValue(clamp((event.x - x1) / (x2 - x1)))
self:reshape()
self.layout:tryFocus(thumb)
end
self:onPressStart(press)
self:onPressDrag(press)
self:onEnter(function (event)

View File

@@ -14,6 +14,7 @@ return function (self)
type = 'text',
align = 'middle center',
margin = 0,
canFocus = false,
}
local increment = self:addChild {
@@ -31,7 +32,7 @@ return function (self)
local function updateValue ()
if not self.options then return end
local option = self.options[self.index]
self.value = option.value
self:setValue(option.value)
view.text = option.text
end