mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
* FocusManager: Fix `focus_flags` check in `moveFocusTo` (0 is truthy in Lua, can't do AND checks like in C ;).) * FileManager+FileChooser: Pass our custom title bar directly to FileChooser (which also means we can now use FC's FocusManager layout directly). * FileChooser/Menu: Get rid of the weird `outer_title_bar` hack, and simply take a `custom_title_bar` pointer to an actual TitleBar instance instead. * FileManager/Menu/ListMenu/CoverMenu: Fix content height computations in `_recalculateDimen` (all the non-FM cases were including an old and now unused padding value, `self.header_padding`, leading to more blank space at the bottom than necessary, and, worse, leading to different item heights between FM views, possibly leading to unnecessary thumbnail scaling !) * ButtonDialog: Proper focus management when the ButtonTable is wrapped in a ScrollableContainer. * ConfigDialog: Implement a stupid workaround for a weird FocusManager issue when going back from `[⋮]` buttons. * ConfigDialog: Don't move the visual focus in `update` (i.e., we use `NOT_FOCUS` now that it works as intended). * DictQuickLookup: Ensures the `Menu` key bind does the exact same thing as the hamburger icon. * DictQuickLookup: Ensure we refocus after having mangled the FocusManager layout (prevents an old focus highlight from lingering on the wrong button). * FileChooser: Stop flagging it as no_title, because it is *never* without a title. (This behavior was a remnant of the previous FM-specific title bar hacks, which are no longer a thing). * FileChooser: Stop calling `mergeTitleBarIntoLayout` twice in `updateItems`. We already call Menu's, which handles it. (Prevents the title bar from being added twice to the FocusManager layout). * FocusManager: Relax the `Unfocus` checks in `moveFocusTo` to ensure we *always* unfocus something (if unfocusing was requested), even if we have to blast the whole widget tree to do so. This ensures callers that mangle self.layout can expect things to work after calling it regardless of how borked the current focus is. * FocusManager: Allow passing `focus_flags` to `refocusWidget`, so that it can be forwarded to the internal `moveFocusTo` call. * FocusManager: The above also allows us to enforce a default that ensures we do *not* send a Focus event on Touch devices, even if they have the hasDPad devcap. This essentially restores the previous/current behavior of not showing the visual feedback from such focus "events" sent programmatically, given the `focus_flags` check fix at the root of this PR ;). * InputDialog: Fix numerous issues relating to double/ghost instances of both InputText and VirtualKeyboard, ensuring we only ever have a single InputText & VK instance live. * InputDialog: Make sure every way we have of hiding the VK play nice together, especially when the `toggleKeyboard` button (shown w/ `add_nav_bar`) is at play. And doubly so when we're `fullscreen`, as hiding the VK implies resizing the widget. * InputText: Make sure we're flagged as in-focus when tapping inside the text field. * InputText: Make sure we don't attempt to show an already-visible VK in the custom `hasDPad` `onFocus` handler. * Menu: Get rid of an old and no longer used (nor meaningful) hack in `onFocus` about the initial/programmatically-sent Focus event. * Menu: Get rid of the unused `header_padding` field mentioned earlier in the FM/FC fixes. * Menu: Use `FOCUS_ONLY_ON_NT` in the explicit `moveFocusTo` call in `updatePageInfo`, so as to keep the current behavior of not showing the visual feedback of this focus on Touch devices. * Menu: Make sure *all* the `moveFocusTo` calls are gated behind the `hasDPad` devcap (previously, that was only the case for `updatePageInfo`, but not `mergeTitleBarIntoLayout` (which is called by `updateItems`). * MultiInputDialog: Actively get rid of the InputText & VK instances from the base class's constructor that we do not use. * MultiInputDialog: Ensure the FocusManager layout is *slightly* less broken (password fields can still be a bit weird, though). * TextViewer: Get rid of the unfocus -> layout mangling -> refocus hack now that `refocusWidget` handles this case sanely. * VirtualKeyboard: Notify our parent InputDialog when we get closed, so it can act accordingly (e.g., resize itself when `fullscreen`). * ScrollableContainer: Implement the necessary machinery for focus handling inside ButtonDialog (specifically, when scrolling via PgUp/PgDwn). * TextEditor: Given the above fixes, the plugin is no longer disabled on non-touch devices. * ReaderBookMark: Make sure we request a full refresh when closing the "Edit note" dialog, as CRe highlights may extend past its dimensions, and if it's closed separately from VK, the refresh would have been limited to its own dimensions, leaving a neat InputDialog-sized hole in the highlights ;).
330 lines
11 KiB
Lua
330 lines
11 KiB
Lua
--[[--
|
|
A button dialog widget that shows a grid of buttons.
|
|
|
|
@usage
|
|
local button_dialog = ButtonDialog:new{
|
|
buttons = {
|
|
{
|
|
{
|
|
text = "First row, left side",
|
|
callback = function() end,
|
|
hold_callback = function() end
|
|
},
|
|
{
|
|
text = "First row, middle",
|
|
callback = function() end
|
|
},
|
|
{
|
|
text = "First row, right side",
|
|
callback = function() end
|
|
}
|
|
},
|
|
{
|
|
{
|
|
text = "Second row, full span",
|
|
callback = function() end
|
|
}
|
|
},
|
|
{
|
|
{
|
|
text = "Third row, left side",
|
|
callback = function() end
|
|
},
|
|
{
|
|
text = "Third row, right side",
|
|
callback = function() end
|
|
}
|
|
}
|
|
}
|
|
}
|
|
--]]
|
|
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
local ButtonTable = require("ui/widget/buttontable")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
local Device = require("device")
|
|
local Font = require("ui/font")
|
|
local FocusManager = require("ui/widget/focusmanager")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local LineWidget = require("ui/widget/linewidget")
|
|
local MovableContainer = require("ui/widget/container/movablecontainer")
|
|
local ScrollableContainer = require("ui/widget/container/scrollablecontainer")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
local UIManager = require("ui/uimanager")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
local Screen = Device.screen
|
|
local util = require("util")
|
|
|
|
local ButtonDialog = FocusManager:extend{
|
|
buttons = nil,
|
|
width = nil,
|
|
width_factor = nil, -- number between 0 and 1, factor to the smallest of screen width and height
|
|
shrink_unneeded_width = false, -- have 'width' meaning 'max_width'
|
|
shrink_min_width = nil, -- default to ButtonTable's default
|
|
tap_close_callback = nil,
|
|
alpha = nil, -- passed to MovableContainer
|
|
-- If scrolling, prefers using this/these numbers of buttons rows per page
|
|
-- (depending on what the screen height allows) to compute the height.
|
|
rows_per_page = nil, -- number or array of numbers
|
|
|
|
title = nil,
|
|
title_align = "left",
|
|
title_face = Font:getFace("x_smalltfont"),
|
|
title_padding = Size.padding.large,
|
|
title_margin = Size.margin.title,
|
|
use_info_style = true, -- set to false to have bold font style of the title
|
|
info_face = Font:getFace("infofont"),
|
|
info_padding = Size.padding.default,
|
|
info_margin = Size.margin.default,
|
|
dismissable = true, -- set to false if any button callback is required
|
|
}
|
|
|
|
function ButtonDialog:init()
|
|
if not self.width then
|
|
if not self.width_factor then
|
|
self.width_factor = 0.9 -- default if no width specified
|
|
end
|
|
self.width = math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * self.width_factor)
|
|
end
|
|
if self.dismissable then
|
|
if Device:hasKeys() then
|
|
local back_group = util.tableDeepCopy(Device.input.group.Back)
|
|
if Device:hasFewKeys() then
|
|
table.insert(back_group, "Left")
|
|
self.key_events.Close = { { back_group } }
|
|
else
|
|
table.insert(back_group, "Menu")
|
|
self.key_events.Close = { { back_group } }
|
|
end
|
|
end
|
|
if Device:isTouchDevice() then
|
|
self.ges_events.TapClose = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = Geom:new{
|
|
x = 0, y = 0,
|
|
w = Screen:getWidth(),
|
|
h = Screen:getHeight(),
|
|
}
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
self.buttontable = ButtonTable:new{
|
|
buttons = self.buttons,
|
|
width = self.width - 2*Size.border.window - 2*Size.padding.button,
|
|
shrink_unneeded_width = self.shrink_unneeded_width,
|
|
shrink_min_width = self.shrink_min_width,
|
|
show_parent = self,
|
|
}
|
|
local buttontable_width = self.buttontable:getSize().w -- may be shrinked
|
|
|
|
local title_widget, title_widget_height
|
|
if self.title then
|
|
local title_padding, title_margin, title_face
|
|
if self.use_info_style then
|
|
title_padding = self.info_padding
|
|
title_margin = self.info_margin
|
|
title_face = self.info_face
|
|
else
|
|
title_padding = self.title_padding
|
|
title_margin = self.title_margin
|
|
title_face = self.title_face
|
|
end
|
|
title_widget = FrameContainer:new{
|
|
padding = title_padding,
|
|
margin = title_margin,
|
|
bordersize = 0,
|
|
TextBoxWidget:new{
|
|
text = self.title,
|
|
width = buttontable_width - 2 * (title_padding + title_margin),
|
|
face = title_face,
|
|
alignment = self.title_align,
|
|
},
|
|
}
|
|
title_widget_height = title_widget:getSize().h + Size.line.medium
|
|
else
|
|
title_widget = VerticalSpan:new{}
|
|
title_widget_height = 0
|
|
end
|
|
self.top_to_content_offset = Size.padding.buttontable + Size.margin.default + title_widget_height
|
|
|
|
-- If the ButtonTable ends up being taller than the screen, wrap it inside a ScrollableContainer.
|
|
-- Ensure some small top and bottom padding, so the scrollbar stand out, and some outer margin
|
|
-- so the this dialog does not take the full height and stand as a popup.
|
|
local max_height = Screen:getHeight() - 2*Size.padding.buttontable - 2*Size.margin.default - title_widget_height
|
|
local height = self.buttontable:getSize().h
|
|
local scontainer, scrollbar_width
|
|
if height > max_height then
|
|
-- Adjust the ScrollableContainer to an integer multiple of the row height
|
|
-- (assuming all rows get the same height), so when scrolling per page,
|
|
-- we always end up seeing full rows.
|
|
self.buttontable:setupGridScrollBehaviour()
|
|
local step_scroll_grid = self.buttontable:getStepScrollGrid()
|
|
local row_height = step_scroll_grid[1].bottom + 1 - step_scroll_grid[1].top
|
|
local fit_rows = math.floor(max_height / row_height)
|
|
if self.rows_per_page then
|
|
if type(self.rows_per_page) == "number" then
|
|
if fit_rows > self.rows_per_page then
|
|
fit_rows = self.rows_per_page
|
|
end
|
|
else
|
|
for _, nb in ipairs(self.rows_per_page) do
|
|
if fit_rows >= nb then
|
|
fit_rows = nb
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- (Comment the next line to test ScrollableContainer behaviour when things do not fit)
|
|
max_height = row_height * fit_rows
|
|
scrollbar_width = ScrollableContainer:getScrollbarWidth()
|
|
self.cropping_widget = ScrollableContainer:new{
|
|
dimen = Geom:new{
|
|
-- We'll be exceeding the provided width in this case (let's not bother
|
|
-- ensuring it, we'd need to re-setup the ButtonTable...)
|
|
w = buttontable_width + scrollbar_width,
|
|
h = max_height,
|
|
},
|
|
show_parent = self,
|
|
step_scroll_grid = step_scroll_grid,
|
|
self.buttontable,
|
|
}
|
|
scontainer = VerticalGroup:new{
|
|
VerticalSpan:new{ width=Size.padding.buttontable },
|
|
self.cropping_widget,
|
|
VerticalSpan:new{ width=Size.padding.buttontable },
|
|
}
|
|
end
|
|
local separator
|
|
if self.title then
|
|
separator = LineWidget:new{
|
|
background = Blitbuffer.COLOR_GRAY,
|
|
dimen = Geom:new{
|
|
w = buttontable_width + (scrollbar_width or 0),
|
|
h = Size.line.medium,
|
|
},
|
|
}
|
|
else
|
|
separator = VerticalSpan:new{}
|
|
end
|
|
self.movable = MovableContainer:new{
|
|
alpha = self.alpha,
|
|
anchor = self.anchor,
|
|
FrameContainer:new{
|
|
background = Blitbuffer.COLOR_WHITE,
|
|
bordersize = Size.border.window,
|
|
radius = Size.radius.window,
|
|
padding = Size.padding.button,
|
|
-- No padding at top or bottom to make all buttons
|
|
-- look the same size
|
|
padding_top = 0,
|
|
padding_bottom = 0,
|
|
VerticalGroup:new{
|
|
title_widget,
|
|
separator,
|
|
scontainer or self.buttontable,
|
|
},
|
|
}
|
|
}
|
|
|
|
-- No need to reinvent the wheel, ButtonTable's layout is perfect as-is
|
|
self.layout = self.buttontable.layout
|
|
-- But we'll want to control focus in its place, though
|
|
self.buttontable.layout = nil
|
|
|
|
self[1] = CenterContainer:new{
|
|
dimen = Screen:getSize(),
|
|
self.movable,
|
|
}
|
|
end
|
|
|
|
function ButtonDialog:getContentSize()
|
|
return self.movable.dimen
|
|
end
|
|
|
|
function ButtonDialog:getButtonById(id)
|
|
return self.buttontable:getButtonById(id)
|
|
end
|
|
|
|
function ButtonDialog:getScrolledOffset()
|
|
if self.cropping_widget then
|
|
return self.cropping_widget:getScrolledOffset()
|
|
end
|
|
end
|
|
|
|
function ButtonDialog:setScrolledOffset(offset_point)
|
|
if offset_point and self.cropping_widget then
|
|
return self.cropping_widget:setScrolledOffset(offset_point)
|
|
end
|
|
end
|
|
|
|
function ButtonDialog:setTitle(title)
|
|
self.title = title
|
|
self:free()
|
|
self:init()
|
|
UIManager:setDirty("all", "ui")
|
|
end
|
|
|
|
function ButtonDialog:onShow()
|
|
UIManager:setDirty(self, function()
|
|
return "ui", self.movable.dimen
|
|
end)
|
|
end
|
|
|
|
function ButtonDialog:onCloseWidget()
|
|
UIManager:setDirty(nil, function()
|
|
return "flashui", self.movable.dimen
|
|
end)
|
|
end
|
|
|
|
function ButtonDialog:onClose()
|
|
if self.tap_close_callback then
|
|
self.tap_close_callback()
|
|
end
|
|
UIManager:close(self)
|
|
return true
|
|
end
|
|
|
|
function ButtonDialog:onTapClose(arg, ges)
|
|
if ges.pos:notIntersectWith(self.movable.dimen) then
|
|
self:onClose()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function ButtonDialog:paintTo(...)
|
|
FocusManager.paintTo(self, ...)
|
|
self.dimen = self.movable.dimen
|
|
end
|
|
|
|
function ButtonDialog:onFocusMove(args)
|
|
local ret = FocusManager.onFocusMove(self, args)
|
|
|
|
-- If we're using a ScrollableContainer, ask it to scroll to the focused item
|
|
if self.cropping_widget then
|
|
local focus = self:getFocusItem()
|
|
if self.dimen and focus and focus.dimen then
|
|
local button_y_offset = focus.dimen.y - self.dimen.y - self.top_to_content_offset
|
|
-- NOTE: The final argument ensures we'll always keep the neighboring item visible.
|
|
-- (i.e., the top/bottom of the scrolled view is actually the previous/next item).
|
|
self.cropping_widget:_scrollBy(0, button_y_offset, true)
|
|
end
|
|
end
|
|
|
|
return ret
|
|
end
|
|
|
|
function ButtonDialog:_onPageScrollToRow(row)
|
|
-- ScrollableContainer will pass us the row number of the top widget at the current scroll offset
|
|
self:moveFocusTo(1, row)
|
|
end
|
|
|
|
return ButtonDialog
|