2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/frontend/ui/widget/spinwidget.lua

342 lines
11 KiB
Lua
Raw Normal View History

local Blitbuffer = require("ffi/blitbuffer")
local ButtonTable = require("ui/widget/buttontable")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local FocusManager = require("ui/widget/focusmanager")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local MovableContainer = require("ui/widget/container/movablecontainer")
local NumberPickerWidget = require("ui/widget/numberpickerwidget")
local Size = require("ui/size")
local TitleBar = require("ui/widget/titlebar")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local _ = require("gettext")
local Screen = Device.screen
local T = require("ffi/util").template
Clarify our OOP semantics across the codebase (#9586) Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local SpinWidget = FocusManager:extend{
title_text = "",
info_text = nil,
width = nil,
width_factor = nil, -- number between 0 and 1, factor to the smallest of screen width and height
height = nil,
2019-09-23 22:24:45 +00:00
value_table = nil,
value_index = nil,
value = 1,
value_min = 0,
value_max = 20,
value_step = 1,
value_hold_step = 4,
precision = nil, -- default "%02d" in NumberPickerWidget
wrap = false,
cancel_text = _("Close"),
ok_text = _("Apply"),
ok_always_enabled = false, -- set to true to enable OK button for unchanged value
cancel_callback = nil,
callback = nil,
close_callback = nil,
keep_shown_on_apply = false,
-- Set this to add upper default button that restores number to its default value
default_value = nil,
default_text = nil,
-- Optional extra button
extra_text = nil,
extra_callback = nil,
-- Optional extra button above ok/cancel buttons row
option_text = nil,
option_callback = nil,
unit = nil, -- unit to show or nil
}
function SpinWidget:init()
-- used to enable ok_button, self.value may be changed in extra callback
self.original_value = self.value_table and self.value_table[self.value_index or 1] or self.value
self.screen_width = Screen:getWidth()
self.screen_height = Screen:getHeight()
if not self.width then
if not self.width_factor then
self.width_factor = 0.6 -- default if no width speficied
end
self.width = math.floor(math.min(self.screen_width, self.screen_height) * self.width_factor)
end
if Device:hasKeys() then
self.key_events.Close = { { Device.input.group.Back } }
end
if Device:isTouchDevice() then
self.ges_events.TapClose = {
GestureRange:new{
ges = "tap",
range = Geom:new{
w = self.screen_width,
h = self.screen_height,
}
},
}
end
Tame some ButtonTable users into re-using Buttontable instances if possible (#7166) * QuickDictLookup, ImageViewer, NumberPicker: Smarter `update` that will re-use most of the widget's layout instead of re-instantiating all the things. * SpinWidget/DoubleSpinWidget: The NumberPicker change above renders a hack to preserve alpha on these widgets almost unnecessary. Also fixed said hack to also apply to the center, value button. * Button: Don't re-instantiate the frame in setText/setIcon when unnecessary (e.g., no change at all, or no layout change). * Button: Add a refresh method that repaints and refreshes a *specific* Button (provided it's been painted once) all on its lonesome. * ConfigDialog: Free everything that's going to be re-instatiated on update * A few more post #7118 fixes: * SkimTo: Always flag the chapter nav buttons as vsync * Button: Fix the highlight on rounded buttons when vsync is enabled (e.g., it's now entirely visible, instead of showing a weird inverted corner glitch). * Some more heuristic tweaks in Menu/TouchMenu/Button/IconButton * ButtonTable: fix the annoying rounding issue I'd noticed in #7054 ;). * Enable dithering in TextBoxWidget (e.g., in the Wikipedia full view). This involved moving the HW dithering align fixup to base, where it always ought to have been ;). * Switch a few widgets that were using "partial" on close to "ui", or, more rarely, "flashui". The intent being to limit "partial" purely to the Reader, because it has a latency cost when mixed with other refreshes, which happens often enough in UI ;). * Minor documentation tweaks around UIManager's `setDirty` to reflect that change. * ReaderFooter: Force a footer repaint on resume if it is visible (otherwise, just update it). * ReaderBookmark: In the same vein, don't repaint an invisible footer on bookmark count changes.
2021-01-28 23:20:15 +00:00
if self.unit and self.unit ~= "" then
self.precision = self.precision and self.precision or "%1d"
end
Tame some ButtonTable users into re-using Buttontable instances if possible (#7166) * QuickDictLookup, ImageViewer, NumberPicker: Smarter `update` that will re-use most of the widget's layout instead of re-instantiating all the things. * SpinWidget/DoubleSpinWidget: The NumberPicker change above renders a hack to preserve alpha on these widgets almost unnecessary. Also fixed said hack to also apply to the center, value button. * Button: Don't re-instantiate the frame in setText/setIcon when unnecessary (e.g., no change at all, or no layout change). * Button: Add a refresh method that repaints and refreshes a *specific* Button (provided it's been painted once) all on its lonesome. * ConfigDialog: Free everything that's going to be re-instatiated on update * A few more post #7118 fixes: * SkimTo: Always flag the chapter nav buttons as vsync * Button: Fix the highlight on rounded buttons when vsync is enabled (e.g., it's now entirely visible, instead of showing a weird inverted corner glitch). * Some more heuristic tweaks in Menu/TouchMenu/Button/IconButton * ButtonTable: fix the annoying rounding issue I'd noticed in #7054 ;). * Enable dithering in TextBoxWidget (e.g., in the Wikipedia full view). This involved moving the HW dithering align fixup to base, where it always ought to have been ;). * Switch a few widgets that were using "partial" on close to "ui", or, more rarely, "flashui". The intent being to limit "partial" purely to the Reader, because it has a latency cost when mixed with other refreshes, which happens often enough in UI ;). * Minor documentation tweaks around UIManager's `setDirty` to reflect that change. * ReaderFooter: Force a footer repaint on resume if it is visible (otherwise, just update it). * ReaderBookmark: In the same vein, don't repaint an invisible footer on bookmark count changes.
2021-01-28 23:20:15 +00:00
-- Actually the widget layout
self:update()
end
function SpinWidget:update(numberpicker_value, numberpicker_value_index)
local prev_movable_offset = self.movable and self.movable:getMovedOffset()
local prev_movable_alpha = self.movable and self.movable.alpha
self.layout = {}
local value_widget = NumberPickerWidget:new{
show_parent = self,
value = numberpicker_value or self.value,
2019-09-23 22:24:45 +00:00
value_table = self.value_table,
value_index = numberpicker_value_index or self.value_index,
value_min = self.value_min,
value_max = self.value_max,
value_step = self.value_step,
value_hold_step = self.value_hold_step,
precision = self.precision,
wrap = self.wrap,
picker_updated_callback = function(value, value_index)
self:update(value, value_index)
end,
unit = self.unit,
}
self:mergeLayoutInVertical(value_widget)
local value_group = HorizontalGroup:new{
align = "center",
value_widget,
}
local title_bar = TitleBar:new{
width = self.width,
align = "left",
with_bottom_line = true,
title = self.title_text,
title_shrink_font_to_fit = true,
info_text = self.info_text,
show_parent = self,
}
local buttons = {}
if self.default_value then
local unit = ""
if self.unit then
if self.unit == "°" then
unit = self.unit
elseif self.unit ~= "" then
unit = "\u{202F}" .. self.unit -- use Narrow No-Break Space (NNBSP) here
end
end
local value
if self.default_text then
value = self.default_text
else
if self.value_table then
value = self.value_table[self.default_value]
else
value = self.default_value
end
if self.precision then
value = string.format(self.precision, value)
end
end
table.insert(buttons, {
{
text = T(_("Default value: %1%2"), value, unit),
callback = function()
if value_widget.value_table then
value_widget.value_index = self.default_value
else
value_widget.value = self.default_value
end
value_widget:update()
end,
},
})
end
local extra_button = {
text = self.extra_text,
callback = function()
if self.extra_callback then
self.value, self.value_index = value_widget:getValue()
self.extra_callback(self)
end
if not self.keep_shown_on_apply then -- assume extra wants it same as ok
self:onClose()
end
end,
}
local option_button = {
text = self.option_text,
callback = function()
if self.option_callback then
self.value, self.value_index = value_widget:getValue()
self.option_callback(self)
end
if not self.keep_shown_on_apply then -- assume option wants it same as ok
self:onClose()
end
end,
}
if self.extra_text and not self.option_text then
table.insert(buttons, {extra_button})
elseif self.option_text and not self.extra_text then
table.insert(buttons, {option_button})
elseif self.extra_text and self.option_text then
table.insert(buttons, {extra_button, option_button})
end
table.insert(buttons, {
{
text = self.cancel_text,
callback = function()
if self.cancel_callback then
self.cancel_callback()
end
self:onClose()
end,
},
{
text = self.ok_text,
enabled = self.ok_always_enabled or self.original_value ~= value_widget:getValue(),
callback = function()
self.value, self.value_index = value_widget:getValue()
self.original_value = self.value
if self.callback then
self.callback(self)
end
if self.keep_shown_on_apply then
self:update()
else
self:onClose()
end
end,
},
})
local ok_cancel_buttons = ButtonTable:new{
width = self.width - 2 * Size.padding.default,
buttons = buttons,
zero_sep = true,
show_parent = self,
}
self:mergeLayoutInVertical(ok_cancel_buttons)
self.vgroup = VerticalGroup:new{
align = "left",
title_bar,
}
table.insert(self.vgroup, CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = value_group:getSize().h + 4 * Size.padding.large,
},
value_group,
})
table.insert(self.vgroup, CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = ok_cancel_buttons:getSize().h,
},
ok_cancel_buttons,
})
self.spin_frame = FrameContainer:new{
radius = Size.radius.window,
padding = 0,
margin = 0,
background = Blitbuffer.COLOR_WHITE,
self.vgroup,
}
self.movable = MovableContainer:new{
alpha = prev_movable_alpha,
self.spin_frame,
}
self[1] = WidgetContainer:new{
align = "center",
dimen = Geom:new{
x = 0, y = 0,
w = self.screen_width,
h = self.screen_height,
},
self.movable,
}
if self._added_widgets then
for _, widget in ipairs(self._added_widgets) do
self:addWidget(widget, true)
end
end
if prev_movable_offset then
self.movable:setMovedOffset(prev_movable_offset)
end
self:refocusWidget()
UIManager:setDirty(self, function()
return "ui", self.spin_frame.dimen
end)
end
-- This is almost the same functionality as in InputDialog, except positioning
function SpinWidget:addWidget(widget, re_init)
table.insert(self.layout, #self.layout, {widget})
if not re_init then -- backup widget for re-init
widget = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = widget:getSize().h,
},
widget,
}
if not self._added_widgets then
self._added_widgets = {}
end
table.insert(self._added_widgets, widget)
end
-- Insert widget before the bottom buttons and their previous vspan.
-- This is different to InputDialog.
table.insert(self.vgroup, #self.vgroup, widget)
end
function SpinWidget:getAddedWidgetAvailableWidth()
return self.width - 2 * Size.padding.large
end
function SpinWidget:hasMoved()
local offset = self.movable:getMovedOffset()
return offset.x ~= 0 or offset.y ~= 0
end
function SpinWidget:onCloseWidget()
UIManager:setDirty(nil, function()
Tame some ButtonTable users into re-using Buttontable instances if possible (#7166) * QuickDictLookup, ImageViewer, NumberPicker: Smarter `update` that will re-use most of the widget's layout instead of re-instantiating all the things. * SpinWidget/DoubleSpinWidget: The NumberPicker change above renders a hack to preserve alpha on these widgets almost unnecessary. Also fixed said hack to also apply to the center, value button. * Button: Don't re-instantiate the frame in setText/setIcon when unnecessary (e.g., no change at all, or no layout change). * Button: Add a refresh method that repaints and refreshes a *specific* Button (provided it's been painted once) all on its lonesome. * ConfigDialog: Free everything that's going to be re-instatiated on update * A few more post #7118 fixes: * SkimTo: Always flag the chapter nav buttons as vsync * Button: Fix the highlight on rounded buttons when vsync is enabled (e.g., it's now entirely visible, instead of showing a weird inverted corner glitch). * Some more heuristic tweaks in Menu/TouchMenu/Button/IconButton * ButtonTable: fix the annoying rounding issue I'd noticed in #7054 ;). * Enable dithering in TextBoxWidget (e.g., in the Wikipedia full view). This involved moving the HW dithering align fixup to base, where it always ought to have been ;). * Switch a few widgets that were using "partial" on close to "ui", or, more rarely, "flashui". The intent being to limit "partial" purely to the Reader, because it has a latency cost when mixed with other refreshes, which happens often enough in UI ;). * Minor documentation tweaks around UIManager's `setDirty` to reflect that change. * ReaderFooter: Force a footer repaint on resume if it is visible (otherwise, just update it). * ReaderBookmark: In the same vein, don't repaint an invisible footer on bookmark count changes.
2021-01-28 23:20:15 +00:00
return "ui", self.spin_frame.dimen
end)
end
function SpinWidget:onShow()
UIManager:setDirty(self, function()
return "ui", self.spin_frame.dimen
end)
return true
end
function SpinWidget:onTapClose(arg, ges_ev)
if ges_ev.pos:notIntersectWith(self.spin_frame.dimen) then
self:onClose()
end
return true
end
function SpinWidget:onClose()
UIManager:close(self)
if self.close_callback then
self.close_callback()
end
return true
end
return SpinWidget