From 7c9130744c1fabdce22078b1f5b91e2bf6fffc5d Mon Sep 17 00:00:00 2001 From: chrox Date: Tue, 14 Oct 2014 21:33:13 +0800 Subject: [PATCH] add collapsable TOC menu --- frontend/apps/reader/modules/readertoc.lua | 126 ++++++++++++++++-- frontend/ui/widget/menu.lua | 53 ++++++-- resources/icons/appbar.control.collapse.png | Bin 0 -> 246 bytes resources/icons/appbar.control.expand.png | Bin 0 -> 213 bytes .../icons/src/appbar.control.collapse.svg | 49 +++++++ resources/icons/src/appbar.control.expand.svg | 49 +++++++ spec/unit/readertoc_spec.lua | 24 +++- 7 files changed, 277 insertions(+), 24 deletions(-) create mode 100644 resources/icons/appbar.control.collapse.png create mode 100644 resources/icons/appbar.control.expand.png create mode 100644 resources/icons/src/appbar.control.collapse.svg create mode 100644 resources/icons/src/appbar.control.expand.svg diff --git a/frontend/apps/reader/modules/readertoc.lua b/frontend/apps/reader/modules/readertoc.lua index 499cde266..9d98a57c5 100644 --- a/frontend/apps/reader/modules/readertoc.lua +++ b/frontend/apps/reader/modules/readertoc.lua @@ -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:updateCurrentNode() + if #self.collapsed_toc > 0 then + for i, v in ipairs(self.collapsed_toc) do + if v.page > self.pageno then + 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 = (" "):rep(v.depth-1)..self:cleanUpTocTitle(v.title) + v.text = self.toc_indent: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] - if v.page > self.pageno then - self.toc.current = i > 1 and i - 1 or 1 - break + + -- 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, { diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 724a94b22..65fc7b0ab 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -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 - - 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 mandatory_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.info_face, + ""..mandatory, true, self.bold).x + + 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, diff --git a/resources/icons/appbar.control.collapse.png b/resources/icons/appbar.control.collapse.png new file mode 100644 index 0000000000000000000000000000000000000000..b87cad71f1d0f0be8078d0acaec799c5e91ac18d GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHi$UACHPo%hEGGK%r(&7sn8d^Jgb*6l6B!V428VwLnB_ zk$Q;0(hJNNEq};IZi}}Md)8gvY@NZ7q0WEn%r=L#ZS3JwSN(HZofRtBa7JWeb(D*d zFasllfZ&-ui)LCX@qH3u*zwz|;Ctg<7KZ7}Gj5*Q!+D=s#&!Ck;|z;m%r#(}(S0s+ l`u;1`@prfUz5m&kG5Y)2<-XF~HbCbvc)I$ztaD0e0sx`BRKoxO literal 0 HcmV?d00001 diff --git a/resources/icons/appbar.control.expand.png b/resources/icons/appbar.control.expand.png new file mode 100644 index 0000000000000000000000000000000000000000..284407b2d44a06c71ac5fac00f880bc205908109 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHi$UACD^Ijl_i@p(IZi#}JM4YbR~wZ3y6S(HDDjKttz< zU%jGRBKxZR#Gcki--XsMs^sM7``oiT{AvvAb(R^CX+jJPp}jx{C@3ZKFj-vE-0HCW z3!9CWZo}?NY-aWAj(7^EKb1Qk+N^#5c-8w^?Z3=xt%TfxwlR3R`njxgN@xNAMhim~ literal 0 HcmV?d00001 diff --git a/resources/icons/src/appbar.control.collapse.svg b/resources/icons/src/appbar.control.collapse.svg new file mode 100644 index 000000000..101eed96d --- /dev/null +++ b/resources/icons/src/appbar.control.collapse.svg @@ -0,0 +1,49 @@ + +image/svg+xml \ No newline at end of file diff --git a/resources/icons/src/appbar.control.expand.svg b/resources/icons/src/appbar.control.expand.svg new file mode 100644 index 000000000..311087080 --- /dev/null +++ b/resources/icons/src/appbar.control.expand.svg @@ -0,0 +1,49 @@ + +image/svg+xml \ No newline at end of file diff --git a/spec/unit/readertoc_spec.lua b/spec/unit/readertoc_spec.lua index 1b62d8cd1..a4c93098e 100644 --- a/spec/unit/readertoc_spec.lua +++ b/spec/unit/readertoc_spec.lua @@ -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)