2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/frontend/ui/widget/numberpickerwidget.lua
poire-z f0122cf457 Button: handle 'width' as the final outer width
All our widgets are considering their provided 'width'
as the outer width, except Button which considered it
as some 'inner width', to which padding/border/margin
were added. Let's have them all consistent.
Some other widgets using Button had tweaks to account
for that odd behaviour: fix and simplify them.
Also fix Button layout when text is left aligned.
2023-03-23 20:28:38 +01:00

348 lines
13 KiB
Lua

--[[--
A customizable number picker.
Example:
local NumberPickerWidget = require("ui/widget/numberpickerwidget")
local numberpicker = NumberPickerWidget:new{
-- for floating point (decimals), use something like "%.2f"
precision = "%02d",
value = 0,
value_min = 0,
value_max = 23,
value_step = 1,
value_hold_step = 4,
wrap = true,
}
--]]
local Button = require("ui/widget/button")
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 Font = require("ui/font")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local Size = require("ui/size")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local _ = require("gettext")
local T = require("ffi/util").template
local Screen = Device.screen
local NumberPickerWidget = FocusManager:extend{
spinner_face = Font:getFace("smalltfont"),
precision = "%02d",
width = nil,
height = nil,
value = 0,
value_min = 0,
value_max = 23,
value_step = 1,
value_hold_step = 4,
value_table = nil,
value_index = nil,
wrap = true,
-- in case we need calculate number of days in a given month and year
date_month = nil,
date_year = nil,
-- on update signal to the caller and pass updated value
picker_updated_callback = nil,
unit = "",
}
function NumberPickerWidget:init()
self.screen_width = Screen:getWidth()
self.screen_height = Screen:getHeight()
self.width = self.width or math.floor(math.min(self.screen_width, self.screen_height) * 0.2)
if self.value_table then
self.value_index = self.value_index or 1
self.value = self.value_table[self.value_index]
end
self.layout = {}
-- Widget layout
local bordersize = Size.border.default
local margin = Size.margin.default
local button_up = Button:new{
text = "",
bordersize = bordersize,
margin = margin,
radius = 0,
text_font_size = 24,
width = self.width,
show_parent = self.show_parent,
callback = function()
if self.date_month and self.date_year then
self.value_max = self:getDaysInMonth(self.date_month:getValue(), self.date_year:getValue())
end
self.value = self:changeValue(self.value, self.value_step, self.value_max, self.value_min, self.wrap)
self:update()
end,
hold_callback = function()
if self.date_month and self.date_year then
self.value_max = self:getDaysInMonth(self.date_month:getValue(), self.date_year:getValue())
end
self.value = self:changeValue(self.value, self.value_hold_step, self.value_max, self.value_min, self.wrap)
self:update()
end
}
table.insert(self.layout, {button_up})
local button_down = Button:new{
text = "",
bordersize = bordersize,
margin = margin,
radius = 0,
text_font_size = 24,
width = self.width,
show_parent = self.show_parent,
callback = function()
if self.date_month and self.date_year then
self.value_max = self:getDaysInMonth(self.date_month:getValue(), self.date_year:getValue())
end
self.value = self:changeValue(self.value, self.value_step * -1, self.value_max, self.value_min, self.wrap)
self:update()
end,
hold_callback = function()
if self.date_month and self.date_year then
self.value_max = self:getDaysInMonth(self.date_month:getValue(), self.date_year:getValue())
end
self.value = self:changeValue(self.value, self.value_hold_step * -1, self.value_max, self.value_min, self.wrap)
self:update()
end
}
table.insert(self.layout, {button_down})
local empty_space = VerticalSpan:new{ width = Size.padding.large }
self.formatted_value = self.value
if not self.value_table then
self.formatted_value = string.format(self.precision, self.formatted_value)
end
local input_dialog
local callback_input = nil
if self.value_table == nil then
callback_input = function()
if self.date_month and self.date_year then
self.value_max = self:getDaysInMonth(self.date_month:getValue(), self.date_year:getValue())
end
input_dialog = InputDialog:new{
title = _("Enter number"),
input_hint = T("%1 (%2 - %3)", self.formatted_value, self.value_min, self.value_max),
input_type = "number",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("OK"),
is_enter_default = true,
callback = function()
local input_text = input_dialog:getInputText()
local input_value = tonumber(input_text)
local turn_off_checks = false
-- if the first character of the input string is ":" turn off min-max checks
if input_text:match("^:") and G_reader_settings:isTrue("debug_verbose") then
turn_off_checks = true -- turn off checks
input_text = input_text:gsub("^:", "") -- shorten input
input_value = tonumber(input_text) -- try to get value
end
-- if the input text starts with `=`, try to evaluate the expression
-- only allow `math.*` and no other functions to be safe here.
if input_text:match("^=") then
local function evaluate_string(code)
-- only execute if there are no chars (except math.xxx) and no {[]} in the user input
local check_code = code:gsub("math%.%a*", "")
if check_code:find("[%a%[%]%{%}]") then
return nil
end
code = code:gsub("^=", "return ")
local env = {math = math} -- restrict to only math functions
local func, dummy = load(code, "user_sandbox", nil, env)
if func then
return pcall(func)
end
end
local dummy
dummy, input_value = evaluate_string(input_text)
input_value = dummy and tonumber(input_value)
end
if turn_off_checks then
if not input_value then return end
if input_value < self.value_min or input_value > self.value_max then
UIManager:show(InfoMessage:new{
text = T(_("ATTENTION:\nPrefixing the input with ':' disables sanity checks!\nThis value should be in the range of %1 - %2.\nUndefined behavior may occur."), self.value_min, self.value_max),
})
end
self.value = input_value
self:update()
UIManager:close(input_dialog)
return
end
if input_value and input_value >= self.value_min and input_value <= self.value_max then
self.value = input_value
self:update()
UIManager:close(input_dialog)
elseif input_value and input_value < self.value_min then
UIManager:show(InfoMessage:new{
text = T(_("This value should be %1 or more."), self.value_min),
timeout = 2,
})
elseif input_value and input_value > self.value_max then
UIManager:show(InfoMessage:new{
text = T(_("This value should be %1 or less."), self.value_max),
timeout = 2,
})
else
UIManager:show(InfoMessage:new{
text = _("Invalid value. Please enter a valid value."),
timeout = 2
})
end
end,
},
},
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
end
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
self.text_value = Button:new{
text = tostring(self.formatted_value) .. unit,
bordersize = 0,
padding = 0,
text_font_face = self.spinner_face.font,
text_font_size = self.spinner_face.orig_size,
width = self.width,
show_parent = self.show_parent,
callback = callback_input,
}
if callback_input then
table.insert(self.layout, 2, {self.text_value})
end
local widget_spinner = VerticalGroup:new{
align = "center",
button_up,
empty_space,
self.text_value,
empty_space,
button_down,
}
self.frame = FrameContainer:new{
bordersize = 0,
padding = Size.padding.default,
CenterContainer:new{
align = "center",
dimen = Geom:new{
w = widget_spinner:getSize().w,
h = widget_spinner:getSize().h
},
widget_spinner
}
}
self.dimen = self.frame:getSize()
self[1] = self.frame
self:refocusWidget()
UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen
end)
end
--[[--
Update.
--]]
function NumberPickerWidget:update()
self.formatted_value = self.value
if not self.value_table then
self.formatted_value = string.format(self.precision, self.formatted_value)
end
self.text_value:setText(tostring(self.formatted_value), self.width)
self:refocusWidget()
UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen
end)
if self.picker_updated_callback then
self.picker_updated_callback(self.value, self.value_index)
end
end
--[[--
Change value.
--]]
function NumberPickerWidget:changeValue(value, step, max, min, wrap)
if self.value_index then
self.value_index = self.value_index + step
if self.value_index > #self.value_table then
self.value_index = wrap and 1 or #self.value_table
elseif
self.value_index < 1 then
self.value_index = wrap and #self.value_table or 1
end
value = self.value_table[self.value_index]
else
value = value + step
if value > max then
value = wrap and min or max
elseif value < min then
value = wrap and max or min
end
end
return value
end
--[[--
Get days in month.
--]]
function NumberPickerWidget:getDaysInMonth(month, year)
local days_in_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
local days = days_in_month[month]
-- check for leap year
if (month == 2) then
if year % 4 == 0 then
if year % 100 == 0 then
if year % 400 == 0 then
days = 29
end
else
days = 29
end
end
end
return days
end
--[[--
Get value.
--]]
function NumberPickerWidget:getValue()
return self.value, self.value_index
end
return NumberPickerWidget