2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/frontend/ui/widget/menu.lua
poire-z d6d49a64a7 [RTL UI] use auto or LTR text direction in some specific cases
Allow TextBoxWidget new text direction/lang parameters to be
set on upper widgets, and propagate them all the way to it
(ScrollTextWidget, InputText, InputDialog, TextViewer).

Use specific non-default ones in some specific cases:
- Force LTR text direction when showing HTML and CSS, and
  configuration files (in some plugins).
- Use Wikipedia server language and text direction when
  showing an article.
- Use auto with Dictionary results, as we don't know the
  dictionary language, and they may contain mixed content.
- Force LTR when showing some paths (still needs more of them)

TextEditor plugin: add 2 new options "Auto paragraph direction"
and "Force paragraph direction LTR".

Footnotes popup: grab HTML direction, and forward it
to MuPDF for proper display.
2019-12-08 15:10:51 +01:00

1276 lines
42 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[--
Widget that displays a shortcut icon for menu item.
--]]
local Blitbuffer = require("ffi/blitbuffer")
local BottomContainer = require("ui/widget/container/bottomcontainer")
local Button = require("ui/widget/button")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
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 InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local LeftContainer = require("ui/widget/container/leftcontainer")
local Math = require("optmath")
local OverlapGroup = require("ui/widget/overlapgroup")
local RightContainer = require("ui/widget/container/rightcontainer")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
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 WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local util = require("ffi/util")
local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen
local getMenuText = require("util").getMenuText
local ItemShortCutIcon = WidgetContainer:new{
dimen = Geom:new{ w = Screen:scaleBySize(22), h = Screen:scaleBySize(22) },
key = nil,
bordersize = Size.border.default,
radius = 0,
style = "square",
}
function ItemShortCutIcon:init()
if not self.key then
return
end
local radius = 0
local background = Blitbuffer.COLOR_WHITE
if self.style == "rounded_corner" then
radius = math.floor(self.width/2)
elseif self.style == "grey_square" then
background = Blitbuffer.COLOR_LIGHT_GRAY
end
--- @todo Calculate font size by icon size 01.05 2012 (houqp).
local sc_face
if self.key:len() > 1 then
sc_face = Font:getFace("ffont", 14)
else
sc_face = Font:getFace("scfont", 22)
end
self[1] = FrameContainer:new{
padding = 0,
bordersize = self.bordersize,
radius = radius,
background = background,
dimen = self.dimen,
CenterContainer:new{
dimen = self.dimen,
TextWidget:new{
text = self.key,
face = sc_face,
},
},
}
end
--[[
NOTICE:
@menu entry must be provided in order to close the menu
--]]
local MenuCloseButton = InputContainer:new{
overlap_align = "right",
padding_right = 0,
menu = nil,
dimen = Geom:new{},
}
function MenuCloseButton:init()
self[1] = TextWidget:new{
text = "×",
face = Font:getFace("cfont", 30), -- this font size align nicely with title
}
local text_size = self[1]:getSize()
-- The text box height is greater than its width, and we want this × to
-- be diagonally aligned with our top right border
local text_width_pad = (text_size.h - text_size.w) / 2
-- We also add the provided padding_right
self.dimen = Geom:new{
w = text_size.w + text_width_pad + self.padding_right,
h = text_size.h,
}
self.ges_events.Close = {
GestureRange:new{
ges = "tap",
range = self.dimen,
},
doc = "Close menu",
}
end
function MenuCloseButton:onClose()
self.menu:onClose()
return true
end
--[[
Widget that displays an item for menu
--]]
local MenuItem = InputContainer:new{
text = nil,
show_parent = nil,
detail = nil,
font = "cfont",
font_size = 24,
infont = "infont",
infont_size = 18,
dimen = nil,
shortcut = nil,
shortcut_style = "square",
_underline_container = nil,
linesize = Size.line.medium,
single_line = false,
-- Align text & mandatory baselines (only when single_line=true)
align_baselines = false,
}
function MenuItem:init()
self.content_width = self.dimen.w - 2 * Size.padding.fullscreen
local shortcut_icon_dimen = Geom:new()
if self.shortcut then
shortcut_icon_dimen.w = math.floor(self.dimen.h*4/5)
shortcut_icon_dimen.h = shortcut_icon_dimen.w
self.content_width = self.content_width - shortcut_icon_dimen.w - Size.span.horizontal_default
end
self.detail = self.text
-- we need this table per-instance, so we declare it here
if Device:isTouchDevice() then
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",
},
}
end
local max_item_height = self.dimen.h - 2 * self.linesize
-- We want to show at least one line, so cap the provided font sizes
local max_font_size = TextBoxWidget:getFontSizeToFitHeight(max_item_height, 1)
if self.font_size > max_font_size then
self.font_size = max_font_size
end
if self.infont_size > max_font_size then
self.infont_size = max_font_size
end
local multilines_show_more_text = G_reader_settings:isTrue("items_multilines_show_more_text")
if not self.single_line and not multilines_show_more_text then
-- For non single line menus (File browser, Bookmarks), if the
-- user provided font size is large and would not allow showing
-- more than one line in our item height, just switch to single
-- line mode. This allows, when truncating, to take the full
-- width and cut inside a word to add the ellipsis - while in
-- multilines modes, with TextBoxWidget, words are wrapped to
-- follow line breaking rules, and the ellipsis might be placed
-- way earlier than the full width.
local min_font_size_2_lines = TextBoxWidget:getFontSizeToFitHeight(max_item_height, 2)
if self.font_size > min_font_size_2_lines then
self.single_line = true
end
end
-- State button and indentation for tree expand/collapse (for TOC)
local state_button_width = self.state_size.w or 0
local state_button = self.state or HorizontalSpan:new{
width = state_button_width,
}
local state_indent = self.state and self.state.indent or ""
local state_container = LeftContainer:new{
dimen = Geom:new{w = self.content_width/2, h = self.dimen.h},
HorizontalGroup:new{
TextWidget:new{
text = state_indent,
face = Font:getFace(self.font, self.font_size),
},
state_button,
}
}
-- Font for main text (may have its size decreased to make text fit)
self.face = Font:getFace(self.font, self.font_size)
-- Font for "mandatory" on the right
self.info_face = Font:getFace(self.infont, self.infont_size)
-- "mandatory" is the text on the right: file size, page number...
-- Padding before mandatory
local text_mandatory_padding = 0
local text_ellipsis_mandatory_padding = 0
if self.mandatory then
text_mandatory_padding = Size.span.horizontal_default
-- Smaller padding when ellipsis for better visual feeling
text_ellipsis_mandatory_padding = Size.span.horizontal_small
end
local mandatory = self.mandatory and ""..self.mandatory or ""
local mandatory_widget = TextWidget:new{
text = mandatory,
face = self.info_face,
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local mandatory_w = mandatory_widget:getWidth()
local available_width = self.content_width - state_button_width - text_mandatory_padding - mandatory_w
local item_name
-- Whether we show text on a single or multiple lines, we don't want it shortened
-- because of some \n that would push the following text on another line that would
-- overflow and not be displayed, or show a tofu char when displayed by TextWidget:
-- get rid of any \n (which could be found in highlighted text in bookmarks).
local text = self.text:gsub("\n", " ")
if self.single_line then -- items only in single line
-- No font size change: text will be truncated if it overflows
item_name = TextWidget:new{
text = text,
face = self.face,
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local w = item_name:getWidth()
if w > available_width then
-- We give it a little more room if truncated for better visual
-- feeling (which might make it no more truncated, but well...)
local text_max_width_if_ellipsis = available_width + text_mandatory_padding - text_ellipsis_mandatory_padding
item_name:setMaxWidth(text_max_width_if_ellipsis)
end
if self.align_baselines then -- Align baselines of text and mandatory
local name_baseline = item_name:getBaseline()
local mandatory_baseline = mandatory_widget:getBaseline()
local baselines_diff = Math.round(name_baseline - mandatory_baseline)
if baselines_diff > 0 then
mandatory_widget = VerticalGroup:new{
VerticalSpan:new{width = baselines_diff},
mandatory_widget,
}
elseif baselines_diff < 0 then
item_name = VerticalGroup:new{
VerticalSpan:new{width = -baselines_diff},
item_name,
}
end
end
elseif multilines_show_more_text then
-- Multi-lines, with font size decrease if needed to show more of the text.
-- It would be costly/slow with use_xtext if we were to try all
-- font sizes from self.font_size to min_font_size (12).
-- So, we try to optimize the search of the best font size.
logger.dbg("multilines_show_more_text menu item font sizing start")
local function make_item_name(font_size)
if item_name then
item_name:free()
end
logger.dbg("multilines_show_more_text trying font size", font_size)
item_name = TextBoxWidget:new {
text = text,
face = Font:getFace(self.font, font_size),
width = available_width,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
-- return true if we fit
return item_name:getSize().h <= max_item_height
end
local min_font_size = 12
-- First, try with specified font size: short text might fit
if not make_item_name(self.font_size) then
-- It doesn't, try with min font size: very long text might not fit
if not make_item_name(min_font_size) then
-- Does not fit with min font size: keep widget with min_font_size, but
-- impose a max height to show only the first lines up to where it fits
item_name:free()
item_name.height = max_item_height
item_name.height_adjust = true
item_name.height_overflow_show_ellipsis = true
item_name:init()
else
-- Text fits with min font size: try to find some larger
-- font size in between that make text fit, with some
-- binary search to limit the number of checks.
local bad_font_size = self.font_size
local good_font_size = min_font_size
local item_name_is_good = true
while true do
local test_font_size = math.floor((good_font_size + bad_font_size) / 2)
if test_font_size == good_font_size then -- +1 would be bad_font_size
if not item_name_is_good then
make_item_name(good_font_size)
end
break
end
if make_item_name(test_font_size) then
good_font_size = test_font_size
item_name_is_good = true
else
bad_font_size = test_font_size
item_name_is_good = false
end
end
end
end
else
-- Multi-lines, with fixed user provided font size
item_name = TextBoxWidget:new {
text = text,
face = self.face,
width = available_width,
height = max_item_height,
height_adjust = true,
height_overflow_show_ellipsis = true,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
end
local text_container = LeftContainer:new{
dimen = Geom:new{w = self.content_width, h = self.dimen.h},
HorizontalGroup:new{
HorizontalSpan:new{
width = self.state_size.w,
},
item_name,
}
}
local mandatory_container = RightContainer:new{
dimen = Geom:new{w = self.content_width, h = self.dimen.h},
mandatory_widget,
}
self._underline_container = UnderlineContainer:new{
color = self.line_color,
linesize = self.linesize,
vertical_align = "center",
padding = 0,
dimen = Geom:new{
w = self.content_width,
h = self.dimen.h
},
HorizontalGroup:new{
align = "center",
OverlapGroup:new{
dimen = Geom:new{w = self.content_width, h = self.dimen.h},
state_container,
text_container,
mandatory_container,
},
}
}
local hgroup = HorizontalGroup:new{
align = "center",
HorizontalSpan:new{ width = Size.padding.fullscreen },
}
if self.shortcut then
table.insert(hgroup, ItemShortCutIcon:new{
dimen = shortcut_icon_dimen,
key = self.shortcut,
style = self.shortcut_style,
})
table.insert(hgroup, HorizontalSpan:new{ width = Size.span.horizontal_default })
end
table.insert(hgroup, self._underline_container)
self[1] = FrameContainer:new{
bordersize = 0,
padding = 0,
hgroup,
}
end
function MenuItem:onFocus(initial_focus)
if Device:isTouchDevice() then
-- Devices which are Keys capable will get this onFocus called by
-- updateItems(), which will toggle the underline color of first item.
-- If the device is also Touch capable, let's not show the initial
-- underline for a prettier display (it will be shown only when keys
-- are used).
if not initial_focus or self.menu.did_focus_with_keys then
self._underline_container.color = Blitbuffer.COLOR_BLACK
self.menu.did_focus_with_keys = true
end
else
self._underline_container.color = Blitbuffer.COLOR_BLACK
end
return true
end
function MenuItem:onUnfocus()
self._underline_container.color = self.line_color
return true
end
function MenuItem:onShowItemDetail()
UIManager:show(InfoMessage:new{ text = self.detail, })
return true
end
function MenuItem:getGesPosition(ges)
local dimen = self[1].dimen
return {
x = (ges.pos.x - dimen.x)/dimen.w,
y = (ges.pos.y - dimen.y)/dimen.h,
}
end
function MenuItem:onTapSelect(arg, ges)
local pos = self:getGesPosition(ges)
if G_reader_settings:isFalse("flash_ui") then
logger.dbg("creating coroutine for menu select")
local co = coroutine.create(function()
self.menu:onMenuSelect(self.table, pos)
end)
coroutine.resume(co)
else
self[1].invert = true
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen
end)
UIManager:tickAfterNext(function()
logger.dbg("creating coroutine for menu select")
local co = coroutine.create(function()
self.menu:onMenuSelect(self.table, pos)
end)
coroutine.resume(co)
self[1].invert = false
--UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
end)
end
return true
end
function MenuItem:onHoldSelect(arg, ges)
local pos = self:getGesPosition(ges)
if G_reader_settings:isFalse("flash_ui") then
self.menu:onMenuHold(self.table, pos)
else
self[1].invert = true
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen
end)
UIManager:tickAfterNext(function()
self.menu:onMenuHold(self.table, pos)
self[1].invert = false
--UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
end)
end
return true
end
--[[
Widget that displays menu
--]]
local Menu = FocusManager:new{
show_parent = nil,
-- face for displaying item contents
cface = Font:getFace("cfont"),
-- face for menu title
tface = Font:getFace("tfont"),
-- face for paging info display
fface = Font:getFace("ffont"),
-- font for item shortcut
sface = Font:getFace("scfont"),
title = "No Title",
-- default width and height
width = nil,
-- height will be calculated according to item number if not given
height = nil,
header_padding = Size.padding.large,
dimen = Geom:new{},
item_table = {},
item_shortcuts = {
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L", "Del",
"Z", "X", "C", "V", "B", "N", "M", ".", "Sym",
},
item_table_stack = nil,
is_enable_shortcut = true,
item_dimen = nil,
page = 1,
item_group = nil,
page_info = nil,
page_return = nil,
paths = {}, -- table to trace navigation path
-- set this to true to not paint as popup menu
is_borderless = false,
-- if you want to embed the menu widget into another widget, set
-- this to false
is_popout = true,
-- set this to true to add close button
has_close_button = true,
-- close_callback is a function, which is executed when menu is closed
-- it is usually set by the widget which creates the menu
close_callback = nil,
linesize = Size.line.medium,
perpage = G_reader_settings:readSetting("items_per_page") or 14,
line_color = Blitbuffer.COLOR_DARK_GRAY,
}
function Menu:_recalculateDimen()
self.perpage = self.perpage_custom or G_reader_settings:readSetting("items_per_page") or 14
self.span_width = 0
self.dimen.w = self.width
self.dimen.h = self.height or Screen:getHeight()
if self.dimen.h > Screen:getHeight() or self.dimen.h == nil then
self.dimen.h = Screen:getHeight()
end
self.item_dimen = Geom:new{
w = self.dimen.w,
h = Screen:scaleBySize(46),
}
local height_dim
local bottom_height = 0
local top_height = 0
if self.page_return_arrow and self.page_info_text then
bottom_height = math.max(self.page_return_arrow:getSize().h, self.page_info_text:getSize().h)
+ 2 * Size.padding.button
end
if self.menu_title and not self.no_title then
top_height = self.menu_title_group:getSize().h + 2 * Size.padding.small
end
height_dim = self.dimen.h - bottom_height - top_height
self.item_dimen.h = math.floor(height_dim / self.perpage)
self.span_width = math.floor((height_dim - (self.perpage * (self.item_dimen.h ))) / 2 -1 )
self.page_num = math.ceil(#self.item_table / self.perpage)
-- fix current page if out of range
if self.page_num > 0 and self.page > self.page_num then self.page = self.page_num end
end
function Menu:init()
self.show_parent = self.show_parent or self
self.item_table_stack = {}
self.dimen.w = self.width
self.dimen.h = self.height or Screen:getHeight()
if self.dimen.h > Screen:getHeight() or self.dimen.h == nil then
self.dimen.h = Screen:getHeight()
end
self.page = 1
-----------------------------------
-- start to set up widget layout --
-----------------------------------
self.menu_title = TextWidget:new{
overlap_align = "center",
text = self.title,
face = self.tface,
}
local menu_title_container = CenterContainer:new{
dimen = Geom:new{
w = self.dimen.w,
h = self.menu_title:getSize().h,
},
self.menu_title,
}
local path_text_container
if self.show_path then
self.path_text = TextWidget:new{
face = Font:getFace("xx_smallinfofont"),
text = self.path,
para_direction_rtl = false, -- force LTR
max_width = self.dimen.w - 2*Size.padding.small,
truncate_left = true,
}
path_text_container = CenterContainer:new{
dimen = Geom:new{
w = self.dimen.w,
h = self.path_text:getSize().h,
},
self.path_text,
}
self.menu_title_group = VerticalGroup:new{
align = "center",
menu_title_container,
path_text_container,
}
else
self.menu_title_group = VerticalGroup:new{
align = "center",
menu_title_container
}
end
-- group for title bar
self.title_bar = OverlapGroup:new{
dimen = {w = self.dimen.w, h = self.menu_title_group:getSize().h},
self.menu_title_group,
}
-- group for items
self.item_group = VerticalGroup:new{}
-- group for page info
self.page_info_left_chev = Button:new{
icon = "resources/icons/appbar.chevron.left.png",
callback = function() self:onPrevPage() end,
bordersize = 0,
show_parent = self.show_parent,
}
self.page_info_right_chev = Button:new{
icon = "resources/icons/appbar.chevron.right.png",
callback = function() self:onNextPage() end,
bordersize = 0,
show_parent = self.show_parent,
}
self.page_info_first_chev = Button:new{
icon = "resources/icons/appbar.chevron.first.png",
callback = function() self:onFirstPage() end,
bordersize = 0,
show_parent = self.show_parent,
}
self.page_info_last_chev = Button:new{
icon = "resources/icons/appbar.chevron.last.png",
callback = function() self:onLastPage() end,
bordersize = 0,
show_parent = self.show_parent,
}
self.page_info_spacer = HorizontalSpan:new{
width = Screen:scaleBySize(32),
}
self.page_info_left_chev:hide()
self.page_info_right_chev:hide()
self.page_info_first_chev:hide()
self.page_info_last_chev:hide()
local title_goto, type_goto, hint_func
local buttons = {
{
{
text = _("Cancel"),
callback = function()
self.page_info_text:closeInputDialog()
end,
},
{
text = _("Go to page"),
is_enter_default = true,
callback = function()
local page = tonumber(self.page_info_text.input_dialog:getInputText())
if page and page >= 1 and page <= self.page_num then
self:onGotoPage(page)
end
self.page_info_text:closeInputDialog()
end,
},
},
}
if self.goto_letter then
title_goto = _("Enter page number or letter")
type_goto = "string"
hint_func = function()
return string.format("(1 - %s) or (a - z)", self.page_num)
end
table.insert(buttons[1], {
text = _("Go to letter"),
is_enter_default = true,
callback = function()
for k, v in ipairs(self.item_table) do
--- @todo Support utf8 lowercase.
local filename = util.basename(v.path):lower()
local search_string = self.page_info_text.input_dialog:getInputText():lower()
local i, _ = filename:find(search_string)
if i == 1 and not v.is_go_up then
self:onGotoPage(math.ceil(k / self.perpage))
break
end
end
self.page_info_text:closeInputDialog()
end,
})
else
title_goto = _("Enter page number")
type_goto = "number"
hint_func = function()
return string.format("(1 - %s)", self.page_num)
end
end
self.page_info_text = Button:new{
text = "",
hold_input = {
title = title_goto ,
type = type_goto,
hint_func = hint_func,
buttons = buttons,
},
bordersize = 0,
text_font_face = "cfont",
text_font_size = 20,
text_font_bold = false,
}
self.page_info = HorizontalGroup:new{
self.page_info_first_chev,
self.page_info_spacer,
self.page_info_left_chev,
self.page_info_text,
self.page_info_right_chev,
self.page_info_spacer,
self.page_info_last_chev,
}
-- return button
self.page_return_arrow = Button:new{
icon = "resources/icons/appbar.arrow.left.up.png",
callback = function()
if self.onReturn then self:onReturn() end
end,
bordersize = 0,
show_parent = self.show_parent,
readonly = self.return_arrow_propagation,
}
self.page_return_arrow:hide()
self.return_button = HorizontalGroup:new{
HorizontalSpan:new{
width = Size.span.horizontal_small,
},
self.page_return_arrow,
}
local header = VerticalGroup:new{
VerticalSpan:new{width = self.header_padding},
self.title_bar,
}
local body = self.item_group
local footer = BottomContainer:new{
dimen = self.dimen:copy(),
self.page_info,
}
local page_return = BottomContainer:new{
dimen = self.dimen:copy(),
WidgetContainer:new{
dimen = Geom:new{
w = Screen:getWidth(),
h = self.page_return_arrow:getSize().h,
},
self.return_button,
}
}
self:_recalculateDimen()
self.vertical_span = HorizontalGroup:new{
VerticalSpan:new{ width = self.span_width }
}
if self.no_title then
self.content_group = VerticalGroup:new{
align = "left",
self.vertical_span,
body,
}
else
self.content_group = VerticalGroup:new{
align = "left",
header,
self.vertical_span,
body,
}
end
local content = OverlapGroup:new{
dimen = self.dimen:copy(),
self.content_group,
page_return,
footer,
}
self[1] = FrameContainer:new{
background = Blitbuffer.COLOR_WHITE,
bordersize = self.is_borderless and 0 or 2,
padding = 0,
margin = 0,
radius = self.is_popout and math.floor(self.dimen.w/20) or 0,
content
}
------------------------------------------
-- start to set up input event callback --
------------------------------------------
if Device:isTouchDevice() then
if self.has_close_button then
table.insert(self.title_bar, MenuCloseButton:new{
menu = self,
padding_right = self.header_padding,
})
end
-- watch for outer region if it's a self contained widget
if self.is_popout then
self.ges_events.TapCloseAllMenus = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
}
}
}
end
-- delegate swipe gesture to GestureManager in filemanager
if self.is_file_manager ~= true then
self.ges_events.Swipe = {
GestureRange:new{
ges = "swipe",
range = self.dimen,
}
}
end
self.ges_events.Close = self.on_close_ges
end
if not Device:hasKeyboard() then
-- remove menu item shortcut for K4
self.is_enable_shortcut = false
end
if Device:hasKeys() then
-- set up keyboard events
self.key_events.Close = { {"Back"}, doc = "close menu" }
self.key_events.NextPage = {
{Input.group.PgFwd}, doc = "goto next page of the menu"
}
self.key_events.PrevPage = {
{Input.group.PgBack}, doc = "goto previous page of the menu"
}
end
if Device:hasDPad() then
-- we won't catch presses to "Right", leave that to MenuItem.
self.key_events.FocusRight = nil
-- shortcut icon is not needed for touch device
if self.is_enable_shortcut then
self.key_events.SelectByShortCut = { {self.item_shortcuts} }
end
self.key_events.Select = {
{"Press"}, doc = "select current menu item"
}
self.key_events.Right = {
{"Right"}, doc = "hold menu item"
}
end
if #self.item_table > 0 then
-- if the table is not yet initialized, this call
-- must be done manually:
self.page = math.ceil((self.item_table.current or 1) / self.perpage)
end
if self.path_items then
self:refreshPath()
else
self:updateItems()
end
end
function Menu:onCloseWidget()
--- @fixme
-- we cannot refresh regionally using the dimen field
-- because some menus without menu title use VerticalGroup to include
-- a text widget which is not calculated into the dimen.
-- For example, it's a dirty hack to use two menus(one this menu and one
-- touch menu) in the filemanager in order to capture tap gesture to popup
-- the filemanager menu.
-- NOTE: For the same reason, don't make it flash,
-- because that'll trigger when we close the FM and open a book...
UIManager:setDirty(nil, "partial")
end
function Menu:updatePageInfo(select_number)
if self.item_group[1] then
if Device:hasDPad() then
-- reset focus manager accordingly
self.selected = { x = 1, y = select_number }
end
-- update page information
if self.page_num > 1 then
self.page_info_text:setText(util.template(_("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_first_chev:showHide(self.page_num > 2)
self.page_info_last_chev:showHide(self.page_num > 2)
self.page_return_arrow:showHide(self.onReturn ~= nil)
self.page_info_left_chev:enableDisable(self.page > 1)
self.page_info_right_chev:enableDisable(self.page < self.page_num)
self.page_info_first_chev:enableDisable(self.page > 1)
self.page_info_last_chev:enableDisable(self.page < self.page_num)
self.page_return_arrow:enableDisable(#self.paths > 0)
else
self.page_info_text:setText(_("No choices available"))
end
end
function Menu:updateItems(select_number)
local old_dimen = self.dimen and self.dimen:copy()
-- self.layout must be updated for focusmanager
self.layout = {}
self.item_group:clear()
self.page_info:resetLayout()
self.return_button:resetLayout()
self.vertical_span:clear()
self.content_group:resetLayout()
self:_recalculateDimen()
-- default to select the first item
if not select_number then
select_number = 1
end
--font size between 12 and 18 for better matching
local infont_size = math.floor(18 - (self.perpage - 6) / 3)
--default font size between 14 and 24 for better matching
local font_size = G_reader_settings:readSetting("items_font_size") or math.floor(24 - ((self.perpage - 6)/ 18) * 10 )
for c = 1, math.min(self.perpage, #self.item_table) do
-- calculate index in item_table
local i = (self.page - 1) * self.perpage + c
if i <= #self.item_table then
local item_shortcut = nil
local shortcut_style = "square"
if self.is_enable_shortcut then
-- give different shortcut_style to keys in different
-- lines of keyboard
if c >= 11 and c <= 20 then
--shortcut_style = "rounded_corner"
shortcut_style = "grey_square"
end
item_shortcut = self.item_shortcuts[c]
end
local item_tmp = MenuItem:new{
show_parent = self.show_parent,
state = self.item_table[i].state,
state_size = self.state_size or {},
text = getMenuText(self.item_table[i]),
mandatory = self.item_table[i].mandatory,
bold = self.item_table.current == i or self.item_table[i].bold == true,
dim = self.item_table[i].dim,
font = "smallinfofont",
font_size = font_size,
infont = "infont",
infont_size = infont_size,
dimen = self.item_dimen:new(),
shortcut = item_shortcut,
shortcut_style = shortcut_style,
table = self.item_table[i],
menu = self,
linesize = self.linesize,
single_line = self.single_line,
align_baselines = self.align_baselines,
line_color = self.line_color,
}
table.insert(self.item_group, item_tmp)
-- this is for focus manager
table.insert(self.layout, {item_tmp})
end -- if i <= self.items
end -- for c=1, self.perpage
self:updatePageInfo(select_number)
if self.show_path then
self.path_text:setText(self.path)
end
UIManager:setDirty(self.show_parent, function()
local refresh_dimen =
old_dimen and old_dimen:combine(self.dimen)
or self.dimen
return "ui", refresh_dimen
end)
end
--[[
the itemnumber paramter determines menu page number after switching item table
1. itemnumber >= 0
the page number is calculated with items per page
2. itemnumber == nil
the page number is 1
3. itemnumber is negative number
the page number is not changed, used when item_table is appended with
new entries
alternatively, itemmatch may be provided as a {key = value} table,
and the page number will be the page containing the first item for
which item.key = value
--]]
function Menu:switchItemTable(new_title, new_item_table, itemnumber, itemmatch)
if self.menu_title and new_title then
self.menu_title:setText(new_title)
end
if itemnumber == nil then
self.page = 1
elseif itemnumber >= 0 then
self.page = math.ceil(itemnumber / self.perpage)
end
if type(itemmatch) == "table" then
local key, value = next(itemmatch)
for num, item in ipairs(new_item_table) do
if item[key] == value then
self.page = math.floor((num-1) / self.perpage) + 1
break
end
end
end
-- make sure current page is in right page range
local max_pages = math.ceil(#new_item_table / self.perpage)
if self.page > max_pages then
self.page = max_pages
end
self.item_table = new_item_table
self:updateItems()
end
function Menu:onScreenResize(dimen)
--- @todo Investigate: could this cause minor memory leaks?
self:init()
return false
end
function Menu:onSelectByShortCut(_, keyevent)
for k,v in ipairs(self.item_shortcuts) do
if k > self.perpage then
break
elseif v == keyevent.key then
if self.item_table[(self.page-1)*self.perpage + k] then
self:onMenuSelect(self.item_table[(self.page-1)*self.perpage + k])
end
break
end
end
return true
end
function Menu:onShowGotoDialog()
if self.page_info_text and self.page_info_text.hold_input then
self.page_info_text:onInput(self.page_info_text.hold_input)
end
return true
end
function Menu:onWrapFirst()
if self.page > 1 then
self.page = self.page - 1
local end_position = self.perpage
if self.page == self.page_num then
end_position = #self.item_table % self.perpage
end
self:updateItems(end_position)
end
return false
end
function Menu:onWrapLast()
if self.page < self.page_num then
self:onNextPage()
end
return false
end
--[[
override this function to process the item selected in a different manner
]]--
function Menu:onMenuSelect(item)
if item.sub_item_table == nil then
if self.close_callback then
self.close_callback()
end
self:onMenuChoice(item)
else
-- save menu title for later resume
self.item_table.title = self.title
table.insert(self.item_table_stack, self.item_table)
self:switchItemTable(item.text, item.sub_item_table)
end
return true
end
--[[
default to call item callback
override this function to handle the choice
--]]
function Menu:onMenuChoice(item)
if item.callback then
item.callback()
end
return true
end
--[[
override this function to process the item hold in a different manner
]]--
function Menu:onMenuHold(item)
return true
end
function Menu:onNextPage()
if self.onNext and self.page == self.page_num - 1 then
self:onNext()
end
if self.page < self.page_num then
self.page = self.page + 1
self:updateItems()
elseif self.page == self.page_num then
-- on the last page, we check if we're on the last item
local end_position = #self.item_table % self.perpage
if end_position == 0 then
end_position = self.perpage
end
if end_position ~= self.selected.y then
self:updateItems(end_position)
end
self.page = 1
self:updateItems()
end
return true
end
function Menu: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 Menu:onFirstPage()
self.page = 1
self:updateItems()
return true
end
function Menu:onLastPage()
self.page = self.page_num
self:updateItems()
return true
end
function Menu:onGotoPage(page)
self.page = page
self:updateItems()
return true
end
function Menu:onSelect()
local item = self.item_table[(self.page-1)*self.perpage+self.selected.y]
if item then
self:onMenuSelect(item)
end
return true
end
function Menu:onRight()
local item = self.item_table[(self.page-1)*self.perpage+self.selected.y]
if item then
self:onMenuHold(item)
end
return true
end
function Menu:onClose()
local table_length = #self.item_table_stack
if table_length == 0 then
self:onCloseAllMenus()
else
-- back to parent menu
local parent_item_table = table.remove(self.item_table_stack, table_length)
self:switchItemTable(parent_item_table.title, parent_item_table)
end
return true
end
function Menu:onCloseAllMenus()
UIManager:close(self)
if self.close_callback then
self.close_callback()
end
return true
end
function Menu:onTapCloseAllMenus(arg, ges_ev)
if ges_ev.pos:notIntersectWith(self.dimen) then
self:onCloseAllMenus()
return true
end
end
function Menu:onSwipe(arg, ges_ev)
if ges_ev.direction == "west" then
self:onNextPage()
elseif ges_ev.direction == "east" then
self:onPrevPage()
elseif ges_ev.direction == "south" then
if self.has_close_button and not self.no_title then
-- If there is a close button displayed (so, this Menu can be
-- closed), allow easier closing with swipe up/down
self:onClose()
end
-- If there is no close button, it's a top level Menu and swipe
-- up/down may hide/show top menu
elseif ges_ev.direction == "north" then
-- no use for now
do end -- luacheck: ignore 541
else -- diagonal swipe
-- trigger full refresh
UIManager:setDirty(nil, "full")
end
end
function Menu.itemTableFromTouchMenu(t)
local item_t = {}
for k,v in pairs(t) do
local item = { text = k }
if v.callback then
item.callback = v.callback
else
item.sub_item_table = v
end
table.insert(item_t, item)
end
return item_t
end
return Menu