mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
add collapsable TOC menu
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local CenterContainer = require("ui/widget/container/centercontainer")
|
||||
local GestureRange = require("ui/gesturerange")
|
||||
local Button = require("ui/widget/button")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Menu = require("ui/widget/menu")
|
||||
local Geom = require("ui/geometry")
|
||||
local Screen = require("ui/screen")
|
||||
local Device = require("ui/device")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Event = require("ui/event")
|
||||
local Font = require("ui/font")
|
||||
local DEBUG = require("dbg")
|
||||
@@ -14,6 +15,10 @@ local _ = require("gettext")
|
||||
local ReaderToc = InputContainer:new{
|
||||
toc = nil,
|
||||
ticks = {},
|
||||
toc_indent = " ",
|
||||
collapsed_toc = {},
|
||||
collapse_depth = 2,
|
||||
expanded_nodes = {},
|
||||
toc_menu_title = _("Table of contents"),
|
||||
}
|
||||
|
||||
@@ -54,6 +59,7 @@ end
|
||||
function ReaderToc:onUpdateToc()
|
||||
self.toc = nil
|
||||
self.ticks = {}
|
||||
self.collapsed_toc = {}
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -208,29 +214,66 @@ function ReaderToc:getChapterPagesDone(pageno, level)
|
||||
return previous_chapter
|
||||
end
|
||||
|
||||
function ReaderToc:onShowToc()
|
||||
self:fillToc()
|
||||
-- build menu items
|
||||
if #self.toc > 0 and not self.toc[1].text then
|
||||
for _,v in ipairs(self.toc) do
|
||||
v.text = (" "):rep(v.depth-1)..self:cleanUpTocTitle(v.title)
|
||||
v.mandatory = v.page
|
||||
end
|
||||
end
|
||||
-- update current entry
|
||||
if #self.toc > 0 then
|
||||
for i=1, #self.toc do
|
||||
v = self.toc[i]
|
||||
function ReaderToc:updateCurrentNode()
|
||||
if #self.collapsed_toc > 0 then
|
||||
for i, v in ipairs(self.collapsed_toc) do
|
||||
if v.page > self.pageno then
|
||||
self.toc.current = i > 1 and i - 1 or 1
|
||||
self.collapsed_toc.current = i > 1 and i - 1 or 1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderToc:onShowToc()
|
||||
self:fillToc()
|
||||
local max_depth = self:getMaxDepth()
|
||||
-- build menu items
|
||||
if #self.toc > 0 and not self.toc[1].text then
|
||||
for _,v in ipairs(self.toc) do
|
||||
v.text = self.toc_indent:rep(v.depth-1)..self:cleanUpTocTitle(v.title)
|
||||
v.mandatory = v.page
|
||||
end
|
||||
end
|
||||
|
||||
-- update collapsible state
|
||||
self.expand_button = Button:new{
|
||||
icon = "resources/icons/appbar.control.expand.png",
|
||||
bordersize = 0,
|
||||
show_parent = self,
|
||||
}
|
||||
|
||||
self.collapse_button = Button:new{
|
||||
icon = "resources/icons/appbar.control.collapse.png",
|
||||
bordersize = 0,
|
||||
show_parent = self,
|
||||
}
|
||||
|
||||
if #self.toc > 0 and #self.collapsed_toc == 0 then
|
||||
local depth = 0
|
||||
for i = #self.toc, 1, -1 do
|
||||
local v = self.toc[i]
|
||||
-- node v has child node(s)
|
||||
if v.depth < depth then
|
||||
v.state = self.expand_button:new{
|
||||
callback = function() self:expandToc(i) end,
|
||||
indent = self.toc_indent:rep(v.depth-1),
|
||||
}
|
||||
end
|
||||
if v.depth < self.collapse_depth then
|
||||
table.insert(self.collapsed_toc, 1, v)
|
||||
end
|
||||
depth = v.depth
|
||||
end
|
||||
end
|
||||
|
||||
self:updateCurrentNode()
|
||||
|
||||
local button_size = self.expand_button:getSize()
|
||||
local toc_menu = Menu:new{
|
||||
title = _("Table of Contents"),
|
||||
item_table = self.toc,
|
||||
item_table = self.collapsed_toc,
|
||||
state_size = button_size,
|
||||
ui = self.ui,
|
||||
is_borderless = true,
|
||||
width = Screen:getWidth(),
|
||||
@@ -264,11 +307,74 @@ function ReaderToc:onShowToc()
|
||||
|
||||
toc_menu.show_parent = menu_container
|
||||
|
||||
self.toc_menu = toc_menu
|
||||
|
||||
UIManager:show(menu_container)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- expand TOC node of index in raw toc table
|
||||
function ReaderToc:expandToc(index)
|
||||
table.insert(self.expanded_nodes, index)
|
||||
local cur_node = self.toc[index]
|
||||
local cur_depth = cur_node.depth
|
||||
local collapsed_index = nil
|
||||
for i, v in ipairs(self.collapsed_toc) do
|
||||
if v.page == cur_node.page and v.depth == cur_depth
|
||||
and v.text == cur_node.text then
|
||||
collapsed_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
for i = index + 1, #self.toc do
|
||||
local v = self.toc[i]
|
||||
if v.depth == cur_depth + 1 then
|
||||
collapsed_index = collapsed_index + 1
|
||||
table.insert(self.collapsed_toc, collapsed_index, v)
|
||||
elseif v.depth <= cur_depth then
|
||||
break
|
||||
end
|
||||
end
|
||||
-- change state of current node to expanded
|
||||
cur_node.state = self.collapse_button:new{
|
||||
callback = function() self:collapseToc(index) end,
|
||||
indent = self.toc_indent:rep(cur_depth-1),
|
||||
}
|
||||
self:updateCurrentNode()
|
||||
self.toc_menu:swithItemTable(nil, self.collapsed_toc, -1)
|
||||
end
|
||||
|
||||
-- collapse TOC node of index in raw toc table
|
||||
function ReaderToc:collapseToc(index)
|
||||
local cur_node = self.toc[index]
|
||||
local cur_depth = cur_node.depth
|
||||
local i = 1
|
||||
local is_child_node = false
|
||||
while i <= #self.collapsed_toc do
|
||||
local v = self.collapsed_toc[i]
|
||||
if v.page > cur_node.page and v.depth <= cur_depth then
|
||||
is_child_node = false
|
||||
end
|
||||
if is_child_node then
|
||||
table.remove(self.collapsed_toc, i)
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
if v.page == cur_node.page and v.depth == cur_depth
|
||||
and v.text == cur_node.text then
|
||||
is_child_node = true
|
||||
end
|
||||
end
|
||||
-- change state of current node to collapsed
|
||||
cur_node.state = self.expand_button:new{
|
||||
callback = function() self:expandToc(index) end,
|
||||
indent = self.toc_indent:rep(cur_depth-1),
|
||||
}
|
||||
self:updateCurrentNode()
|
||||
self.toc_menu:swithItemTable(nil, self.collapsed_toc, -1)
|
||||
end
|
||||
|
||||
function ReaderToc:addToMainMenu(tab_item_table)
|
||||
-- insert table to main reader menu
|
||||
table.insert(tab_item_table.navi, 1, {
|
||||
|
||||
@@ -160,28 +160,51 @@ function MenuItem:init()
|
||||
end
|
||||
|
||||
local mandatory = self.mandatory and ""..self.mandatory.." " or ""
|
||||
local mandatory_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.info_face, ""..mandatory, true).x
|
||||
local mandatory_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.info_face,
|
||||
""..mandatory, true, self.bold).x
|
||||
|
||||
w = RenderText:sizeUtf8Text(0, self.dimen.w, self.face, self.text, true).x
|
||||
if w + mandatory_w >= self.content_width then
|
||||
if Device:isTouchDevice() then
|
||||
else
|
||||
local state_button_width = self.state_size.w or 0
|
||||
w = RenderText:sizeUtf8Text(0, self.dimen.w, self.face,
|
||||
self.text, true, self.bold).x
|
||||
if w + mandatory_w + state_button_width >= self.content_width then
|
||||
if Device:hasKeyboard() then
|
||||
self.active_key_events.ShowItemDetail = {
|
||||
{"Right"}, doc = "show item detail"
|
||||
}
|
||||
end
|
||||
local indicator = " >> "
|
||||
local indicator_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.face, indicator, true).x
|
||||
local indicator_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.face,
|
||||
indicator, true, self.bold).x
|
||||
self.text = RenderText:getSubTextByWidth(self.text, self.face,
|
||||
self.content_width - indicator_w - mandatory_w, true) .. indicator
|
||||
self.content_width - indicator_w - mandatory_w - state_button_width,
|
||||
true, self.bold) .. indicator
|
||||
end
|
||||
|
||||
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{
|
||||
HorizontalSpan:new{
|
||||
width = RenderText:sizeUtf8Text(0, self.dimen.w, self.face,
|
||||
state_indent, true, self.bold).x,
|
||||
},
|
||||
state_button
|
||||
}
|
||||
}
|
||||
local text_container = LeftContainer:new{
|
||||
dimen = Geom:new{w = self.content_width, h = self.dimen.h},
|
||||
TextWidget:new{
|
||||
text = self.text,
|
||||
face = self.face,
|
||||
bold = self.bold,
|
||||
HorizontalGroup:new{
|
||||
HorizontalSpan:new{
|
||||
width = self.state_size.w,
|
||||
},
|
||||
TextWidget:new{
|
||||
text = self.text,
|
||||
face = self.face,
|
||||
bold = self.bold,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +218,7 @@ function MenuItem:init()
|
||||
}
|
||||
|
||||
self._underline_container = UnderlineContainer:new{
|
||||
vertical_align = "center",
|
||||
dimen = Geom:new{
|
||||
w = self.content_width,
|
||||
h = self.dimen.h
|
||||
@@ -203,16 +227,17 @@ function MenuItem:init()
|
||||
align = "center",
|
||||
OverlapGroup:new{
|
||||
dimen = Geom:new{w = self.content_width, h = self.dimen.h},
|
||||
state_container,
|
||||
text_container,
|
||||
mandatory_container,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
self[1] = FrameContainer:new{
|
||||
bordersize = 0,
|
||||
padding = 0,
|
||||
HorizontalGroup:new{
|
||||
align = "center",
|
||||
HorizontalSpan:new{ width = 5 },
|
||||
ItemShortCutIcon:new{
|
||||
dimen = shortcut_icon_dimen,
|
||||
@@ -569,6 +594,8 @@ function Menu:updateItems(select_number)
|
||||
end
|
||||
local item_tmp = MenuItem:new{
|
||||
show_parent = self.show_parent,
|
||||
state = self.item_table[i].state,
|
||||
state_size = self.state_size or {},
|
||||
text = self.item_table[i].text,
|
||||
mandatory = self.item_table[i].mandatory,
|
||||
bold = self.item_table.current == i,
|
||||
|
||||
BIN
resources/icons/appbar.control.collapse.png
Normal file
BIN
resources/icons/appbar.control.collapse.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 B |
BIN
resources/icons/appbar.control.expand.png
Normal file
BIN
resources/icons/appbar.control.expand.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 B |
49
resources/icons/src/appbar.control.collapse.svg
Normal file
49
resources/icons/src/appbar.control.collapse.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
enable-background="new 0 0 76.00 76.00"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="appbar.control.collapse.svg"
|
||||
inkscape:export-filename="/home/chrox/dev/koreader/resources/icons/appbar.control.collapse.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"><metadata
|
||||
id="metadata10"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs8" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="1015"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="12.421053"
|
||||
inkscape:cx="30.041478"
|
||||
inkscape:cy="18.178404"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" /><path
|
||||
d="M 16,3 8,13 8,13 0,3 z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.2;stroke-linejoin:round"
|
||||
sodipodi:nodetypes="ccccc" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
49
resources/icons/src/appbar.control.expand.svg
Normal file
49
resources/icons/src/appbar.control.expand.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
enable-background="new 0 0 76.00 76.00"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="appbar.control.expand.svg"
|
||||
inkscape:export-filename="/home/chrox/dev/koreader/resources/icons/appbar.control.expand.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"><metadata
|
||||
id="metadata10"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs8" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="1015"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="12.421053"
|
||||
inkscape:cx="30.041478"
|
||||
inkscape:cy="18.178404"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" /><path
|
||||
d="M 3,0 13,8 13,8 3,16 z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.2;stroke-linejoin:round"
|
||||
sodipodi:nodetypes="ccccc" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -24,7 +24,7 @@ describe("Readertoc module", function()
|
||||
local ticks_level_0 = nil
|
||||
it("should get ticks of level 0", function()
|
||||
ticks_level_0 = toc:getTocTicks(0)
|
||||
DEBUG("ticks", ticks_level_0)
|
||||
--DEBUG("ticks", ticks_level_0)
|
||||
assert.are.same(28, #ticks_level_0)
|
||||
end)
|
||||
local ticks_level_1 = nil
|
||||
@@ -68,4 +68,26 @@ describe("Readertoc module", function()
|
||||
assert.are.same(0, toc:getChapterPagesDone(100, 0))
|
||||
assert.are.same(10, toc:getChapterPagesDone(200, 0))
|
||||
end)
|
||||
describe("collasible TOC", function()
|
||||
it("should collapse the secondary toc nodes by default", function()
|
||||
toc:onShowToc()
|
||||
assert.are.same(7, #toc.collapsed_toc)
|
||||
end)
|
||||
it("should not expand toc nodes that have no child nodes", function()
|
||||
toc:expandToc(2)
|
||||
assert.are.same(7, #toc.collapsed_toc)
|
||||
end)
|
||||
it("should expand toc nodes that have child nodes", function()
|
||||
toc:expandToc(3)
|
||||
assert.are.same(13, #toc.collapsed_toc)
|
||||
toc:expandToc(18)
|
||||
assert.are.same(18, #toc.collapsed_toc)
|
||||
end)
|
||||
it("should collapse toc nodes that have been expanded", function()
|
||||
toc:collapseToc(3)
|
||||
assert.are.same(12, #toc.collapsed_toc)
|
||||
toc:collapseToc(18)
|
||||
assert.are.same(7, #toc.collapsed_toc)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user