Files
koreader-mirror/frontend/ui/widget/datetimewidget.lua

568 lines
19 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[--
Widget for setting the date or time.
Example for input a time:
local DateTimeWidget = require("ui/widget/datetimewidget")
local @{gettext|_} = require("gettext")
local time_widget = DateTimeWidget:new{
hour = 10,
min = 30,
ok_text = _("Set time"),
title_text = _("Set time"),
info_text = _("Some information"),
callback = function(time)
-- use time.hour and time.min here
end
}
UIManager:show(time_widget)
Example for input a date:
local DateTimeWidget = require("ui/widget/datetimewidget")
local @{gettext|_} = require("gettext")
local date_widget = DateTimeWidget:new{
year = 2021,
month = 12,
day = 31,
ok_text = _("Set date"),
title_text = _("Set date"),
callback = function(time)
-- use time.year, time.month, time.day here
end
}
UIManager:show(date_widget)
Example to input a duration in days, hours and minutes:
local DateTimeWidget = require("ui/widget/datetimewidget")
local @{gettext|_} = require("gettext")
local date_widget = DateTimeWidget:new{
day = 5,
hour = 12,
min = 0,
ok_text = _("Set"),
title_text = _("Set duration"),
callback = function(time)
-- use time.day, time.hour, time.min here
end
}
UIManager:show(date_widget)
--]]--
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 Font = require("ui/font")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local NumberPickerWidget = require("ui/widget/numberpickerwidget")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
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 DateTimeWidget = FocusManager:extend{
title_face = Font:getFace("x_smalltfont"),
info_text = nil,
width = nil,
height = nil,
ok_text = _("Apply"),
cancel_text = _("Close"),
-- Optional extra button on bottom
extra_text = nil,
extra_callback = nil,
}
function DateTimeWidget:init()
self.nb_pickers = 0
if self.year then
self.nb_pickers = self.nb_pickers + 1
end
if self.month then
self.nb_pickers = self.nb_pickers + 1
end
if self.day then
self.nb_pickers = self.nb_pickers + 1
end
if self.hour then
self.nb_pickers = self.nb_pickers + 1
end
if self.min then
self.nb_pickers = self.nb_pickers + 1
end
if self.sec then
self.nb_pickers = self.nb_pickers + 1
end
self.layout = {}
self.screen_width = Screen:getWidth()
self.screen_height = Screen:getHeight()
local width_scale_factor = 0.6
if self.nb_pickers == 3 then
width_scale_factor = 0.8
elseif self.nb_pickers == 4 then
width_scale_factor = 0.85
elseif self.nb_pickers >=5 then
width_scale_factor = 0.95
end
self.width = self.width or math.floor(math.min(self.screen_width, self.screen_height) * width_scale_factor)
self:registerKeyEvents()
if Device:isTouchDevice() then
self.ges_events.TapClose = {
GestureRange:new{
ges = "tap",
range = Geom:new{
w = self.screen_width,
h = self.screen_height,
}
},
}
end
-- Actually the widget layout
self:createLayout()
self.non_touch_with_action_dpad = Device:hasDPad() and Device:useDPadAsActionKeys() and not Device:isTouchDevice()
-- Move focus to OK button on NT devices with key_events, saves time for users
if self.non_touch_with_action_dpad then
-- Since button table is the last row in our layout, and OK is the last button
-- We need to set focus to both last row, and last column
local last_row = #self.layout
local last_col = #self.layout[last_row]
self:moveFocusTo(last_col, last_row)
end
end
function DateTimeWidget:registerKeyEvents()
if not Device:hasKeys() then return end
self.key_events.Close = { { Device.input.group.Back } }
if Device:hasDPad() and Device:useDPadAsActionKeys() then
if self.nb_pickers == 1 then
self.key_events.CenterWidgetValueUp = { { Device.input.group.PgFwd }, event = "DateTimeButtonPressed", args = { "center_widget", 1 } }
self.key_events.CenterWidgetValueDown = { { Device.input.group.PgBack }, event = "DateTimeButtonPressed", args = { "center_widget", -1 } }
elseif self.nb_pickers == 2 or self.nb_pickers == 3 then
self.key_events.LeftWidgetValueUp = { { "LPgFwd" }, event = "DateTimeButtonPressed", args = { "left_widget", 1 } }
self.key_events.LeftWidgetValueDown = { { "LPgBack" }, event = "DateTimeButtonPressed", args = { "left_widget", -1 } }
self.key_events.RightWidgetValueUp = { { "RPgFwd" }, event = "DateTimeButtonPressed", args = { "right_widget", 1 } }
self.key_events.RightWidgetValueDown = { { "RPgBack" }, event = "DateTimeButtonPressed", args = { "right_widget", -1 } }
if self.nb_pickers == 3 and (Device:hasScreenKB() or Device:hasKeyboard()) then
local modifier = Device:hasKeyboard() and "Shift" or "ScreenKB"
self.key_events.CenterWidgetValueUp = {
{ modifier, Device.input.group.PgFwd },
event = "DateTimeButtonPressed",
args = { "center_widget", 1 }
}
self.key_events.CenterWidgetValueDown = {
{ modifier, Device.input.group.PgBack },
event = "DateTimeButtonPressed",
args = { "center_widget", -1 }
}
end
end -- if self.nb_pickers
end -- if Device:hasDPad() and Device:useDPadAsActionKeys()
end
function DateTimeWidget:createLayout()
-- Empty table w/ the methods we use NOP'ed
local dummy_widget = {}
function dummy_widget:free() end
function dummy_widget:getValue() end
function dummy_widget:update() end
-- The following calculation is stolen from NumberPickerWidget
local number_picker_widgets_width = math.floor(math.min(self.screen_width, self.screen_height) * 0.2)
if self.nb_pickers > 3 then
number_picker_widgets_width = math.floor(number_picker_widgets_width * 3 / self.nb_pickers)
end
if self.year then
self.year_widget = NumberPickerWidget:new{
show_parent = self,
value = self.year,
value_min = self.year_min or 2021,
value_max = self.year_max or 2525,
value_step = 1,
value_hold_step = self.year_hold_step or 4,
width = number_picker_widgets_width,
picker_updated_callback = function(value)
if self.month and self.day then
local current_month = self.month_widget:getValue()
local current_day = self.day_widget:getValue()
local days_in_month = self.day_widget:getDaysInMonth(current_month, value)
if current_day > days_in_month then
self.day_widget.value = days_in_month
self.day_widget:update()
end
end
end,
}
self:mergeLayoutInHorizontal(self.year_widget)
else
self.year_widget = dummy_widget
end
if self.month then
self.month_widget = NumberPickerWidget:new{
show_parent = self,
value = self.month,
value_min = self.month_min or 1,
value_max = self.month_max or 12,
value_step = 1,
value_hold_step = self.month_hold_step or 3,
width = number_picker_widgets_width,
picker_updated_callback = function(value)
if self.day then
local current_day = self.day_widget:getValue()
local year_value = self.year and self.year_widget:getValue() or 2021 -- default non-leap year
local days_in_month = self.day_widget:getDaysInMonth(value, year_value)
if current_day > days_in_month then
self.day_widget.value = days_in_month
self.day_widget:update()
end
end
end,
}
self:mergeLayoutInHorizontal(self.month_widget)
else
self.month_widget = dummy_widget
end
if self.day then
self.day_widget = NumberPickerWidget:new{
show_parent = self,
value = self.day,
value_min = self.day_min or 1,
value_max = self.day_max or 31,
value_step = 1,
value_hold_step = self.day_hold_step or 3,
width = number_picker_widgets_width,
date_month = self.month and self.month_widget or nil,
date_year = self.year and self.year_widget or nil,
}
self:mergeLayoutInHorizontal(self.day_widget)
else
self.day_widget = dummy_widget
end
if self.hour then
self.hour_widget = NumberPickerWidget:new{
show_parent = self,
value = self.hour,
value_min = self.hour_min or 0,
value_max = self.hour_max or 23,
value_step = 1,
value_hold_step = self.hour_hold_step or 4,
width = number_picker_widgets_width,
}
self:mergeLayoutInHorizontal(self.hour_widget)
else
self.hour_widget = dummy_widget
end
if self.min then
self.min_widget = NumberPickerWidget:new{
show_parent = self,
value = self.min,
value_min = self.min_min or 0,
value_max = self.min_max or 59,
value_step = self.min_step or 1,
value_hold_step = self.min_hold_step or 10,
width = number_picker_widgets_width,
}
self:mergeLayoutInHorizontal(self.min_widget)
else
self.min_widget = dummy_widget
end
if self.sec then
self.sec_widget = NumberPickerWidget:new{
show_parent = self,
value = self.sec,
value_min = self.sec_min or 0,
value_max = self.sec_max or 59,
value_step = 1,
value_hold_step = self.sec_hold_step or 10,
width = number_picker_widgets_width,
}
self:mergeLayoutInHorizontal(self.sec_widget)
else
self.sec_widget = dummy_widget
end
local separator_date = TextWidget:new{
text = "",
face = self.title_face,
bold = true,
}
local separator_time = TextWidget:new{
text = ":",
face = self.title_face,
bold = true,
}
local separator_date_time = TextWidget:new{
text = "/",
face = self.title_face,
bold = true,
}
local date_group = HorizontalGroup:new{
align = "center",
self.year_widget, -- 1
separator_date, -- 2
self.month_widget, -- 3
separator_date, -- 4
self.day_widget, -- 5
separator_date_time, -- 6
self.hour_widget, -- 7
separator_time, -- 8
self.min_widget, -- 9
separator_time, -- 10
self.sec_widget, -- 11
}
-- remove empty widgets plus trailing placeholder
for i = #date_group, 1, -2 do
if date_group[i] == dummy_widget then
table.remove(date_group, i)
table.remove(date_group, i-1)
end
end
-- clean up leading separator
if date_group[1] == separator_date or date_group[1] == separator_date_time or date_group[1] == separator_time then
table.remove(date_group, 1)
end
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
table.insert(buttons, {
{
text = self.default_text or T(_("Default value: %1"), self.default_value),
callback = function()
if self.default_callback then
self.default_callback({
year = self.year_widget:getValue(),
month = self.month_widget:getValue(),
day = self.day_widget:getValue(),
hour = self.hour_widget:getValue(),
minute = self.min_widget:getValue(),
second = self.sec_widget:getValue(),
})
end
if not self.keep_shown_on_apply then -- assume extra wants it same as ok
self:onClose()
end
end,
},
})
end
if self.extra_text then
table.insert(buttons, {
{
text = self.extra_text,
callback = function()
self.extra_callback(self)
end,
},
})
end
table.insert(buttons, {
{
text = self.cancel_text,
callback = function()
if self.cancel_callback then
self.cancel_callback(self)
end
self:onClose()
end,
},
{
text = self.ok_text,
callback = function()
if self.callback then
self.year = self.year_widget:getValue()
self.month = self.month_widget:getValue()
self.day = self.day_widget:getValue()
self.hour = self.hour_widget:getValue()
self.min = self.min_widget:getValue()
self.sec = self.sec_widget:getValue()
self:callback(self)
end
if not self.keep_shown_on_apply then
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.date_frame = FrameContainer:new{
radius = Size.radius.window,
bordersize = Size.border.window,
padding = 0,
margin = 0,
background = Blitbuffer.COLOR_WHITE,
VerticalGroup:new{
align = "left",
title_bar,
CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = math.floor(date_group:getSize().h * 1.2),
},
date_group,
},
CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = ok_cancel_buttons:getSize().h,
},
ok_cancel_buttons,
},
}
}
self[1] = WidgetContainer:new{
align = "center",
dimen = Geom:new{
x = 0, y = 0,
w = self.screen_width,
h = self.screen_height,
},
FrameContainer:new{
bordersize = 0,
padding = Size.padding.default,
self.date_frame,
}
}
self:refocusWidget()
end
function DateTimeWidget:addWidget(widget)
table.insert(self.layout, #self.layout, {widget})
widget = HorizontalGroup:new{
align = "center",
HorizontalSpan:new{ width = Size.span.horizontal_default },
widget,
}
table.insert(self.date_frame[1], #self.date_frame[1], widget)
if self.non_touch_with_action_dpad then
-- We need to reset focus again, otherwise FocusManager will not know about the new additions
-- and the cursor keys will become unresponsive.
local last_row = #self.layout
local last_col = #self.layout[last_row]
self:moveFocusTo(last_col, last_row)
end
end
function DateTimeWidget:getAddedWidgetAvailableWidth()
return self.date_frame[1][1].width - 2*Size.padding.default
end
function DateTimeWidget:update(year, month, day, hour, min, sec)
self.year_widget.value = year
self.year_widget:update()
self.month_widget.value = month
self.month_widget:update()
self.day_widget.value = day
self.day_widget:update()
self.hour_widget.value = hour
self.hour_widget:update()
self.min_widget.value = min
self.min_widget:update()
self.sec_widget.value = sec
self.sec_widget:update()
end
function DateTimeWidget:onCloseWidget()
-- Let our main WidgetContainer free its child widgets
self[1]:free()
UIManager:setDirty(nil, function()
return "ui", self.date_frame.dimen
end)
end
function DateTimeWidget:onShow()
UIManager:setDirty(self, function()
return "ui", self.date_frame.dimen
end)
return true
end
function DateTimeWidget:onTapClose(arg, ges_ev)
if ges_ev.pos:notIntersectWith(self.date_frame.dimen) then
self:onClose()
end
return true
end
function DateTimeWidget:onClose()
UIManager:close(self)
return true
end
--[[
This method processes value changes based on the direction of the spin, applying the appropriate
step value to the target widget component (left, center, or right).
@param args {table} A table containing:
- target_side {string}. Either "left_widget", "center_widget" or "right_widget" indicating which side to modify
- direction {number}. The direction of change (1 for increase, -1 for decrease)
@return {boolean} Returns true to indicate the event was handled
]]
function DateTimeWidget:onDateTimeButtonPressed(args)
local target_side, direction = unpack(args)
local widget_order = {
left_widget = { "year", "month", "day", "hour", "min" },
center_widget = { "month", "day", "hour", "min", "sec", "year" },
right_widget = { "sec", "min", "hour", "day", "month" }
}
local function get_target_widget(instance, side)
for _, key in ipairs(widget_order[side]) do
if instance[key] then
return instance[key .. "_widget"]
end
end
end
local target_widget = get_target_widget(self, target_side)
if self.day and target_widget == self.day_widget then
if target_widget.date_month and target_widget.date_year then
target_widget.value_max = target_widget:getDaysInMonth(
target_widget.date_month:getValue(),
target_widget.date_year:getValue()
)
end
end
target_widget.value = target_widget:changeValue(target_widget.value_step * direction)
target_widget:update()
return true
end
return DateTimeWidget