2018-03-30 10:46:36 +00:00
|
|
|
local Device = require("device")
|
2013-10-22 15:11:31 +00:00
|
|
|
local Event = require("ui/event")
|
2018-03-18 10:42:35 +00:00
|
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
|
|
local logger = require("logger")
|
2013-10-22 15:11:31 +00:00
|
|
|
local UIManager = require("ui/uimanager")
|
2012-06-10 15:52:09 +00:00
|
|
|
--[[
|
|
|
|
Wrapper Widget that manages focus for a whole dialog
|
|
|
|
|
|
|
|
supports a 2D model of active elements
|
|
|
|
|
|
|
|
e.g.:
|
2014-03-13 13:52:43 +00:00
|
|
|
layout = {
|
2018-03-14 21:16:38 +00:00
|
|
|
{ textinput, textinput, item },
|
|
|
|
{ okbutton, cancelbutton, item },
|
|
|
|
{ nil, item, nil },
|
|
|
|
{ nil, item, nil },
|
|
|
|
{ nil, item, nil },
|
2014-03-13 13:52:43 +00:00
|
|
|
}
|
2018-03-14 21:16:38 +00:00
|
|
|
Navigate the layout by trying to avoid not set or nil value.
|
|
|
|
Provide a simple wrap around in the vertical direction.
|
|
|
|
The first element of the first table must be valid to ensure
|
|
|
|
to not get stuck in an invalid position.
|
2012-06-10 15:52:09 +00:00
|
|
|
|
|
|
|
but notice that this does _not_ do the layout for you,
|
|
|
|
it rather defines an abstract layout.
|
|
|
|
]]
|
2013-10-18 20:38:07 +00:00
|
|
|
local FocusManager = InputContainer:new{
|
2014-03-13 13:52:43 +00:00
|
|
|
selected = nil, -- defaults to x=1, y=1
|
|
|
|
layout = nil, -- mandatory
|
2022-01-25 00:32:46 +00:00
|
|
|
movement_allowed = { x = true, y = true },
|
2012-06-10 15:52:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function FocusManager:init()
|
2015-09-13 08:08:31 +00:00
|
|
|
if not self.selected then
|
|
|
|
self.selected = { x = 1, y = 1 }
|
|
|
|
end
|
A few graphics fixes after #4541 (#4554)
* Various FocusManager related tweaks to limit its usage to devices with a DPad, and prevent initial button highlights in Dialogs on devices where it makes no sense (i.e., those without a DPad. And even on DPad devices, I'm not even sure how we'd go about making one of those pop up anyway, because no Touch ;)!).
* One mysterious fix to text-only Buttons so that the flash_ui highlight always works, and always honors `FrameContainer`'s pill shape. (Before that, an unhighlight on a text button with a callback that didn't repaint anything [say, the find first/find last buttons in the Reader's search bar when you're already on the first/last match] would do a square black highlight, and a white pill-shaped unhighlight (leaving the black corners visible)).
The workaround makes *absolutely* no sense to me (as `self[1] -> self.frame`, AFAICT), but it works, and ensures all highlights/unhighlights are pill-shaped, so at least we're not doing maths for rounded corners for nothing ;).
2019-02-07 23:56:32 +00:00
|
|
|
|
|
|
|
if Device:hasDPad() then
|
2018-03-30 10:46:36 +00:00
|
|
|
self.key_events = {
|
|
|
|
-- these will all generate the same event, just with different arguments
|
|
|
|
FocusUp = { {"Up"}, doc = "move focus up", event = "FocusMove", args = {0, -1} },
|
|
|
|
FocusDown = { {"Down"}, doc = "move focus down", event = "FocusMove", args = {0, 1} },
|
|
|
|
FocusLeft = { {"Left"}, doc = "move focus left", event = "FocusMove", args = {-1, 0} },
|
|
|
|
FocusRight = { {"Right"}, doc = "move focus right", event = "FocusMove", args = {1, 0} },
|
|
|
|
}
|
2020-06-04 11:26:18 +00:00
|
|
|
if Device:hasFewKeys() then
|
|
|
|
self.key_events.FocusLeft = nil
|
|
|
|
end
|
2018-03-30 10:46:36 +00:00
|
|
|
end
|
2012-06-10 15:52:09 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function FocusManager:onFocusMove(args)
|
2022-01-25 00:32:46 +00:00
|
|
|
if not self.layout then -- allow parent focus manger to handle the event
|
|
|
|
return false
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
local dx, dy = unpack(args)
|
|
|
|
|
|
|
|
if (dx ~= 0 and not self.movement_allowed.x)
|
|
|
|
or (dy ~= 0 and not self.movement_allowed.y) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2022-01-25 00:32:46 +00:00
|
|
|
if not self.layout[self.selected.y] or not self.layout[self.selected.y][self.selected.x] then
|
2014-03-13 13:52:43 +00:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
local current_item = self.layout[self.selected.y][self.selected.x]
|
|
|
|
while true do
|
2018-03-14 21:16:38 +00:00
|
|
|
if not self.layout[self.selected.y + dy] then
|
2018-03-18 10:42:35 +00:00
|
|
|
--horizontal border, try to wraparound
|
2020-06-27 06:43:28 +00:00
|
|
|
if not self:_wrapAroundY(dy) then
|
2014-03-13 13:52:43 +00:00
|
|
|
break
|
|
|
|
end
|
2018-03-21 11:21:48 +00:00
|
|
|
elseif not self.layout[self.selected.y + dy][self.selected.x] then
|
|
|
|
--inner horizontal border, trying to be clever and step down
|
2018-03-30 10:46:36 +00:00
|
|
|
if not self:_verticalStep(dy) then
|
|
|
|
break
|
|
|
|
end
|
2018-03-14 21:16:38 +00:00
|
|
|
elseif not self.layout[self.selected.y + dy][self.selected.x + dx] then
|
2020-06-27 06:43:28 +00:00
|
|
|
--vertical border, try to wraparound
|
|
|
|
if not self:_wrapAroundX(dx) then
|
|
|
|
break
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
else
|
|
|
|
self.selected.y = self.selected.y + dy
|
2018-03-14 21:16:38 +00:00
|
|
|
self.selected.x = self.selected.x + dx
|
2017-10-07 10:11:00 +00:00
|
|
|
end
|
2018-03-21 11:21:48 +00:00
|
|
|
logger.dbg("Cursor position : ".. self.selected.y .." : "..self.selected.x)
|
2014-03-13 13:52:43 +00:00
|
|
|
|
|
|
|
if self.layout[self.selected.y][self.selected.x] ~= current_item
|
|
|
|
or not self.layout[self.selected.y][self.selected.x].is_inactive then
|
|
|
|
-- we found a different object to focus
|
|
|
|
current_item:handleEvent(Event:new("Unfocus"))
|
|
|
|
self.layout[self.selected.y][self.selected.x]:handleEvent(Event:new("Focus"))
|
A few graphics fixes after #4541 (#4554)
* Various FocusManager related tweaks to limit its usage to devices with a DPad, and prevent initial button highlights in Dialogs on devices where it makes no sense (i.e., those without a DPad. And even on DPad devices, I'm not even sure how we'd go about making one of those pop up anyway, because no Touch ;)!).
* One mysterious fix to text-only Buttons so that the flash_ui highlight always works, and always honors `FrameContainer`'s pill shape. (Before that, an unhighlight on a text button with a callback that didn't repaint anything [say, the find first/find last buttons in the Reader's search bar when you're already on the first/last match] would do a square black highlight, and a white pill-shaped unhighlight (leaving the black corners visible)).
The workaround makes *absolutely* no sense to me (as `self[1] -> self.frame`, AFAICT), but it works, and ensures all highlights/unhighlights are pill-shaped, so at least we're not doing maths for rounded corners for nothing ;).
2019-02-07 23:56:32 +00:00
|
|
|
-- Trigger a fast repaint, this does not count toward a flashing eink refresh
|
|
|
|
-- NOTE: Ideally, we'd only have to repaint the specific subwidget we're highlighting,
|
|
|
|
-- but we may not know its exact coordinates, so, redraw the parent widget instead.
|
2018-03-14 21:16:38 +00:00
|
|
|
UIManager:setDirty(self.show_parent or self, "fast")
|
2014-03-13 13:52:43 +00:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return true
|
2012-06-10 18:14:29 +00:00
|
|
|
end
|
|
|
|
|
2020-06-27 06:43:28 +00:00
|
|
|
--- Go to the last valid item directly left or right of the current item.
|
|
|
|
-- @return false if none could be found
|
|
|
|
function FocusManager:_wrapAroundX(dx)
|
|
|
|
local x = self.selected.x
|
|
|
|
while self.layout[x - dx] do
|
|
|
|
x = x - dx
|
|
|
|
end
|
|
|
|
if x ~= self.selected.x then
|
|
|
|
self.selected.x = x
|
|
|
|
if not self.layout[self.selected.y][self.selected.x] then
|
|
|
|
--call verticalStep on the current line to perform the search
|
|
|
|
return self:_verticalStep(0)
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Go to the last valid item directly above or below the current item.
|
|
|
|
-- @return false if none could be found
|
|
|
|
function FocusManager:_wrapAroundY(dy)
|
2018-03-14 21:16:38 +00:00
|
|
|
local y = self.selected.y
|
2018-03-21 11:21:48 +00:00
|
|
|
while self.layout[y - dy] do
|
2018-03-14 21:16:38 +00:00
|
|
|
y = y - dy
|
|
|
|
end
|
|
|
|
if y ~= self.selected.y then
|
|
|
|
self.selected.y = y
|
2018-03-21 11:21:48 +00:00
|
|
|
if not self.layout[self.selected.y][self.selected.x] then
|
|
|
|
--call verticalStep on the current line to perform the search
|
2018-03-30 10:46:36 +00:00
|
|
|
return self:_verticalStep(0)
|
2018-03-21 11:21:48 +00:00
|
|
|
end
|
2018-03-14 21:16:38 +00:00
|
|
|
return true
|
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
2012-06-10 18:14:29 +00:00
|
|
|
end
|
2013-10-18 20:38:07 +00:00
|
|
|
|
2018-03-21 11:21:48 +00:00
|
|
|
function FocusManager:_verticalStep(dy)
|
|
|
|
local x = self.selected.x
|
2018-03-30 10:46:36 +00:00
|
|
|
if type(self.layout[self.selected.y + dy]) ~= "table" or self.layout[self.selected.y + dy] == {} then
|
|
|
|
logger.err("[FocusManager] : Malformed layout")
|
|
|
|
return false
|
|
|
|
end
|
2018-03-21 11:21:48 +00:00
|
|
|
--looking for the item on the line below, the closest on the left side
|
|
|
|
while not self.layout[self.selected.y + dy][x] do
|
|
|
|
x = x - 1
|
|
|
|
if x == 0 then
|
|
|
|
--if he is not on the left, must be on the right
|
|
|
|
x = self.selected.x
|
|
|
|
while not self.layout[self.selected.y + dy][x] do
|
|
|
|
x = x + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self.selected.x = x
|
|
|
|
self.selected.y = self.selected.y + dy
|
2018-03-30 10:46:36 +00:00
|
|
|
return true
|
2018-03-21 11:21:48 +00:00
|
|
|
end
|
|
|
|
|
2015-09-13 08:09:00 +00:00
|
|
|
function FocusManager:getFocusItem()
|
2022-01-25 00:32:46 +00:00
|
|
|
if not self.layout then
|
|
|
|
return nil
|
|
|
|
end
|
2015-09-13 08:09:00 +00:00
|
|
|
return self.layout[self.selected.y][self.selected.x]
|
|
|
|
end
|
|
|
|
|
2022-01-25 00:32:46 +00:00
|
|
|
function FocusManager:sendTapEventToFocusedWidget()
|
|
|
|
local focused_widget = self:getFocusItem()
|
|
|
|
if focused_widget then
|
|
|
|
-- center of widget position
|
|
|
|
local point = focused_widget.dimen:copy()
|
|
|
|
point.x = point.x + point.w / 2
|
|
|
|
point.y = point.y + point.h / 2
|
|
|
|
point.w = 0
|
|
|
|
point.h = 0
|
|
|
|
UIManager:sendEvent(Event:new("Gesture", {
|
|
|
|
ges = "tap",
|
|
|
|
pos = point,
|
|
|
|
}))
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
function FocusManager:mergeLayoutInVertical(child)
|
|
|
|
if not child.layout then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
for _, row in ipairs(child.layout) do
|
|
|
|
table.insert(self.layout, row)
|
|
|
|
end
|
|
|
|
child:disableFocusManagement()
|
|
|
|
end
|
|
|
|
|
|
|
|
function FocusManager:mergeLayoutInHorizontal(child)
|
|
|
|
if not child.layout then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
for i, row in ipairs(child.layout) do
|
|
|
|
local prow = self.layout[i]
|
|
|
|
if not prow then
|
|
|
|
prow = {}
|
|
|
|
self.layout[i] = prow
|
|
|
|
end
|
|
|
|
for _, widget in ipairs(row) do
|
|
|
|
table.insert(prow, widget)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
child:disableFocusManagement()
|
|
|
|
end
|
|
|
|
|
|
|
|
function FocusManager:disableFocusManagement()
|
|
|
|
self.layout = nil -- turn off focus feature
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Container call this method after init to let first widget render in focus style
|
|
|
|
function FocusManager:focusTopLeftWidget()
|
2022-01-28 21:55:26 +00:00
|
|
|
if Device:hasDPad() then
|
|
|
|
-- trigger selected widget in focused style
|
|
|
|
self:onFocusMove({0, 0})
|
|
|
|
end
|
2022-01-25 00:32:46 +00:00
|
|
|
end
|
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
return FocusManager
|