2
0
mirror of https://github.com/koreader/koreader synced 2024-11-16 06:12:56 +00:00
koreader/frontend/ui/widget/frontlightwidget.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

597 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.

local Blitbuffer = require("ffi/blitbuffer")
local Button = require("ui/widget/button")
local ButtonProgressWidget = require("ui/widget/buttonprogresswidget")
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 Math = require("optmath")
local NaturalLight = require("ui/widget/naturallightwidget")
local ProgressWidget = require("ui/widget/progresswidget")
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 VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local time = require("ui/time")
local _ = require("gettext")
local C_ = _.pgettext
local Screen = Device.screen
local FrontLightWidget = FocusManager:extend{
name = "FrontLightWidget",
width = nil,
height = nil,
-- This should stay active during natural light configuration
is_always_active = true,
rate = Screen.low_pan_rate and 3 or 30, -- Widget update rate.
last_time = 0, -- Tracks last update time to prevent update spamming.
}
function FrontLightWidget:init()
-- Layout constants
self.medium_font_face = Font:getFace("ffont")
self.screen_width = Screen:getWidth()
self.screen_height = Screen:getHeight()
self.span = Math.round(self.screen_height * 0.01)
self.width = math.floor(self.screen_width * 0.95)
-- State constants
self.powerd = Device:getPowerDevice()
-- Frontlight
self.fl = {}
self.fl.min = self.powerd.fl_min
self.fl.max = self.powerd.fl_max
self.fl.cur = self.powerd:frontlightIntensity()
local fl_steps = self.fl.max - self.fl.min + 1
self.fl.stride = math.ceil(fl_steps / 25)
self.fl.steps = math.ceil(fl_steps / self.fl.stride)
if (self.fl.steps - 1) * self.fl.stride < self.fl.max - self.fl.min then
self.fl.steps = self.fl.steps + 1
end
self.fl.steps = math.min(self.fl.steps, fl_steps)
-- Warmth
self.has_nl = Device:hasNaturalLight()
self.has_nl_mixer = Device:hasNaturalLightMixer()
self.has_nl_api = Device:hasNaturalLightApi()
if self.has_nl then
self.nl = {}
self.nl.min = self.powerd.fl_warmth_min
self.nl.max = self.powerd.fl_warmth_max
self.nl.cur = self.powerd:toNativeWarmth(self.powerd:frontlightWarmth())
local nl_steps = self.nl.max - self.nl.min + 1
self.nl.stride = math.ceil(nl_steps / 25)
self.nl.steps = math.ceil(nl_steps / self.nl.stride)
if (self.nl.steps - 1) * self.nl.stride < self.nl.max - self.nl.min then
self.nl.steps = self.nl.steps + 1
end
self.nl.steps = math.min(self.nl.steps, nl_steps)
end
-- Input
if Device:hasKeys() then
self.key_events.Close = { {Device.input.group.Back}, doc = "close frontlight" }
end
if Device:isTouchDevice() then
self.ges_events = {
TapProgress = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = self.screen_width,
h = self.screen_height,
}
},
},
PanProgress = {
GestureRange:new{
ges = "pan",
range = Geom:new{
x = 0, y = 0,
w = self.screen_width,
h = self.screen_height,
}
},
},
}
end
-- Widget layout
self:layout()
end
function FrontLightWidget:layout()
self.layout = {}
local main_container = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = math.floor(self.screen_height * 0.2),
},
}
-- Frontlight
-- Bigger spans, as ProgressWidget appears to be ever so slightly smaller than ButtonProgressWidget ;).
local fl_padding_span = VerticalSpan:new{ width = Math.round(self.span * 1.5) }
local fl_group_above = HorizontalGroup:new{ align = "center" }
local fl_group_below = HorizontalGroup:new{ align = "center" }
local main_group = VerticalGroup:new{ align = "center" }
local ticks = {}
for i = 1, self.fl.steps - 2 do
table.insert(ticks, i * self.fl.stride)
end
self.fl_progress = ProgressWidget:new{
width = math.floor(self.screen_width * 0.9),
height = Size.item.height_big,
percentage = self.fl.cur / self.fl.max,
ticks = ticks,
tick_width = Screen:scaleBySize(0.5),
last = self.fl.max,
}
local fl_header = TextWidget:new{
text = _("Brightness"),
face = self.medium_font_face,
bold = true,
max_width = math.floor(self.screen_width * 0.95),
}
self.fl_minus = Button:new{
text = "",
margin = Size.margin.small,
radius = 0,
enabled = self.fl.cur ~= self.fl.min,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setBrightness(self.fl.cur - 1)
end,
}
self.fl_plus = Button:new{
text = "",
margin = Size.margin.small,
radius = 0,
enabled = self.fl.cur ~= self.fl.max,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setBrightness(self.fl.cur + 1)
end,
}
self.fl_level = TextWidget:new{
text = tostring(self.fl.cur),
face = self.medium_font_face,
max_width = math.floor(self.screen_width * 0.95 - 1.275 * self.fl_minus.width - 1.275 * self.fl_plus.width),
}
local fl_level_container = CenterContainer:new{
dimen = Geom:new{
w = self.fl_level.max_width,
h = self.fl_level:getSize().h
},
self.fl_level,
}
local fl_min = Button:new{
text = C_("Extrema", "Min"),
margin = Size.margin.small,
radius = 0,
enabled = true,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setBrightness(self.fl.min + 1)
end, -- min is 1 (We use 0 to mean "toggle")
}
local fl_max = Button:new{
text = C_("Extrema", "Max"),
margin = Size.margin.small,
radius = 0,
enabled = true,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setBrightness(self.fl.max)
end,
}
local fl_toggle = Button:new{
text = _("Toggle"),
margin = Size.margin.small,
radius = 0,
enabled = true,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setBrightness(self.fl.min)
end,
}
local fl_spacer = HorizontalSpan:new{
width = math.floor((self.screen_width * 0.95 - 1.2 * self.fl_minus.width - 1.2 * self.fl_plus.width - 1.2 * fl_toggle.width) / 2),
}
local fl_buttons_above = HorizontalGroup:new{
align = "center",
self.fl_minus,
fl_level_container,
self.fl_plus,
}
self.layout[1] = {self.fl_minus, self.fl_plus}
local fl_buttons_below = HorizontalGroup:new{
align = "center",
fl_min,
fl_spacer,
fl_toggle,
fl_spacer,
fl_max,
}
self.layout[2] = {fl_min, fl_toggle, fl_max}
if self.has_nl then
-- Only insert a "Brightness" caption if we also add the full set of warmth widgets below,
-- otherwise, it's implied by the title bar ;).
table.insert(main_group, fl_header)
end
table.insert(fl_group_above, fl_buttons_above)
table.insert(fl_group_below, fl_buttons_below)
table.insert(main_group, fl_padding_span)
table.insert(main_group, fl_group_above)
table.insert(main_group, fl_padding_span)
table.insert(main_group, self.fl_progress)
table.insert(main_group, fl_padding_span)
table.insert(main_group, fl_group_below)
table.insert(main_group, fl_padding_span)
-- Warmth
if self.has_nl then
-- Smaller spans, as ButtonProgressWidget appears to be ever so slightly taller than ProgressWidget ;).
local nl_padding_span = VerticalSpan:new{ width = self.span }
local nl_group_above = HorizontalGroup:new{ align = "center" }
local nl_group_below = HorizontalGroup:new{ align = "center" }
self.nl_progress = ButtonProgressWidget:new{
width = math.floor(self.screen_width * 0.9),
font_size = 20, -- match Button's default
padding = 0,
thin_grey_style = false,
num_buttons = self.nl.steps - 1, -- no button for step 0
position = math.floor(self.nl.cur / self.nl.stride),
default_position = math.floor(self.nl.cur / self.nl.stride),
callback = function(i)
self:setWarmth(Math.round(i * self.nl.stride), false)
end,
show_parent = self,
enabled = true,
}
-- We want a wider gap between the two sets of widgets
local nl_span = VerticalSpan:new{ width = Size.span.vertical_large * 4 }
local nl_header = TextWidget:new{
text = _("Warmth"),
face = self.medium_font_face,
bold = true,
max_width = math.floor(self.screen_width * 0.95),
}
self.nl_minus = Button:new{
text = "",
margin = Size.margin.small,
radius = 0,
enabled = self.nl.cur ~= self.nl.min,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setWarmth(self.nl.cur - 1, true) end,
}
self.nl_plus = Button:new{
text = "",
margin = Size.margin.small,
radius = 0,
enabled = self.nl.cur ~= self.nl.max,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setWarmth(self.nl.cur + 1, true) end,
}
self.nl_level = TextWidget:new{
text = tostring(self.nl.cur),
face = self.medium_font_face,
max_width = math.floor(self.screen_width * 0.95 - 1.275 * self.nl_minus.width - 1.275 * self.nl_plus.width),
}
local nl_level_container = CenterContainer:new{
dimen = Geom:new{
w = self.nl_level.max_width,
h = self.nl_level:getSize().h
},
self.nl_level,
}
local nl_min = Button:new{
text = C_("Extrema", "Min"),
margin = Size.margin.small,
radius = 0,
enabled = true,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setWarmth(self.nl.min, true)
end,
}
local nl_max = Button:new{
text = C_("Extrema", "Max"),
margin = Size.margin.small,
radius = 0,
enabled = true,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
self:setWarmth(self.nl.max, true)
end,
}
local nl_spacer = HorizontalSpan:new{
width = math.floor((self.screen_width * 0.95 - 1.2 * self.nl_minus.width - 1.2 * self.nl_plus.width) / 2),
}
local nl_buttons_above = HorizontalGroup:new{
align = "center",
self.nl_minus,
nl_level_container,
self.nl_plus,
}
self.layout[3] = {self.nl_minus, self.nl_plus}
local nl_buttons_below = HorizontalGroup:new{
align = "center",
nl_min,
nl_spacer,
nl_max,
}
self.layout[4] = {nl_min, nl_max}
table.insert(main_group, nl_span)
table.insert(main_group, nl_header)
table.insert(nl_group_above, nl_buttons_above)
table.insert(nl_group_below, nl_buttons_below)
table.insert(main_group, nl_padding_span)
table.insert(main_group, nl_group_above)
table.insert(main_group, nl_padding_span)
table.insert(main_group, self.nl_progress)
table.insert(main_group, nl_padding_span)
table.insert(main_group, nl_group_below)
table.insert(main_group, nl_padding_span)
-- Aura One R/G/B widget
if not self.has_nl_mixer and not self.has_nl_api then
local nl_setup = Button:new{
text = _("Configure"),
margin = Size.margin.small,
radius = 0,
width = math.floor(self.screen_width * 0.2),
show_parent = self,
callback = function()
UIManager:show(NaturalLight:new{fl_widget = self})
end,
}
table.insert(main_group, nl_setup)
self.layout[5] = {nl_setup}
end
end
table.insert(main_container, main_group)
-- Reset container height to what it actually contains
main_container.dimen.h = main_group:getSize().h
-- Common
local title_bar = TitleBar:new{
title = _("Frontlight"),
width = self.width,
align = "left",
with_bottom_line = true,
bottom_v_padding = 0,
close_callback = function()
self:onClose()
end,
show_parent = self,
}
local inner_frame = FrameContainer:new{
padding = Size.padding.button,
margin = Size.margin.small,
bordersize = 0,
main_container,
}
local center_container = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = inner_frame:getSize().h,
},
inner_frame,
}
self.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,
center_container,
}
}
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,
self.frame,
},
}
end
function FrontLightWidget:update()
self:refocusWidget()
UIManager:setDirty(self, function()
return "ui", self.frame.dimen
end)
return true
end
function FrontLightWidget:updateBrightnessWidgets()
self.fl_progress:setPercentage(self.fl.cur / self.fl.max)
self.fl_level:setText(tostring(self.fl.cur))
if self.fl.cur == self.fl.min then
self.fl_minus:disable()
else
self.fl_minus:enable()
end
if self.fl.cur == self.fl.max then
self.fl_plus:disable()
else
self.fl_plus:enable()
end
end
function FrontLightWidget:refreshBrightnessWidgets()
self:updateBrightnessWidgets()
self:update()
end
function FrontLightWidget:setBrightness(intensity)
-- Let fl.min through, as that's what we use for the Toggle button ;).
if intensity ~= self.fl.min and intensity == self.fl.cur then
return
end
-- Set brightness
self:setFrontLightIntensity(intensity)
-- Update the progress bar
self:updateBrightnessWidgets()
-- Refresh widget
self:update()
end
function FrontLightWidget:setWarmth(warmth, update_position)
if warmth == self.nl.cur then
return
end
-- Set warmth
self.powerd:setWarmth(self.powerd:fromNativeWarmth(warmth))
-- Retrieve the value PowerD actually set, in case there were rounding shenanigans and we blew the range...
self.nl.cur = self.powerd:toNativeWarmth(self.powerd:frontlightWarmth())
-- If we were not called by ButtonProgressWidget's callback, we'll have to update its progress bar ourselves.
if update_position then
self.nl_progress:setPosition(math.floor(self.nl.cur / self.nl.stride), self.nl_progress.default_position)
end
self.nl_level:setText(tostring(self.nl.cur))
if self.nl.cur == self.nl.min then
self.nl_minus:disable()
else
self.nl_minus:enable()
end
if self.nl.cur == self.nl.max then
self.nl_plus:disable()
else
self.nl_plus:enable()
end
-- Refresh widget
self:update()
end
function FrontLightWidget:setFrontLightIntensity(intensity)
self.fl.cur = intensity
-- min (which is always 0) means toggle
if self.fl.cur == self.fl.min then
self.powerd:toggleFrontlight()
else
self.powerd:setIntensity(self.fl.cur)
end
-- Retrieve the real level set by PowerD (will be different from `intensity` on toggle)
self.fl.cur = self.powerd:frontlightIntensity()
end
function FrontLightWidget:onCloseWidget()
UIManager:setDirty(nil, function()
return "flashui", self.frame.dimen
end)
end
function FrontLightWidget:onShow()
-- NOTE: Keep this one as UI, it'll get coalesced...
UIManager:setDirty(self, function()
return "ui", self.frame.dimen
end)
return true
end
function FrontLightWidget:onAnyKeyPressed()
UIManager:close(self)
return true
end
function FrontLightWidget:onClose()
UIManager:close(self)
return true
end
function FrontLightWidget:onTapProgress(arg, ges_ev)
-- The throttling has a tendency to wreak a bit of a havoc,
-- so, if the widget hasn't been repainted yet, go away.
if not self.fl_progress.dimen or not self.frame.dimen then
return true
end
if ges_ev.pos:intersectWith(self.fl_progress.dimen) then
-- Unschedule any pending updates.
UIManager:unschedule(self.refreshBrightnessWidgets)
local perc = self.fl_progress:getPercentageFromPosition(ges_ev.pos)
if not perc then
return true
end
local num = Math.round(perc * self.fl.max)
-- Always set the frontlight intensity.
self:setFrontLightIntensity(num)
-- But limit the widget update frequency on E Ink.
if Screen.low_pan_rate then
local current_time = time.now()
local last_time = self.last_time or 0
if current_time - last_time > time.s(1 / self.rate) then
self.last_time = current_time
else
-- Schedule a final update after we stop panning.
UIManager:scheduleIn(0.075, self.refreshBrightnessWidgets, self)
return true
end
end
self:refreshBrightnessWidgets()
elseif not ges_ev.pos:intersectWith(self.frame.dimen) and ges_ev.ges == "tap" then
-- Close when tapping outside.
self:onClose()
end
-- Otherwise, do nothing (it's easy to miss a button).
return true
end
FrontLightWidget.onPanProgress = FrontLightWidget.onTapProgress
return FrontLightWidget