mirror of
https://github.com/koreader/koreader
synced 2024-11-18 03:25:46 +00:00
925fd647dc
Thanks to @poire-z for the idea, it's indeed much nicer this way ;).
491 lines
17 KiB
Lua
491 lines
17 KiB
Lua
local bit = require("bit")
|
|
local Device = require("device")
|
|
local Event = require("ui/event")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local logger = require("logger")
|
|
local UIManager = require("ui/uimanager")
|
|
local util = require("util")
|
|
--[[
|
|
Wrapper Widget that manages focus for a whole dialog
|
|
|
|
supports a 2D model of active elements
|
|
|
|
e.g.:
|
|
layout = {
|
|
{ textinput, textinput, item },
|
|
{ okbutton, cancelbutton, item },
|
|
{ nil, item, nil },
|
|
{ nil, item, nil },
|
|
{ nil, item, nil },
|
|
}
|
|
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.
|
|
|
|
but notice that this does _not_ do the layout for you,
|
|
it rather defines an abstract layout.
|
|
]]
|
|
local FocusManager = InputContainer:extend{
|
|
selected = nil, -- defaults to x=1, y=1
|
|
layout = nil, -- mandatory
|
|
movement_allowed = { x = true, y = true },
|
|
}
|
|
|
|
-- Only build the default mappings once on initialization, or when an external keyboard is (dis-)/connected.
|
|
-- We'll make copies during instantiation.
|
|
local KEY_EVENTS
|
|
local BUILTIN_KEY_EVENTS
|
|
local EXTRA_KEY_EVENTS
|
|
|
|
local function populateEventMappings()
|
|
KEY_EVENTS = {}
|
|
BUILTIN_KEY_EVENTS = {}
|
|
EXTRA_KEY_EVENTS = {}
|
|
|
|
if Device:hasDPad() then
|
|
local event_keys = {}
|
|
-- these will all generate the same event, just with different arguments
|
|
table.insert(event_keys, { "FocusUp", { { "Up" }, event = "FocusMove", args = {0, -1} } })
|
|
table.insert(event_keys, { "FocusRight", { { "Right" }, event = "FocusMove", args = {1, 0} } })
|
|
table.insert(event_keys, { "FocusDown", { { "Down" }, event = "FocusMove", args = {0, 1} } })
|
|
table.insert(event_keys, { "Press", { { "Press" }, event = "Press" } })
|
|
local FEW_KEYS_END_INDEX = #event_keys -- Few keys device: only setup up, down, right and press
|
|
|
|
table.insert(event_keys, { "FocusLeft", { { "Left" }, event = "FocusMove", args = {-1, 0} } })
|
|
local NORMAL_KEYS_END_INDEX = #event_keys
|
|
|
|
-- Advanced Feature: following event handlers can be enabled via settings.reader.lua
|
|
-- Key combinations (Sym+AA, Alt+Up, Tab, Shift+Tab and so on) are not used but shown as examples here
|
|
table.insert(event_keys, { "Hold", { { "Sym", "AA" }, event = "Hold" } })
|
|
-- half rows/columns move, it is helpful for slow device like Kindle DX to move quickly
|
|
table.insert(event_keys, { "HalfFocusUp", { { "Alt", "Up" }, event = "FocusHalfMove", args = {"up"} } })
|
|
table.insert(event_keys, { "HalfFocusRight", { { "Alt", "Right" }, event = "FocusHalfMove", args = {"right"} } })
|
|
table.insert(event_keys, { "HalfFocusDown", { { "Alt", "Down" }, event = "FocusHalfMove", args = {"down"} } })
|
|
table.insert(event_keys, { "HalfFocusLeft", { { "Alt", "Left" }, event = "FocusHalfMove", args = {"left"} } })
|
|
-- for PC navigation behavior support
|
|
table.insert(event_keys, { "FocusNext", { { "Tab" }, event = "FocusNext" } })
|
|
table.insert(event_keys, { "FocusPrevious", { { "Shift", "Tab" }, event = "FocusPrevious" } })
|
|
|
|
for i = 1, FEW_KEYS_END_INDEX do
|
|
local key_name = event_keys[i][1]
|
|
KEY_EVENTS[key_name] = event_keys[i][2]
|
|
BUILTIN_KEY_EVENTS[key_name] = event_keys[i][2]
|
|
end
|
|
if not Device:hasFewKeys() then
|
|
for i = FEW_KEYS_END_INDEX+1, NORMAL_KEYS_END_INDEX do
|
|
local key_name = event_keys[i][1]
|
|
KEY_EVENTS[key_name] = event_keys[i][2]
|
|
BUILTIN_KEY_EVENTS[key_name] = event_keys[i][2]
|
|
end
|
|
local focus_manager_setting = G_reader_settings:child("focus_manager")
|
|
-- Enable advanced feature, like Hold, FocusNext, FocusPrevious
|
|
-- Can also add extra arrow keys like using A, W, D, S for Left, Up, Right, Down
|
|
local alternative_keymaps = focus_manager_setting:readSetting("alternative_keymaps")
|
|
if type(alternative_keymaps) == "table" then
|
|
for i = 1, #event_keys do
|
|
local key_name = event_keys[i][1]
|
|
local alternative_keymap = alternative_keymaps[key_name]
|
|
if alternative_keymap then
|
|
local handler_defition = util.tableDeepCopy(event_keys[i][2])
|
|
handler_defition[1] = alternative_keymap -- replace sample key combinations
|
|
local new_event_key = "Alternative" .. key_name
|
|
KEY_EVENTS[new_event_key] = handler_defition
|
|
EXTRA_KEY_EVENTS[new_event_key] = handler_defition
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
populateEventMappings()
|
|
|
|
function FocusManager:_init()
|
|
InputContainer._init(self)
|
|
|
|
-- These *need* to be instance-specific, hence the copy
|
|
if not self.selected then
|
|
self.selected = { x = 1, y = 1 }
|
|
else
|
|
self.selected = {x = self.selected.x, y = self.selected.y }
|
|
end
|
|
|
|
-- Ditto, as each widget may choose their own custom key bindings
|
|
self.key_events = util.tableDeepCopy(KEY_EVENTS)
|
|
-- We should be fine with a simple ref for those, though
|
|
self.builtin_key_events = BUILTIN_KEY_EVENTS
|
|
self.extra_key_events = EXTRA_KEY_EVENTS
|
|
end
|
|
|
|
function FocusManager:isAlternativeKey(key)
|
|
for _, seq in pairs(self.extra_key_events) do
|
|
for _, oneseq in ipairs(seq) do
|
|
if key:match(oneseq) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function FocusManager:onFocusHalfMove(args)
|
|
if not self.layout then
|
|
return false
|
|
end
|
|
local direction = unpack(args)
|
|
local x, y = self.selected.x, self.selected.y
|
|
local row = self.layout[self.selected.y]
|
|
local dx, dy = 0, 0
|
|
if direction == "up" then
|
|
dy = - math.floor(#self.layout / 2)
|
|
if dy == 0 then
|
|
dy = -1
|
|
elseif dy + y <= 0 then
|
|
dy = -y + 1 -- first row
|
|
end
|
|
elseif direction == "down" then
|
|
dy = math.floor(#self.layout / 2)
|
|
if dy == 0 then
|
|
dy = 1
|
|
elseif dy + y > #self.layout then
|
|
dy = #self.layout - y -- last row
|
|
end
|
|
elseif direction == "left" then
|
|
dx = - math.floor(#row / 2)
|
|
if dx == 0 then
|
|
dx = -1
|
|
elseif dx + x <= 0 then
|
|
dx = -x + 1 -- first column
|
|
end
|
|
elseif direction == "right" then
|
|
dx = math.floor(#row / 2)
|
|
if dx == 0 then
|
|
dx = 1
|
|
elseif dx + x > #row then
|
|
dx = #row - y -- last column
|
|
end
|
|
end
|
|
return self:onFocusMove({dx, dy})
|
|
end
|
|
|
|
function FocusManager:onPress()
|
|
return self:sendTapEventToFocusedWidget()
|
|
end
|
|
|
|
function FocusManager:onHold()
|
|
return self:sendHoldEventToFocusedWidget()
|
|
end
|
|
|
|
-- for tab key
|
|
function FocusManager:onFocusNext()
|
|
if not self.layout then
|
|
return false
|
|
end
|
|
local x, y = self.selected.x, self.selected.y
|
|
local row = self.layout[y]
|
|
local dx, dy = 1, 0
|
|
if not row[x + dx] then -- beyond end of column, go to next row
|
|
dx, dy = 0, 1
|
|
end
|
|
return self:onFocusMove({dx, dy})
|
|
end
|
|
|
|
-- for backtab key
|
|
function FocusManager:onFocusPrevious()
|
|
if not self.layout then
|
|
return false
|
|
end
|
|
local x, y = self.selected.x, self.selected.y
|
|
local row = self.layout[y]
|
|
local dx, dy = -1, 0
|
|
if not row[x + dx] then -- beyond start of column, go to previous row
|
|
dx, dy = 0, -1
|
|
end
|
|
return self:onFocusMove({dx, dy})
|
|
end
|
|
|
|
function FocusManager:onFocusMove(args)
|
|
if not self.layout then -- allow parent focus manger to handle the event
|
|
return false
|
|
end
|
|
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
|
|
|
|
if not self.layout[self.selected.y] or not self.layout[self.selected.y][self.selected.x] then
|
|
logger.dbg("FocusManager: no currently selected widget found")
|
|
return true
|
|
end
|
|
local current_item = self.layout[self.selected.y][self.selected.x]
|
|
while true do
|
|
if not self.layout[self.selected.y + dy] then
|
|
--horizontal border, try to wraparound
|
|
if not self:_wrapAroundY(dy) then
|
|
break
|
|
end
|
|
elseif not self.layout[self.selected.y + dy][self.selected.x] then
|
|
--inner horizontal border, trying to be clever and step down
|
|
if not self:_verticalStep(dy) then
|
|
break
|
|
end
|
|
elseif not self.layout[self.selected.y + dy][self.selected.x + dx] then
|
|
--vertical border, try to wraparound
|
|
if not self:_wrapAroundX(dx) then
|
|
break
|
|
end
|
|
else
|
|
self.selected.y = self.selected.y + dy
|
|
self.selected.x = self.selected.x + dx
|
|
end
|
|
logger.dbg("FocusManager cursor position is:", self.selected.x, ",", self.selected.y)
|
|
|
|
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"))
|
|
-- 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.
|
|
UIManager:setDirty(self.show_parent or self, "fast")
|
|
break
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function FocusManager:onPhysicalKeyboardConnected()
|
|
-- Re-initialize with new keys info.
|
|
populateEventMappings()
|
|
-- We can't just call FocusManager._init because it will *reset* the mappings, losing our widget-specific ones (if any),
|
|
-- and it'll call InputContainer._init, which *also* resets the touch zones.
|
|
-- Instead, we'll just do a merge ourselves.
|
|
util.tableMerge(self.key_events, KEY_EVENTS)
|
|
-- populateEventMappings replaces these, so, update our refs
|
|
self.builtin_key_events = BUILTIN_KEY_EVENTS
|
|
self.extra_key_events = EXTRA_KEY_EVENTS
|
|
end
|
|
|
|
function FocusManager:onPhysicalKeyboardDisconnected()
|
|
local prev_key_events = KEY_EVENTS
|
|
populateEventMappings()
|
|
|
|
-- If we still have keys, remove what disappeared from KEY_EVENTS from self.key_events (if any).
|
|
if Device:hasKeys() then
|
|
-- NOTE: This is slightly overkill, we could very well live with a few unreachable mappings for the rest of this widget's life ;).
|
|
for k, _ in pairs(prev_key_events) do
|
|
if not KEY_EVENTS[k] then
|
|
self.key_events[k] = nil
|
|
end
|
|
end
|
|
else
|
|
-- If we longer have keys at all, that's easy ;).
|
|
self.key_events = {}
|
|
end
|
|
self.builtin_key_events = BUILTIN_KEY_EVENTS
|
|
self.extra_key_events = EXTRA_KEY_EVENTS
|
|
end
|
|
|
|
-- constant, used to reset focus widget after layout recreation
|
|
-- not send Unfocus event
|
|
FocusManager.NOT_UNFOCUS = 1
|
|
-- not need to send Focus event
|
|
FocusManager.NOT_FOCUS = 2
|
|
|
|
--- Move focus to specified widget
|
|
function FocusManager:moveFocusTo(x, y, focus_flags)
|
|
focus_flags = focus_flags or 0
|
|
if not self.layout then
|
|
return false
|
|
end
|
|
local current_item = nil
|
|
if self.layout[self.selected.y] then
|
|
current_item = self.layout[self.selected.y][self.selected.x]
|
|
end
|
|
local target_item = nil
|
|
if self.layout[y] then
|
|
target_item = self.layout[y][x]
|
|
end
|
|
if target_item then
|
|
logger.dbg("FocusManager: Move focus position to:", x, ",", y)
|
|
self.selected.x = x
|
|
self.selected.y = y
|
|
-- widget create new layout on update, previous may be removed from new layout.
|
|
if Device:hasDPad() then
|
|
if not bit.band(focus_flags, FocusManager.NOT_UNFOCUS) and current_item and current_item ~= target_item then
|
|
current_item:handleEvent(Event:new("Unfocus"))
|
|
end
|
|
if not bit.band(focus_flags, FocusManager.NOT_FOCUS) then
|
|
target_item:handleEvent(Event:new("Focus"))
|
|
UIManager:setDirty(self.show_parent or self, "fast")
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- 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[self.selected.y][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)
|
|
local y = self.selected.y
|
|
while self.layout[y - dy] do
|
|
y = y - dy
|
|
end
|
|
if y ~= self.selected.y then
|
|
self.selected.y = y
|
|
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
|
|
|
|
function FocusManager:_verticalStep(dy)
|
|
local x = self.selected.x
|
|
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
|
|
--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
|
|
return true
|
|
end
|
|
|
|
function FocusManager:getFocusItem()
|
|
if not self.layout then
|
|
return nil
|
|
end
|
|
return self.layout[self.selected.y][self.selected.x]
|
|
end
|
|
|
|
function FocusManager:_sendGestureEventToFocusedWidget(gesture)
|
|
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
|
|
logger.dbg("FocusManager: Send", gesture, "to", point.x , ",", point.y)
|
|
UIManager:sendEvent(Event:new("Gesture", {
|
|
ges = gesture,
|
|
pos = point,
|
|
}))
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function FocusManager:sendTapEventToFocusedWidget()
|
|
return self:_sendGestureEventToFocusedWidget("tap")
|
|
end
|
|
|
|
function FocusManager:sendHoldEventToFocusedWidget()
|
|
return self:_sendGestureEventToFocusedWidget("hold")
|
|
end
|
|
|
|
function FocusManager:mergeLayoutInVertical(child, pos)
|
|
if not child.layout then
|
|
return
|
|
end
|
|
if not pos then
|
|
pos = #self.layout + 1 -- end of row
|
|
end
|
|
for _, row in ipairs(child.layout) do
|
|
table.insert(self.layout, pos, row)
|
|
pos = pos + 1
|
|
end
|
|
child:disableFocusManagement(self)
|
|
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(self)
|
|
end
|
|
|
|
function FocusManager:disableFocusManagement(parent)
|
|
self._parent = parent
|
|
-- unfocus current widget in current child container
|
|
-- parent container will call refocusWidget to focus another one
|
|
local row = self.layout[self.selected.y]
|
|
if row and row[self.selected.x] then
|
|
row[self.selected.x]:handleEvent(Event:new("Unfocus"))
|
|
end
|
|
self.layout = nil -- turn off focus feature
|
|
end
|
|
|
|
-- constant for refocusWidget method to ease code reading
|
|
FocusManager.RENDER_IN_NEXT_TICK = true
|
|
|
|
--- Container calls this method to re-set focus widget style
|
|
--- Some container regenerate layout on update and lose focus style
|
|
function FocusManager:refocusWidget(nextTick)
|
|
if not self._parent then
|
|
if not nextTick then
|
|
self:moveFocusTo(self.selected.x, self.selected.y)
|
|
else
|
|
-- sometimes refocusWidget called in widget's action callback
|
|
-- widget may force repaint after callback, like Button with vsync = true
|
|
-- then focus style will be lost, set focus style to next tick to make sure focus style painted
|
|
UIManager:nextTick(function()
|
|
self:moveFocusTo(self.selected.x, self.selected.y)
|
|
end)
|
|
end
|
|
else
|
|
self._parent:refocusWidget(nextTick)
|
|
end
|
|
end
|
|
|
|
return FocusManager
|