mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
The coordinates could wonky otherwise, as TouchMenuItem is an InputContainer. Mostly harmless in practice as that UC is its main widget.
1356 lines
49 KiB
Lua
1356 lines
49 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 RadioMark = require("ui/widget/radiomark")
|
||
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 Utf8Proc = require("ffi/utf8proc")
|
||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||
local VerticalSpan = require("ui/widget/verticalspan")
|
||
local datetime = require("datetime")
|
||
local getMenuText = require("ui/widget/menu").getMenuText
|
||
local _ = require("gettext")
|
||
local ffiUtil = require("ffi/util")
|
||
local util = require("util")
|
||
local T = ffiUtil.template
|
||
local Input = Device.input
|
||
local Screen = Device.screen
|
||
|
||
local DGENERIC_ICON_SIZE = G_defaults:readSetting("DGENERIC_ICON_SIZE")
|
||
|
||
--[[
|
||
TouchMenuItem widget
|
||
--]]
|
||
local TouchMenuItem = InputContainer:extend{
|
||
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,
|
||
},
|
||
},
|
||
HoldSelect = {
|
||
GestureRange:new{
|
||
ges = "hold",
|
||
range = self.dimen,
|
||
},
|
||
},
|
||
}
|
||
|
||
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
|
||
if self.item.radio then
|
||
checkmark_widget = RadioMark:new{
|
||
checkable = item_checkable,
|
||
checked = item_checked,
|
||
enabled = item_enabled,
|
||
}
|
||
else
|
||
checkmark_widget = CheckMark:new{
|
||
checkable = item_checkable,
|
||
checked = item_checked,
|
||
enabled = item_enabled,
|
||
}
|
||
end
|
||
|
||
local checked_widget = CheckMark:new{ -- for layout, to :getSize()
|
||
checked = true,
|
||
}
|
||
|
||
self.checkmark_tap_width = checked_widget:getSize().w + 2*Size.padding.default
|
||
|
||
-- 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
|
||
local text_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.text_truncated = text_widget:isTruncated()
|
||
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,
|
||
},
|
||
text_widget,
|
||
},
|
||
}
|
||
|
||
self._underline_container = UnderlineContainer:new{
|
||
vertical_align = "center",
|
||
dimen = self.dimen:copy(),
|
||
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 true end -- don't propagate
|
||
|
||
local tap_on_checkmark = false
|
||
if ges and ges.pos and ges.pos.x then
|
||
local tap_x = BD.mirroredUILayout() and self.dimen.w - ges.pos.x - 1
|
||
or ges.pos.x
|
||
if tap_x <= self.checkmark_tap_width then
|
||
tap_on_checkmark = true
|
||
end
|
||
end
|
||
|
||
-- If the menu hasn't actually been drawn yet, don't do anything (as it's confusing, and the coordinates may be wrong).
|
||
if not self.item_frame.dimen then return true end
|
||
|
||
if G_reader_settings:isFalse("flash_ui") then
|
||
self.menu:onMenuSelect(self.item, tap_on_checkmark)
|
||
else
|
||
-- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow
|
||
|
||
-- 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
|
||
|
||
-- Highlight
|
||
--
|
||
self.item_frame.invert = true
|
||
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
||
UIManager:setDirty(nil, "fast", highlight_dimen)
|
||
|
||
UIManager:forceRePaint()
|
||
UIManager:yieldToEPDC()
|
||
|
||
-- Unhighlight
|
||
--
|
||
self.item_frame.invert = false
|
||
-- NOTE: If the menu is going to be closed, we can safely drop that.
|
||
if self.item.keep_menu_open then
|
||
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
||
UIManager:setDirty(nil, "ui", highlight_dimen)
|
||
end
|
||
|
||
-- Callback
|
||
--
|
||
self.menu:onMenuSelect(self.item, tap_on_checkmark)
|
||
|
||
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
|
||
-- Allow help_text to be displayed even if menu item disabled
|
||
if self.item.help_text or type(self.item.help_text_func) == "function" then
|
||
local help_text = self.item.help_text
|
||
if self.item.help_text_func then
|
||
help_text = self.item.help_text_func(self)
|
||
end
|
||
if help_text then
|
||
UIManager:show(InfoMessage:new{ text = help_text, })
|
||
end
|
||
end
|
||
return true -- don't propagate
|
||
end
|
||
|
||
if not self.item_frame.dimen then return true end
|
||
|
||
if G_reader_settings:isFalse("flash_ui") then
|
||
self.menu:onMenuHold(self.item, self.text_truncated)
|
||
else
|
||
-- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow
|
||
|
||
-- 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
|
||
|
||
-- Highlight
|
||
--
|
||
self.item_frame.invert = true
|
||
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
||
UIManager:setDirty(nil, "fast", highlight_dimen)
|
||
|
||
UIManager:forceRePaint()
|
||
UIManager:yieldToEPDC()
|
||
|
||
-- Unhighlight
|
||
--
|
||
self.item_frame.invert = false
|
||
-- NOTE: If the menu is going to be closed, we can safely drop that.
|
||
-- (This field defaults to nil, meaning keep the menu open, hence the negated test)
|
||
if self.item.hold_keep_menu_open ~= false then
|
||
UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
||
UIManager:setDirty(nil, "ui", highlight_dimen)
|
||
end
|
||
|
||
-- Callback
|
||
--
|
||
self.menu:onMenuHold(self.item, self.text_truncated)
|
||
|
||
UIManager:forceRePaint()
|
||
end
|
||
return true
|
||
end
|
||
|
||
--[[
|
||
TouchMenuBar widget
|
||
--]]
|
||
local TouchMenuBar = InputContainer:extend{
|
||
width = Screen:getWidth(),
|
||
icons = nil, -- array, mandatory
|
||
-- 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{ x = 0, y = 0, 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:extend{
|
||
tab_item_table = nil, -- mandatory
|
||
-- for returning in multi-level menus
|
||
item_table_stack = nil,
|
||
parent_id = 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 = { { Input.group.Back } }
|
||
if Device:hasFewKeys() then
|
||
self.key_events.Back = { { "Left" } }
|
||
end
|
||
self.key_events.NextPage = { { Input.group.PgFwd } }
|
||
self.key_events.PrevPage = { { Input.group.PgBack } }
|
||
|
||
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,
|
||
hold_callback = function() self:onFirstPage() end,
|
||
bordersize = 0,
|
||
show_parent = self.show_parent,
|
||
}
|
||
self.page_info_right_chev = Button:new{
|
||
icon = chevron_right,
|
||
callback = function() self:onNextPage() end,
|
||
hold_callback = function() self:onLastPage() 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 = Button:new{
|
||
text = "",
|
||
face = self.fface,
|
||
text_font_bold = false,
|
||
callback = function()
|
||
UIManager:show(InfoMessage:new{
|
||
text = datetime.secondsToDateTime(nil, nil, true),
|
||
})
|
||
end,
|
||
hold_callback = function()
|
||
UIManager:broadcastEvent(Event:new("ShowBatteryStatistics"))
|
||
end,
|
||
bordersize = 0,
|
||
show_parent = self.show_parent,
|
||
}
|
||
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,
|
||
-- but we only need to do that if we actually have a FM or RD below us.
|
||
-- Don't do anything when we're switching between the two, or if we don't actually have a live instance of 'em...
|
||
local FileManager = require("apps/filemanager/filemanager")
|
||
local ReaderUI = require("apps/reader/readerui")
|
||
if (FileManager.instance and not FileManager.instance.tearing_down)
|
||
or (ReaderUI.instance and not ReaderUI.instance.tearing_down) then
|
||
UIManager:setDirty(nil, "flashui")
|
||
end
|
||
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,
|
||
item_visible_index = c,
|
||
}
|
||
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 and i ~= #self.item_table 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 = datetime.secondsToHour(os.time(), G_reader_settings:isTrue("twelve_hour_clock"))
|
||
local powerd = Device:getPowerDevice()
|
||
if Device:hasBattery() then
|
||
local batt_lvl = powerd:getCapacity()
|
||
local batt_symbol = powerd:getBatterySymbol(powerd:isCharged(), powerd:isCharging(), batt_lvl)
|
||
time_info_txt = BD.wrap(time_info_txt) .. " " .. BD.wrap("⌁") .. BD.wrap(batt_symbol) .. BD.wrap(batt_lvl .. "%")
|
||
|
||
if Device:hasAuxBattery() and powerd:isAuxBatteryConnected() then
|
||
local aux_batt_lvl = powerd:getAuxCapacity()
|
||
local aux_batt_symbol = powerd:getBatterySymbol(powerd:isAuxCharged(), powerd:isAuxCharging(), aux_batt_lvl)
|
||
time_info_txt = time_info_txt .. " " .. BD.wrap("+") .. BD.wrap(aux_batt_symbol) .. BD.wrap(aux_batt_lvl .. "%")
|
||
end
|
||
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:moveFocusTo(self.cur_tab, 1, FocusManager.NOT_FOCUS) -- 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.parent_id = nil
|
||
self.cur_tab = tab_num
|
||
self.item_table = self.tab_item_table[tab_num]
|
||
self:updateItems()
|
||
end
|
||
|
||
function TouchMenu:backToUpperMenu(no_close)
|
||
if #self.item_table_stack ~= 0 then
|
||
self.item_table = table.remove(self.item_table_stack)
|
||
-- Allow a menu table to refresh itself when going up (ie. from a setting
|
||
-- submenu that may want to have its parent menu updated).
|
||
if self.item_table.needs_refresh and self.item_table.refresh_func then
|
||
self.item_table = self.item_table.refresh_func()
|
||
end
|
||
self.page = 1
|
||
if self.parent_id then
|
||
self:_recalculatePageLayout() -- we need an accurate self.perpage
|
||
for i = 1, #self.item_table do
|
||
if self.item_table[i].menu_item_id == self.parent_id then
|
||
self.page = math.floor( (i - 1) / self.perpage ) + 1
|
||
break
|
||
end
|
||
end
|
||
self.parent_id = nil
|
||
end
|
||
self:updateItems()
|
||
elseif not no_close then
|
||
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:onFirstPage()
|
||
self.page = 1
|
||
self:updateItems()
|
||
return true
|
||
end
|
||
|
||
function TouchMenu:onLastPage()
|
||
self.page = self.page_num
|
||
self:updateItems()
|
||
return true
|
||
end
|
||
|
||
function TouchMenu:onGotoPage(nb)
|
||
if nb > self.page_num then
|
||
self.page = self.page_num
|
||
elseif nb < 1 then
|
||
self.page = 1
|
||
else
|
||
self.page = nb
|
||
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()
|
||
elseif direction == "south" then
|
||
-- We don't allow the menu to be closed (this is also necessary as
|
||
-- a swipe south will be emitted when done opening the menu with
|
||
-- swipe, as the event handled for that is pan south).
|
||
self:backToUpperMenu(true)
|
||
end
|
||
end
|
||
|
||
function TouchMenu:onMenuSelect(item, tap_on_checkmark)
|
||
if self.touch_menu_callback then
|
||
self.touch_menu_callback()
|
||
end
|
||
if tap_on_checkmark and item.checkmark_callback then
|
||
item.checkmark_callback()
|
||
self:updateItems()
|
||
return true
|
||
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_func, 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.parent_id = item.menu_item_id
|
||
self.item_table = sub_item_table
|
||
self.page = 1
|
||
if self.item_table.open_on_menu_item_id_func then
|
||
self:_recalculatePageLayout() -- we need an accurate self.perpage
|
||
local open_id = self.item_table.open_on_menu_item_id_func()
|
||
for i = 1, #self.item_table do
|
||
if self.item_table[i].menu_item_id == open_id then
|
||
self.page = math.floor( (i - 1) / self.perpage ) + 1
|
||
break
|
||
end
|
||
end
|
||
end
|
||
self:updateItems()
|
||
end
|
||
end
|
||
return true
|
||
end
|
||
|
||
function TouchMenu:onMenuHold(item, text_truncated)
|
||
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(self)
|
||
end
|
||
if help_text then
|
||
UIManager:show(InfoMessage:new{ text = help_text, })
|
||
end
|
||
elseif text_truncated then
|
||
UIManager:show(InfoMessage:new{
|
||
text = getMenuText(item),
|
||
show_icon = false,
|
||
})
|
||
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
|
||
|
||
-- Menu search feature
|
||
function TouchMenu:search(search_for)
|
||
local found_menu_items = {}
|
||
|
||
local MAX_MENU_DEPTH = 10 -- our menu max depth is currently 6
|
||
local function recurse(item_table, path, text, icon, depth)
|
||
if item_table.ignored_by_menu_search then
|
||
return
|
||
end
|
||
depth = depth + 1
|
||
if depth > MAX_MENU_DEPTH then
|
||
return
|
||
end
|
||
for i, v in ipairs(item_table) do
|
||
if type(v) == "table" and not v.ignored_by_menu_search then
|
||
local entry_text = v.text_func and v.text_func() or v.text
|
||
local indent = text and ((" "):rep(math.min(depth-1, 6)) .. "→ ") or "→ " -- all spaces here are Hair Space U+200A
|
||
local walk_text = text and (text .. "\n" .. indent .. entry_text) or (indent .. entry_text)
|
||
local walk_path = path .. "." .. i
|
||
if Utf8Proc.lowercase(entry_text):find(search_for, 1, true) then
|
||
table.insert(found_menu_items, {entry_text, icon, walk_path, walk_text})
|
||
end
|
||
local sub_item_table = v.sub_item_table
|
||
if v.sub_item_table_func then
|
||
sub_item_table = v.sub_item_table_func()
|
||
end
|
||
if sub_item_table and not sub_item_table.ignored_by_menu_search then
|
||
recurse(sub_item_table, walk_path, walk_text, icon, depth)
|
||
end
|
||
end
|
||
end
|
||
end -- recurse
|
||
|
||
-- Initial call of recurse, for each tab
|
||
for i = 1, #self.tab_item_table do
|
||
recurse(self.tab_item_table[i], i, nil, self.tab_item_table[i].icon, 0)
|
||
end
|
||
|
||
return found_menu_items
|
||
end
|
||
|
||
function TouchMenu:openMenu(path, with_animation)
|
||
local parts = {}
|
||
for part in util.gsplit(path, "%.", false) do -- path is ie. "2.3.3.1"
|
||
table.insert(parts, tonumber(part))
|
||
end
|
||
util.arrayReverse(parts) -- so we can just table.remove() and pop them from end
|
||
|
||
local function highlightWidget(widget, unhighlight)
|
||
if not widget then return end
|
||
local highlight_dimen = widget.dimen
|
||
if highlight_dimen.w == 0 then
|
||
highlight_dimen.w = widget.width
|
||
end
|
||
if unhighlight then
|
||
widget.invert = false
|
||
UIManager:widgetInvert(widget, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
||
UIManager:setDirty(nil, "ui", highlight_dimen)
|
||
else
|
||
widget.invert = true
|
||
UIManager:widgetInvert(widget, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w)
|
||
UIManager:setDirty(nil, "fast", highlight_dimen)
|
||
end
|
||
end
|
||
|
||
-- Steps/state among consecutive calls to walkStep()
|
||
local STEPS = {
|
||
START = 0,
|
||
TARGET_TAB_HIGHLIGHT_ICON = 1,
|
||
TARGET_TAB_OPEN = 2,
|
||
TARGET_PAGE_OR_HIGHLIGHT_NEXT_PREV = 3,
|
||
TARGET_PAGE_OR_NAVIGATE_NEXT_PREV = 4,
|
||
MENU_ITEM_HIGHLIGHT = 5, -- intermediate or final menu item
|
||
MENU_ITEM_ENTER = 6, -- intermediate menu item only
|
||
DONE = 7,
|
||
}
|
||
local step = STEPS.START
|
||
local tab_nb
|
||
local item_nb
|
||
local walkStep_scheduled
|
||
local trap_widget
|
||
|
||
local function walkStep()
|
||
walkStep_scheduled = false
|
||
-- Default delay if not overriden (-1 means no scheduleIn() so no refresh, 0 means nextTick)
|
||
local next_delay = with_animation and 1 or -1
|
||
if step == STEPS.START then
|
||
-- Ensure some initial delay so search dialog and result list can be closed and refreshed
|
||
next_delay = with_animation and 1 or 0
|
||
step = STEPS.TARGET_TAB_HIGHLIGHT_ICON
|
||
elseif step == STEPS.TARGET_TAB_HIGHLIGHT_ICON then
|
||
tab_nb = table.remove(parts)
|
||
if with_animation then
|
||
highlightWidget(self.bar.icon_widgets[tab_nb].image)
|
||
end
|
||
step = STEPS.TARGET_TAB_OPEN
|
||
elseif step == STEPS.TARGET_TAB_OPEN then
|
||
-- The tab icon wouldn't be unhighligted by any other action.
|
||
-- Animation may have been cancelled, so unhighlight if it was.
|
||
if self.bar.icon_widgets[tab_nb].image.invert then
|
||
highlightWidget(self.bar.icon_widgets[tab_nb].image, true)
|
||
end
|
||
self:switchMenuTab(tab_nb)
|
||
self.bar:switchToTab(tab_nb)
|
||
item_nb = table.remove(parts)
|
||
step = STEPS.TARGET_PAGE_OR_HIGHLIGHT_NEXT_PREV
|
||
elseif step == STEPS.TARGET_PAGE_OR_HIGHLIGHT_NEXT_PREV or
|
||
step == STEPS.TARGET_PAGE_OR_NAVIGATE_NEXT_PREV then
|
||
local target_page = math.floor((item_nb - 1) / self.perpage) + 1
|
||
local pages_diff = target_page - self.page
|
||
if pages_diff == 0 then -- we are on the right menu page
|
||
step = STEPS.MENU_ITEM_HIGHLIGHT
|
||
next_delay = -1 -- we paused before, no need for more pause
|
||
if not with_animation and #parts == 0 then
|
||
-- Except if no animation and we are on the final menu that
|
||
-- we want to highlight: this final highlight needs to be
|
||
-- delayed for it to be drawn after the final menu page is.
|
||
next_delay = 1
|
||
end
|
||
elseif step == STEPS.TARGET_PAGE_OR_HIGHLIGHT_NEXT_PREV then
|
||
-- No need to highlight chevrons if no animation
|
||
if with_animation then
|
||
if pages_diff > 0 then
|
||
highlightWidget(self.page_info_right_chev)
|
||
else
|
||
highlightWidget(self.page_info_left_chev)
|
||
end
|
||
if pages_diff > 1 or pages_diff < -1 then
|
||
-- Change pages quicker if more than one needed, but slow on the last one
|
||
next_delay = 0.5
|
||
end
|
||
end
|
||
step = STEPS.TARGET_PAGE_OR_NAVIGATE_NEXT_PREV
|
||
else -- STEPS.TARGET_PAGE_OR_NAVIGATE_NEXT_PREV
|
||
if pages_diff > 0 then
|
||
self:onNextPage()
|
||
else
|
||
self:onPrevPage()
|
||
end
|
||
step = STEPS.TARGET_PAGE_OR_HIGHLIGHT_NEXT_PREV
|
||
if with_animation and (pages_diff > 1 or pages_diff < -1) then
|
||
-- Change pages quicker if more than one needed, but slow on the last one
|
||
next_delay = 0.5
|
||
end
|
||
end
|
||
elseif step == STEPS.MENU_ITEM_HIGHLIGHT then
|
||
if with_animation or #parts == 0 then
|
||
-- Even if no animation, highlight the final item (and don't unhighlight it)
|
||
local item_visible_index = (item_nb - 1) % self.perpage + 1
|
||
local item_widget
|
||
for i, w in ipairs(self.item_group) do
|
||
if w.item_visible_index == item_visible_index then
|
||
item_widget = w
|
||
break
|
||
end
|
||
end
|
||
if item_widget then
|
||
highlightWidget(item_widget)
|
||
end
|
||
end
|
||
if #parts == 0 then
|
||
step = STEPS.DONE
|
||
else
|
||
step = STEPS.MENU_ITEM_ENTER
|
||
end
|
||
elseif step == STEPS.MENU_ITEM_ENTER then
|
||
self:onMenuSelect(self.item_table[item_nb])
|
||
item_nb = table.remove(parts)
|
||
step = STEPS.TARGET_PAGE_OR_HIGHLIGHT_NEXT_PREV
|
||
else -- STEPS.DONE
|
||
if trap_widget then
|
||
UIManager:close(trap_widget)
|
||
trap_widget = nil
|
||
end
|
||
return
|
||
end
|
||
if next_delay >= 0 then
|
||
walkStep_scheduled = true
|
||
UIManager:scheduleIn(next_delay, walkStep)
|
||
else
|
||
walkStep()
|
||
end
|
||
end
|
||
|
||
-- We use an invisible TrapWidget when no animation, so we can
|
||
-- cancel the delayed final highlight
|
||
local TrapWidget = require("ui/widget/trapwidget")
|
||
trap_widget = TrapWidget:new{
|
||
text = with_animation and _("Walking you there…") or nil,
|
||
dismiss_callback = function()
|
||
trap_widget = nil
|
||
if walkStep_scheduled then
|
||
UIManager:unschedule(walkStep)
|
||
if with_animation then
|
||
-- continue walking as if no animation, so we immediately
|
||
-- reach the requested menu item. We need a new invisible
|
||
-- TrapWidget for the reason explained above in case a
|
||
-- second tap happens.
|
||
with_animation = false
|
||
trap_widget = TrapWidget:new{
|
||
text = nil,
|
||
dismiss_callback = function()
|
||
trap_widget = nil
|
||
if walkStep_scheduled then
|
||
UIManager:unschedule(walkStep)
|
||
end
|
||
end,
|
||
resend_event = true,
|
||
}
|
||
UIManager:show(trap_widget)
|
||
walkStep()
|
||
end
|
||
end
|
||
end,
|
||
resend_event = not with_animation, -- if not animation, don't eat the tap
|
||
}
|
||
UIManager:show(trap_widget) -- catch taps during animaton
|
||
|
||
-- Call it: it will reschedule itself if animation; if not, it will
|
||
-- just execute itself without pause until done.
|
||
-- If tap while animating, it will switch to the non-animation
|
||
-- behaviour, to reach the requested menu item immediately.
|
||
walkStep()
|
||
end
|
||
|
||
function TouchMenu:onShowMenuSearch()
|
||
local InputDialog = require("ui/widget/inputdialog")
|
||
local ConfirmBox = require("ui/widget/confirmbox")
|
||
local Menu = require("ui/widget/menu")
|
||
|
||
local function show_search_results(search_string)
|
||
local found_menu_items = self:search(search_string)
|
||
|
||
local function get_current_search_results()
|
||
local function open_menu(i, animate)
|
||
UIManager:close(self.results_menu_container)
|
||
UIManager:setDirty(nil, "ui")
|
||
self:openMenu(found_menu_items[i][3], animate)
|
||
end
|
||
local function item_callback(i)
|
||
local confirm_box
|
||
confirm_box = ConfirmBox:new{
|
||
text = found_menu_items[i][4],
|
||
icon = found_menu_items[i][2],
|
||
ok_text = _("Open"),
|
||
ok_callback = function()
|
||
UIManager:close(confirm_box)
|
||
open_menu(i)
|
||
end,
|
||
other_buttons = {{
|
||
{
|
||
text = _("Walk me there"),
|
||
callback = function()
|
||
UIManager:close(confirm_box)
|
||
open_menu(i, true)
|
||
end,
|
||
},
|
||
}},
|
||
|
||
}
|
||
UIManager:show(confirm_box)
|
||
end
|
||
|
||
local result_items = {}
|
||
for i = 1, #found_menu_items do
|
||
table.insert(result_items,
|
||
{
|
||
text = found_menu_items[i][1],
|
||
callback = function() item_callback(i) end,
|
||
hold_callback = function() open_menu(i) end,
|
||
}
|
||
)
|
||
end
|
||
return result_items
|
||
end -- get_current_search_results()
|
||
|
||
if #found_menu_items > 0 then
|
||
local results_menu = Menu:new{
|
||
title = _("Search results"),
|
||
item_table = get_current_search_results(),
|
||
width = math.floor(Screen:getWidth() * 0.9),
|
||
height = math.floor(Screen:getHeight() * 0.9),
|
||
single_line = true,
|
||
items_per_page = 10,
|
||
items_font_size = Menu.getItemFontSize(10),
|
||
onMenuSelect = function(item, pos)
|
||
if pos.callback then pos.callback() end
|
||
end,
|
||
onMenuHold = function(item, pos)
|
||
if pos.hold_callback then pos.hold_callback() end
|
||
end,
|
||
close_callback = function()
|
||
UIManager:close(self.results_menu_container)
|
||
end
|
||
}
|
||
|
||
-- build container
|
||
self.results_menu_container = CenterContainer:new{
|
||
dimen = Screen:getSize(),
|
||
results_menu,
|
||
}
|
||
|
||
results_menu.show_parent = self.results_menu_container
|
||
|
||
UIManager:show(self.results_menu_container)
|
||
|
||
else
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("No menus containing '%1' found."), search_string),
|
||
})
|
||
end
|
||
end -- show_search_results()
|
||
|
||
local search_dialog
|
||
search_dialog = InputDialog:new{
|
||
title = _("Search menu entry"),
|
||
description = _("Search for a menu entry containing the following text (case insensitive)."),
|
||
input = G_reader_settings:readSetting("menu_search_string", _("Help")),
|
||
buttons = {
|
||
{
|
||
{
|
||
text = _("Cancel"),
|
||
id = "close",
|
||
callback = function()
|
||
UIManager:close(search_dialog)
|
||
end,
|
||
},
|
||
{
|
||
text = _("Search"),
|
||
is_enter_default = true,
|
||
callback = function()
|
||
local search_for = search_dialog:getInputText()
|
||
search_for = Utf8Proc.lowercase(search_for)
|
||
G_reader_settings:saveSetting("menu_search_string", search_for)
|
||
UIManager:close(search_dialog)
|
||
show_search_results(search_for)
|
||
end,
|
||
},
|
||
}
|
||
},
|
||
}
|
||
|
||
UIManager:show(search_dialog)
|
||
search_dialog:onShowKeyboard()
|
||
end
|
||
|
||
return TouchMenu
|