mirror of
https://github.com/koreader/koreader.git
synced 2025-12-18 12:02:09 +01:00
* Wherever possible, do an actual dumb invert on the Screen BB instead of repainting the widget, *then* inverting it (which is what the "invert" flag does). * Instead of playing with nextTick/tickAfterNext delays, explicitly fence stuff with forceRePaint * And, in the few cases where said Mk. 7 quirk kicks in, make the fences more marked by using a well-placed WAIT_FOR_UPDATE_COMPLETE * Fix an issue in Button where show/hide & enable/disable where actually all toggles, which meant that duplicate calls or timing issues would do the wrong thing. (This broke dimming some icons, and mistakenly dropped the background from FM chevrons, for example). * Speaking of, fix Button's hide/show to actually restore the background properly (there was a stupid typo in the variable name) * Still in Button, fix the insanity of the double repaint on rounded buttons. Turns out it made sense, after all (and was related to said missing background, and bad interaction with invert & text with no background). * KeyValuePage suffered from a similar issue with broken highlights (all black) because of missing backgrounds. * In ConfigDialog, only instanciate IconButtons once (because every tab switch causes a full instantiation; and the initial display implies a full instanciation and an initial tab switch). Otherwise, both instances linger, and catch taps, and as such, double highlights. * ConfigDialog: Restore the "don't repaint ReaderUI" when switching between similarly sized tabs (re #6131). I never could reproduce that on eInk, and I can't now on the emulator, so I'm assuming @poire-z fixed it during the swap to SVG icons. * KeyValuePage: Only instanciate Buttons once (again, this is a widget that goes through a full init every page). Again, caused highlight/dimming issues because buttons were stacked. * Menu: Ditto. * TouchMenu: Now home of the gnarliest unhilight heuristics, because of the sheer amount of different things that can happen (and/or thanks to stuff not flagged covers_fullscreen properly ;p). * Bump base https://github.com/koreader/koreader-base/pull/1280 https://github.com/koreader/koreader-base/pull/1282 https://github.com/koreader/koreader-base/pull/1283 https://github.com/koreader/koreader-base/pull/1284 * Bump android-luajit-launcher https://github.com/koreader/android-luajit-launcher/pull/284 https://github.com/koreader/android-luajit-launcher/pull/285 https://github.com/koreader/android-luajit-launcher/pull/286 https://github.com/koreader/android-luajit-launcher/pull/287
913 lines
32 KiB
Lua
913 lines
32 KiB
Lua
--[[--
|
|
TouchMenu widget for hierarchical menus.
|
|
]]
|
|
local BD = require("ui/bidi")
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
local Button = require("ui/widget/button")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
local CheckMark = require("ui/widget/checkmark")
|
|
local Device = require("device")
|
|
local Event = require("ui/event")
|
|
local FocusManager = require("ui/widget/focusmanager")
|
|
local Font = require("ui/font")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
|
local HorizontalSpan = require("ui/widget/horizontalspan")
|
|
local IconButton = require("ui/widget/iconbutton")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local LeftContainer = require("ui/widget/container/leftcontainer")
|
|
local LineWidget = require("ui/widget/linewidget")
|
|
local RightContainer = require("ui/widget/container/rightcontainer")
|
|
local Size = require("ui/size")
|
|
local TextWidget = require("ui/widget/textwidget")
|
|
local UIManager = require("ui/uimanager")
|
|
local UnderlineContainer = require("ui/widget/container/underlinecontainer")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
local util = require("util")
|
|
local getMenuText = require("ui/widget/menu").getMenuText
|
|
local _ = require("gettext")
|
|
local T = require("ffi/util").template
|
|
local Input = Device.input
|
|
local Screen = Device.screen
|
|
|
|
--[[
|
|
TouchMenuItem widget
|
|
--]]
|
|
local TouchMenuItem = InputContainer:new{
|
|
menu = nil,
|
|
vertical_align = "center",
|
|
item = nil,
|
|
dimen = nil,
|
|
face = Font:getFace("smallinfofont"),
|
|
show_parent = nil,
|
|
}
|
|
|
|
function TouchMenuItem:init()
|
|
self.ges_events = {
|
|
TapSelect = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = self.dimen,
|
|
},
|
|
doc = "Select Menu Item",
|
|
},
|
|
HoldSelect = {
|
|
GestureRange:new{
|
|
ges = "hold",
|
|
range = self.dimen,
|
|
},
|
|
doc = "Hold Menu Item",
|
|
},
|
|
}
|
|
|
|
local item_enabled = self.item.enabled
|
|
if self.item.enabled_func then
|
|
item_enabled = self.item.enabled_func()
|
|
end
|
|
local item_checkable = false
|
|
local item_checked = self.item.checked
|
|
if self.item.checked_func then
|
|
item_checkable = true
|
|
item_checked = self.item.checked_func()
|
|
end
|
|
local checkmark_widget = CheckMark:new{
|
|
checkable = item_checkable,
|
|
checked = item_checked,
|
|
enabled = item_enabled,
|
|
}
|
|
|
|
local checked_widget = CheckMark:new{ -- for layout, to :getSize()
|
|
checked = true,
|
|
}
|
|
|
|
-- text_max_width should be the TouchMenuItem width minus the below
|
|
-- FrameContainer default paddings minus the checked widget width
|
|
local text_max_width = self.dimen.w - 2*Size.padding.default - checked_widget:getSize().w
|
|
local text = getMenuText(self.item)
|
|
local face = self.face
|
|
local forced_baseline, forced_height
|
|
if self.item.font_func then
|
|
-- A font_func() may be provided by ReaderFont to have each font name
|
|
-- displayed in its own font: we must tell TextWidget to use the default
|
|
-- font baseline and height for items to be correctly aligned without
|
|
-- variations due to each font different metrics.
|
|
face = self.item.font_func(self.face.orig_size)
|
|
if face then
|
|
local w = TextWidget:new{ text = "", face = self.face }
|
|
forced_baseline = w:getBaseline()
|
|
forced_height = w:getSize().h
|
|
w:free()
|
|
else
|
|
face = self.face
|
|
end
|
|
end
|
|
self.item_frame = FrameContainer:new{
|
|
width = self.dimen.w,
|
|
bordersize = 0,
|
|
color = Blitbuffer.COLOR_BLACK,
|
|
HorizontalGroup:new {
|
|
align = "center",
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = checked_widget:getSize().w },
|
|
checkmark_widget,
|
|
},
|
|
TextWidget:new{
|
|
text = text,
|
|
max_width = text_max_width,
|
|
fgcolor = item_enabled ~= false and Blitbuffer.COLOR_BLACK or Blitbuffer.COLOR_DARK_GRAY,
|
|
face = face,
|
|
forced_baseline = forced_baseline,
|
|
forced_height = forced_height,
|
|
},
|
|
},
|
|
}
|
|
|
|
self._underline_container = UnderlineContainer:new{
|
|
vertical_align = "center",
|
|
dimen = self.dimen,
|
|
self.item_frame
|
|
}
|
|
|
|
self[1] = self._underline_container
|
|
function self:isEnabled()
|
|
return item_enabled ~= false and true
|
|
end
|
|
end
|
|
|
|
function TouchMenuItem:onFocus()
|
|
self._underline_container.color = Blitbuffer.COLOR_BLACK
|
|
return true
|
|
end
|
|
|
|
function TouchMenuItem:onUnfocus()
|
|
self._underline_container.color = Blitbuffer.COLOR_WHITE
|
|
return true
|
|
end
|
|
|
|
function TouchMenuItem:onTapSelect(arg, ges)
|
|
local enabled = self.item.enabled
|
|
if self.item.enabled_func then
|
|
enabled = self.item.enabled_func()
|
|
end
|
|
if enabled == false then return end
|
|
|
|
if G_reader_settings:isFalse("flash_ui") then
|
|
self.menu:onMenuSelect(self.item)
|
|
else
|
|
-- The item frame's width stops at the text width, but we want it to match the menu's length instead
|
|
local highlight_dimen = self.item_frame.dimen
|
|
highlight_dimen.w = self.item_frame.width
|
|
|
|
self.item_frame.invert = true
|
|
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
|
UIManager:setDirty(nil, function()
|
|
return "fast", highlight_dimen
|
|
end)
|
|
|
|
-- Force the repaint *now*, so we don't have to delay the callback to see the invert...
|
|
UIManager:forceRePaint()
|
|
self.menu:onMenuSelect(self.item)
|
|
UIManager:forceRePaint()
|
|
--UIManager:waitForVSync()
|
|
|
|
self.item_frame.invert = false
|
|
-- NOTE: We can *usually* optimize that repaint away, as most entries in the menu will at least trigger a menu repaint ;).
|
|
-- But when stuff doesn't repaint the menu and keeps it open, we need to do it.
|
|
-- Since it's an *un*highlight containing text, we make it "ui" and not "fast", both so it won't mangle text,
|
|
-- and because "fast" can have some weird side-effects on some devices in this specific instance...
|
|
if self.item.hold_keep_menu_open or self.item.keep_menu_open then
|
|
local top_widget = UIManager:getTopWidget()
|
|
-- If the callback opened a full-screen widget, we're done
|
|
if top_widget.covers_fullscreen then
|
|
return true
|
|
end
|
|
|
|
-- If we're still on top, or if a modal was opened outside of our highlight region, we can unhighlight safely
|
|
if top_widget == self.menu or highlight_dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then
|
|
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
|
UIManager:setDirty(nil, function()
|
|
return "ui", highlight_dimen
|
|
end)
|
|
else
|
|
-- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case,
|
|
-- we can't take any shortcuts, as it would invert/paint *over* the popop.
|
|
-- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly.
|
|
UIManager:waitForVSync()
|
|
UIManager:setDirty(self.show_parent, function()
|
|
return "ui", highlight_dimen
|
|
end)
|
|
end
|
|
end
|
|
--UIManager:forceRePaint()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function TouchMenuItem:onHoldSelect(arg, ges)
|
|
local enabled = self.item.enabled
|
|
if self.item.enabled_func then
|
|
enabled = self.item.enabled_func()
|
|
end
|
|
if enabled == false then return end
|
|
|
|
if G_reader_settings:isFalse("flash_ui") then
|
|
self.menu:onMenuHold(self.item)
|
|
else
|
|
-- The item frame's width stops at the text width, but we want it to match the menu's length instead
|
|
local highlight_dimen = self.item_frame.dimen
|
|
highlight_dimen.w = self.item_frame.width
|
|
|
|
self.item_frame.invert = true
|
|
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
|
UIManager:setDirty(nil, function()
|
|
return "fast", highlight_dimen
|
|
end)
|
|
|
|
-- Force the repaint *now*, so we don't have to delay the callback to see the invert...
|
|
UIManager:forceRePaint()
|
|
self.menu:onMenuHold(self.item)
|
|
UIManager:forceRePaint()
|
|
--UIManager:waitForVSync()
|
|
|
|
self.item_frame.invert = false
|
|
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
|
UIManager:setDirty(nil, function()
|
|
return "ui", highlight_dimen
|
|
end)
|
|
--UIManager:forceRePaint()
|
|
end
|
|
return true
|
|
end
|
|
|
|
--[[
|
|
TouchMenuBar widget
|
|
--]]
|
|
local TouchMenuBar = InputContainer:new{
|
|
width = Screen:getWidth(),
|
|
icons = {},
|
|
-- touch menu that holds the bar, used for trigger repaint on icons
|
|
show_parent = nil,
|
|
menu = nil,
|
|
}
|
|
|
|
function TouchMenuBar:init()
|
|
local icon_sep_width = Size.span.vertical_default
|
|
local icons_sep_width = icon_sep_width * (#self.icons + 1)
|
|
-- we assume all icons are of the same width
|
|
local icon_width = Screen:scaleBySize(DGENERIC_ICON_SIZE)
|
|
local icon_height = icon_width
|
|
-- content_width is the width of all the icon images
|
|
local content_width = icon_width * #self.icons + icons_sep_width
|
|
local spacing_width = (self.width - content_width)/(#self.icons*2)
|
|
local icon_padding = math.min(spacing_width, Screen:scaleBySize(16))
|
|
self.height = icon_height + 2*Size.padding.default
|
|
self.show_parent = self.show_parent or self
|
|
self.bar_icon_group = HorizontalGroup:new{}
|
|
-- build up image widget for menu icon bar
|
|
self.icon_widgets = {}
|
|
-- hold icon seperators
|
|
self.icon_seps = {}
|
|
-- the start_seg for first icon_widget should be 0
|
|
-- we asign negative here to offset it in the loop
|
|
local start_seg = -icon_sep_width
|
|
local end_seg = start_seg
|
|
-- self.width is the screen width
|
|
-- content_width is the width of all the icon images
|
|
-- (2 * icon_padding * #self.icons) is the combined width of icons paddings
|
|
local stretch_width = self.width - content_width - (2 * icon_padding * #self.icons) + icon_sep_width
|
|
|
|
for k, v in ipairs(self.icons) do
|
|
local ib = IconButton:new{
|
|
show_parent = self.show_parent,
|
|
icon = v,
|
|
width = icon_width,
|
|
height = icon_height,
|
|
callback = nil,
|
|
padding_left = icon_padding,
|
|
padding_right = icon_padding,
|
|
menu = self.menu,
|
|
}
|
|
|
|
table.insert(self.icon_widgets, ib)
|
|
table.insert(self.menu.layout, ib) -- for the focusmanager
|
|
|
|
-- we have to use local variable here for closure callback
|
|
local _start_seg = end_seg + icon_sep_width
|
|
local _end_seg = _start_seg + self.icon_widgets[k]:getSize().w
|
|
end_seg = _end_seg -- for next loop _start_seg
|
|
|
|
if BD.mirroredUILayout() then
|
|
_start_seg, _end_seg = self.width - _end_seg, self.width - _start_seg
|
|
end
|
|
|
|
if k == 1 then
|
|
self.bar_sep = LineWidget:new{
|
|
dimen = Geom:new{
|
|
w = self.width,
|
|
h = Size.line.thick,
|
|
},
|
|
empty_segments = {
|
|
{
|
|
s = _start_seg, e = _end_seg
|
|
}
|
|
},
|
|
}
|
|
end
|
|
|
|
local icon_sep = LineWidget:new{
|
|
style = k == 1 and "solid" or "none",
|
|
dimen = Geom:new{
|
|
w = icon_sep_width,
|
|
h = self.height,
|
|
}
|
|
}
|
|
-- no separator on the right
|
|
if k < #self.icons then
|
|
table.insert(self.icon_seps, icon_sep)
|
|
end
|
|
|
|
-- callback to set visual style
|
|
ib.callback = function()
|
|
self.bar_sep.empty_segments = {
|
|
{
|
|
s = _start_seg, e = _end_seg
|
|
}
|
|
}
|
|
for i, sep in ipairs(self.icon_seps) do
|
|
local current_icon, last_icon
|
|
if k == #self.icons then
|
|
current_icon = false
|
|
last_icon = i == k
|
|
else
|
|
current_icon = i == k - 1 or i == k
|
|
last_icon = false
|
|
end
|
|
|
|
-- if the active icon is the last icon then the empty bar segment has
|
|
-- to move over to the right by the width of a separator and the stretch width
|
|
if last_icon then
|
|
local _start_last_seg = icon_sep_width + stretch_width + _start_seg
|
|
local _end_last_seg = icon_sep_width + stretch_width + _end_seg
|
|
if BD.mirroredUILayout() then
|
|
_start_last_seg = _start_seg - icon_sep_width - stretch_width
|
|
_end_last_seg = _end_seg - icon_sep_width - stretch_width
|
|
end
|
|
self.bar_sep.empty_segments = {
|
|
{
|
|
s = _start_last_seg, e = _end_last_seg
|
|
}
|
|
}
|
|
sep.style = "solid"
|
|
-- regular behavior
|
|
else
|
|
sep.style = current_icon and "solid" or "none"
|
|
end
|
|
end
|
|
self.menu:switchMenuTab(k)
|
|
end
|
|
|
|
table.insert(self.bar_icon_group, self.icon_widgets[k])
|
|
table.insert(self.bar_icon_group, icon_sep)
|
|
|
|
-- if we're at the before-last icon, add an extra span and the final separator
|
|
if k == #self.icons - 1 then
|
|
table.insert(self.bar_icon_group, HorizontalSpan:new{
|
|
width = stretch_width
|
|
})
|
|
-- need to create a new LineWidget otherwise it's just a reference to the same instance
|
|
local icon_sep_duplicate = LineWidget:new{
|
|
style = "none",
|
|
dimen = Geom:new{
|
|
w = icon_sep_width,
|
|
h = self.height,
|
|
}
|
|
}
|
|
table.insert(self.icon_seps, icon_sep_duplicate)
|
|
table.insert(self.bar_icon_group, icon_sep_duplicate)
|
|
end
|
|
end
|
|
|
|
self[1] = FrameContainer:new{
|
|
bordersize = 0,
|
|
padding = 0,
|
|
VerticalGroup:new{
|
|
align = "left",
|
|
-- bar icons
|
|
self.bar_icon_group,
|
|
-- horizontal separate line
|
|
self.bar_sep
|
|
},
|
|
}
|
|
self.dimen = Geom:new{ w = self.width, h = self.height }
|
|
end
|
|
|
|
function TouchMenuBar:switchToTab(index)
|
|
-- a little safety check
|
|
-- don't auto-activate a non-existent index
|
|
if index > #self.icon_widgets then
|
|
index = #self.icon_widgets
|
|
end
|
|
if self.menu.tab_item_table[index] and self.menu.tab_item_table[index].remember == false then
|
|
-- Don't auto-activate those that should not be
|
|
-- remembered (FM plus menu on non-touch devices)
|
|
index = 1
|
|
end
|
|
self.icon_widgets[index].callback()
|
|
end
|
|
|
|
--[[
|
|
TouchMenu widget for hierarchical menus
|
|
--]]
|
|
local TouchMenu = FocusManager:new{
|
|
tab_item_table = {},
|
|
-- for returning in multi-level menus
|
|
item_table_stack = nil,
|
|
item_table = nil,
|
|
item_height = Size.item.height_large,
|
|
bordersize = Size.border.window,
|
|
padding = Size.padding.default, -- (not used at top)
|
|
fface = Font:getFace("ffont"),
|
|
width = nil,
|
|
height = nil,
|
|
page = 1,
|
|
max_per_page_default = 10,
|
|
-- for UIManager:setDirty
|
|
show_parent = nil,
|
|
cur_tab = -1,
|
|
close_callback = nil,
|
|
is_fresh = true,
|
|
}
|
|
|
|
function TouchMenu:init()
|
|
-- We won't include self.bordersize in our width calculations, so that
|
|
-- borders are pushed off-(screen-)width and so not visible.
|
|
-- We'll then be similar to bottom menu ConfigDialog (where this
|
|
-- nice effect is caused by some width calculations bug).
|
|
if not self.dimen then self.dimen = Geom:new{} end
|
|
self.show_parent = self.show_parent or self
|
|
if not self.close_callback then
|
|
self.close_callback = function()
|
|
UIManager:close(self.show_parent)
|
|
end
|
|
end
|
|
|
|
self.layout = {}
|
|
|
|
self.ges_events.TapCloseAllMenus = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = Geom:new{
|
|
x = 0, y = 0,
|
|
w = Screen:getWidth(),
|
|
h = Screen:getHeight(),
|
|
}
|
|
}
|
|
}
|
|
self.ges_events.Swipe = {
|
|
GestureRange:new{
|
|
ges = "swipe",
|
|
range = self.dimen,
|
|
}
|
|
}
|
|
|
|
self.key_events.Back = { {"Back"}, doc = "back to upper menu or close touchmenu" }
|
|
if Device:hasFewKeys() then
|
|
self.key_events.Back = { {"Left"}, doc = "back to upper menu or close touchmenu" }
|
|
end
|
|
self.key_events.NextPage = { {Input.group.PgFwd}, doc = "next page" }
|
|
self.key_events.PrevPage = { {Input.group.PgBack}, doc = "previous page" }
|
|
self.key_events.Press = { {"Press"}, doc = "chose selected item" }
|
|
|
|
local icons = {}
|
|
for _, v in ipairs(self.tab_item_table) do
|
|
table.insert(icons, v.icon)
|
|
end
|
|
self.bar = TouchMenuBar:new{
|
|
width = self.width, -- will impose width and push left and right borders offscreen
|
|
icons = icons,
|
|
show_parent = self.show_parent,
|
|
menu = self,
|
|
}
|
|
|
|
self.item_group = VerticalGroup:new{
|
|
align = "center",
|
|
}
|
|
-- group for page info
|
|
local chevron_left = "chevron.left"
|
|
local chevron_right = "chevron.right"
|
|
if BD.mirroredUILayout() then
|
|
chevron_left, chevron_right = chevron_right, chevron_left
|
|
end
|
|
self.page_info_left_chev = Button:new{
|
|
icon = chevron_left,
|
|
callback = function() self:onPrevPage() end,
|
|
bordersize = 0,
|
|
show_parent = self.show_parent,
|
|
}
|
|
self.page_info_right_chev = Button:new{
|
|
icon = chevron_right,
|
|
callback = function() self:onNextPage() end,
|
|
bordersize = 0,
|
|
show_parent = self.show_parent,
|
|
}
|
|
self.page_info_left_chev:hide()
|
|
self.page_info_right_chev:hide()
|
|
self.page_info_text = TextWidget:new{
|
|
text = "",
|
|
face = self.fface,
|
|
}
|
|
self.page_info = HorizontalGroup:new{
|
|
self.page_info_left_chev,
|
|
self.page_info_text,
|
|
self.page_info_right_chev
|
|
}
|
|
-- group for device info
|
|
self.time_info = TextWidget:new{
|
|
text = "",
|
|
face = self.fface,
|
|
}
|
|
self.device_info = HorizontalGroup:new{
|
|
self.time_info,
|
|
-- Add some span to balance up_button image included padding
|
|
HorizontalSpan:new{width = Size.span.horizontal_default},
|
|
}
|
|
local footer_width = self.width - self.padding*2
|
|
local up_button = IconButton:new{
|
|
icon = "chevron.up",
|
|
show_parent = self.show_parent,
|
|
padding_left = math.floor(footer_width*0.33*0.1),
|
|
padding_right = math.floor(footer_width*0.33*0.1),
|
|
callback = function()
|
|
self:backToUpperMenu()
|
|
end,
|
|
}
|
|
local footer_height = up_button:getSize().h + Size.line.thick
|
|
self.footer = HorizontalGroup:new{
|
|
LeftContainer:new{
|
|
dimen = Geom:new{ w = math.floor(footer_width*0.33), h = footer_height},
|
|
up_button,
|
|
},
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = math.floor(footer_width*0.33), h = footer_height},
|
|
self.page_info,
|
|
},
|
|
RightContainer:new{
|
|
dimen = Geom:new{ w = math.floor(footer_width*0.33), h = footer_height},
|
|
self.device_info,
|
|
}
|
|
}
|
|
|
|
self.menu_frame = FrameContainer:new{
|
|
padding = self.padding,
|
|
padding_top = 0, -- ensured by TouchMenuBar
|
|
bordersize = self.bordersize,
|
|
background = Blitbuffer.COLOR_WHITE,
|
|
-- menubar and footer will be inserted in
|
|
-- item_group in updateItems
|
|
self.item_group,
|
|
}
|
|
-- This CenterContainer will make the left and right borders drawn
|
|
-- off-screen
|
|
self[1] = CenterContainer:new{
|
|
dimen = Screen:getSize(),
|
|
ignore = "height",
|
|
self.menu_frame
|
|
}
|
|
|
|
self.item_width = self.width - self.padding*2
|
|
self.split_line = HorizontalGroup:new{
|
|
-- pad with 10 pixel to align with the up arrow in footer
|
|
HorizontalSpan:new{width = Size.span.horizontal_default},
|
|
LineWidget:new{
|
|
background = Blitbuffer.COLOR_GRAY,
|
|
dimen = Geom:new{
|
|
w = self.item_width - 2*Size.span.horizontal_default,
|
|
h = Size.line.medium,
|
|
}
|
|
},
|
|
HorizontalSpan:new{width = Size.span.horizontal_default},
|
|
}
|
|
self.footer_top_margin = VerticalSpan:new{width = Size.span.vertical_default}
|
|
self.bar:switchToTab(self.last_index or 1)
|
|
end
|
|
|
|
function TouchMenu:onCloseWidget()
|
|
-- NOTE: We don't pass a region in order to ensure a full-screen flash to avoid ghosting
|
|
UIManager:setDirty(nil, "flashui")
|
|
end
|
|
|
|
function TouchMenu:_recalculatePageLayout()
|
|
local content_height -- content == item_list + footer
|
|
|
|
local bar_height = self.bar:getSize().h
|
|
local footer_height = self.footer:getSize().h
|
|
if self.height then
|
|
content_height = self.height - bar_height
|
|
else
|
|
content_height = #self.item_table * self.item_height + footer_height
|
|
-- split line height
|
|
content_height = content_height + (#self.item_table - 1)
|
|
content_height = content_height + self.footer_top_margin:getSize().h
|
|
end
|
|
if content_height + bar_height > Screen:getHeight() then
|
|
content_height = Screen:getHeight() - bar_height
|
|
end
|
|
|
|
local item_list_content_height = content_height - footer_height
|
|
self.perpage = math.floor(item_list_content_height / self.item_height)
|
|
local max_per_page = self.item_table.max_per_page or self.max_per_page_default
|
|
if self.perpage > max_per_page then
|
|
self.perpage = max_per_page
|
|
end
|
|
|
|
self.page_num = math.ceil(#self.item_table / self.perpage)
|
|
end
|
|
|
|
function TouchMenu:updateItems()
|
|
local old_dimen = self.dimen and self.dimen:copy()
|
|
self:_recalculatePageLayout()
|
|
self.item_group:clear()
|
|
self.layout = {}
|
|
table.insert(self.item_group, self.bar)
|
|
table.insert(self.layout, self.bar.icon_widgets) -- for the focusmanager
|
|
|
|
for c = 1, self.perpage do
|
|
-- calculate index in item_table
|
|
local i = (self.page - 1) * self.perpage + c
|
|
if i <= #self.item_table then
|
|
local item = self.item_table[i]
|
|
local item_tmp = TouchMenuItem:new{
|
|
item = item,
|
|
menu = self,
|
|
dimen = Geom:new{
|
|
w = self.item_width,
|
|
h = self.item_height,
|
|
},
|
|
show_parent = self.show_parent,
|
|
}
|
|
table.insert(self.item_group, item_tmp)
|
|
if item_tmp:isEnabled() then
|
|
table.insert(self.layout, {[self.cur_tab] = item_tmp}) -- for the focusmanager
|
|
end
|
|
if item.separator and c ~= self.perpage then
|
|
-- insert split line
|
|
table.insert(self.item_group, self.split_line)
|
|
end
|
|
else
|
|
-- item not enough to fill the whole page, break out of loop
|
|
break
|
|
end -- if i <= self.items
|
|
end -- for c=1, self.perpage
|
|
|
|
table.insert(self.item_group, self.footer_top_margin)
|
|
table.insert(self.item_group, self.footer)
|
|
if self.page_num > 1 then
|
|
-- @translators %1 is the current page. %2 is the total number of pages. In some languages a good translation might need to reverse this order, for instance: "Total %2, page %1".
|
|
self.page_info_text:setText(T(_("Page %1 of %2"), self.page, self.page_num))
|
|
else
|
|
self.page_info_text:setText("")
|
|
end
|
|
self.page_info_left_chev:showHide(self.page_num > 1)
|
|
self.page_info_right_chev:showHide(self.page_num > 1)
|
|
self.page_info_left_chev:enableDisable(self.page > 1)
|
|
self.page_info_right_chev:enableDisable(self.page < self.page_num)
|
|
|
|
local time_info_txt = util.secondsToHour(os.time(), G_reader_settings:isTrue("twelve_hour_clock"))
|
|
local powerd = Device:getPowerDevice()
|
|
local batt_lvl = powerd:getCapacity()
|
|
local batt_symbol
|
|
if powerd:isCharging() then
|
|
batt_symbol = ""
|
|
else
|
|
if batt_lvl >= 100 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 90 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 80 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 70 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 60 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 50 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 40 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 30 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 20 then
|
|
batt_symbol = ""
|
|
elseif batt_lvl >= 10 then
|
|
batt_symbol = ""
|
|
else
|
|
batt_symbol = ""
|
|
end
|
|
end
|
|
if Device:hasBattery() then
|
|
time_info_txt = BD.wrap(time_info_txt) .. " " .. BD.wrap("⌁") .. BD.wrap(batt_symbol) .. BD.wrap(batt_lvl .. "%")
|
|
end
|
|
self.time_info:setText(time_info_txt)
|
|
|
|
-- recalculate dimen based on new layout
|
|
self.dimen.w = self.width
|
|
self.dimen.h = self.item_group:getSize().h + self.bordersize*2 + self.padding -- (no padding at top)
|
|
self.selected = { x = self.cur_tab, y = 1 } -- reset the position of the focusmanager
|
|
|
|
-- NOTE: We use a slightly ugly hack to detect a brand new menu vs. a tab switch,
|
|
-- in order to optionally flash on initial menu popup...
|
|
-- NOTE: Also avoid repainting what's underneath us on initial popup.
|
|
-- NOTE: And we also only need to repaint what's behind us when switching to a smaller menu...
|
|
local keep_bg = old_dimen and self.dimen.h >= old_dimen.h
|
|
UIManager:setDirty((self.is_fresh or keep_bg) and self.show_parent or "all", function()
|
|
local refresh_dimen =
|
|
old_dimen and old_dimen:combine(self.dimen)
|
|
or self.dimen
|
|
local refresh_type = "ui"
|
|
if self.is_fresh then
|
|
refresh_type = "flashui"
|
|
-- Drop the region, too, to make it full-screen? May help when starting from a "small" menu.
|
|
--refresh_dimen = nil
|
|
self.is_fresh = false
|
|
end
|
|
return refresh_type, refresh_dimen
|
|
end)
|
|
end
|
|
|
|
function TouchMenu:switchMenuTab(tab_num)
|
|
if self.tab_item_table[tab_num].remember ~= false then
|
|
self.last_index = tab_num
|
|
end
|
|
if self.touch_menu_callback then
|
|
self.touch_menu_callback()
|
|
end
|
|
if self.tab_item_table[tab_num].callback then
|
|
self.tab_item_table[tab_num].callback()
|
|
end
|
|
|
|
-- It's like getting a new menu everytime we switch tab!
|
|
-- Also, switching to the _same_ tab resets the stack and takes us back to
|
|
-- the top of the menu tree
|
|
self.page = 1
|
|
-- clear item table stack
|
|
self.item_table_stack = {}
|
|
self.cur_tab = tab_num
|
|
self.item_table = self.tab_item_table[tab_num]
|
|
self:updateItems()
|
|
end
|
|
|
|
function TouchMenu:backToUpperMenu()
|
|
if #self.item_table_stack ~= 0 then
|
|
self.item_table = table.remove(self.item_table_stack)
|
|
self.page = 1
|
|
self:updateItems()
|
|
else
|
|
self:closeMenu()
|
|
end
|
|
end
|
|
|
|
function TouchMenu:closeMenu()
|
|
self.close_callback()
|
|
end
|
|
|
|
function TouchMenu:onNextPage()
|
|
if self.page < self.page_num then
|
|
self.page = self.page + 1
|
|
elseif self.page == self.page_num then
|
|
self.page = 1
|
|
end
|
|
self:updateItems()
|
|
return true
|
|
end
|
|
|
|
function TouchMenu:onPrevPage()
|
|
if self.page > 1 then
|
|
self.page = self.page - 1
|
|
elseif self.page == 1 then
|
|
self.page = self.page_num
|
|
end
|
|
self:updateItems()
|
|
return true
|
|
end
|
|
|
|
function TouchMenu:onSwipe(arg, ges_ev)
|
|
local direction = BD.flipDirectionIfMirroredUILayout(ges_ev.direction)
|
|
if direction == "west" then
|
|
self:onNextPage()
|
|
elseif direction == "east" then
|
|
self:onPrevPage()
|
|
elseif direction == "north" then
|
|
self:closeMenu()
|
|
end
|
|
end
|
|
|
|
function TouchMenu:onMenuSelect(item)
|
|
if self.touch_menu_callback then
|
|
self.touch_menu_callback()
|
|
end
|
|
if item.tap_input or type(item.tap_input_func) == "function" then
|
|
if not item.keep_menu_open then
|
|
self:closeMenu()
|
|
end
|
|
if item.tap_input then
|
|
self:onInput(item.tap_input)
|
|
else
|
|
self:onInput(item.tap_input_func())
|
|
end
|
|
else
|
|
local sub_item_table = item.sub_item_table
|
|
if item.sub_item_table_func then
|
|
sub_item_table = item.sub_item_table_func()
|
|
end
|
|
if sub_item_table == nil then
|
|
-- keep menu opened if this item is a check option
|
|
local callback, refresh = item.callback, item.checked or item.checked_func
|
|
if item.callback_func then
|
|
callback = item.callback_func()
|
|
end
|
|
if callback then
|
|
-- Provide callback with us, so it can call our
|
|
-- closemenu() or updateItems() when it sees fit
|
|
-- (if not providing checked or checked_fund, caller
|
|
-- must set keep_menu_open=true if that is wished)
|
|
callback(self)
|
|
if refresh then
|
|
self:updateItems()
|
|
elseif not item.keep_menu_open then
|
|
self:closeMenu()
|
|
end
|
|
end
|
|
else
|
|
table.insert(self.item_table_stack, self.item_table)
|
|
self.item_table = sub_item_table
|
|
self.page = 1
|
|
self:updateItems()
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function TouchMenu:onMenuHold(item)
|
|
if self.touch_menu_callback then
|
|
self.touch_menu_callback()
|
|
end
|
|
if item.hold_input or type(item.hold_input_func) == "function" then
|
|
if item.hold_keep_menu_open == false then
|
|
self:closeMenu()
|
|
end
|
|
if item.hold_input then
|
|
self:onInput(item.hold_input)
|
|
else
|
|
self:onInput(item.hold_input_func())
|
|
end
|
|
elseif item.hold_callback or type(item.hold_callback_func) == "function" then
|
|
local callback = item.hold_callback
|
|
if item.hold_callback_func then
|
|
callback = item.hold_callback_func()
|
|
end
|
|
if callback then
|
|
-- With hold, the default is to keep menu open, as we're
|
|
-- most often showing a ConfirmBox that can be cancelled
|
|
-- (provide hold_keep_menu_open=false to override)
|
|
if item.hold_keep_menu_open == false then
|
|
self:closeMenu()
|
|
end
|
|
-- Provide callback with us, so it can call our
|
|
-- closemenu() or updateItems() when it sees fit
|
|
callback(self)
|
|
end
|
|
elseif item.help_text or type(item.help_text_func) == "function" then
|
|
local help_text = item.help_text
|
|
if item.help_text_func then
|
|
help_text = item.help_text_func()
|
|
end
|
|
if help_text then
|
|
UIManager:show(InfoMessage:new{ text = help_text, })
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function TouchMenu:onTapCloseAllMenus(arg, ges_ev)
|
|
if ges_ev.pos:notIntersectWith(self.dimen) then
|
|
self:closeMenu()
|
|
end
|
|
end
|
|
|
|
function TouchMenu:onClose()
|
|
self:closeMenu()
|
|
end
|
|
|
|
function TouchMenu:onBack()
|
|
self:backToUpperMenu()
|
|
end
|
|
|
|
function TouchMenu:onPress()
|
|
self:getFocusItem():handleEvent(Event:new("TapSelect"))
|
|
end
|
|
|
|
return TouchMenu
|