2
0
mirror of https://github.com/koreader/koreader synced 2024-11-18 03:25:46 +00:00
koreader/frontend/ui/widget/spinwidget.lua
NiLuJe fadee1f5dc
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 02:14:48 +02:00

317 lines
9.7 KiB
Lua

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
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,
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,
}
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}, doc = "close spin widget" }
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
if self.unit and self.unit ~= "" then
self.precision = self.precision and self.precision or "%1d"
end
-- 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,
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 = "\xE2\x80\xAF" .. 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)
local vgroup = VerticalGroup:new{
align = "left",
title_bar,
}
table.insert(vgroup, CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = value_group:getSize().h + 4 * Size.padding.large,
},
value_group
})
table.insert(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,
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 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
function SpinWidget:hasMoved()
local offset = self.movable:getMovedOffset()
return offset.x ~= 0 or offset.y ~= 0
end
function SpinWidget:onCloseWidget()
UIManager:setDirty(nil, function()
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:onAnyKeyPressed()
self:onClose()
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