mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
1351 lines
48 KiB
Lua
1351 lines
48 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,
|
||
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")
|
||
local reader_ui = ReaderUI:_getRunningInstance()
|
||
if (FileManager.instance and not FileManager.instance.tearing_down) or (reader_ui and not reader_ui.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)
|
||
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
|