diff --git a/selectmenu.lua b/selectmenu.lua index f016b752e..2fa9793ea 100644 --- a/selectmenu.lua +++ b/selectmenu.lua @@ -43,6 +43,7 @@ SelectMenu = { selected_item = nil, commands = nil, + expandable = false, -- if true handle Right/Left FW selector keys -- NuPogodi, 30.08.12: define font to render menu items own_glyph = 0, -- render menu items with default "cfont" @@ -149,6 +150,22 @@ function SelectMenu:addAllCommands() end end ) + if self.expandable then + self.commands:add(KEY_FW_RIGHT, nil, "", + "expand menu item", + function(sm) + self.selected_item = (sm.perpage * (sm.page - 1) + sm.current) + return "expand" + end + ) + self.commands:add(KEY_FW_LEFT, nil, "", + "collapse menu item", + function(sm) + self.selected_item = (sm.perpage * (sm.page - 1) + sm.current) + return "collapse" + end + ) + end local KEY_Q_to_P = {} for i = KEY_Q, KEY_P do table.insert(KEY_Q_to_P, Keydef:new(i, nil, "")) @@ -312,9 +329,11 @@ function SelectMenu:choose(ypos, height) end -- for c=1, self.perpage end -- if self.items == 0 - -- draw footer - DrawFooter("Page "..self.page.." of "..(math.ceil(self.items / self.perpage)),fface,self.foot_H) - + local footer = "Page "..self.page.." of "..(math.ceil(self.items / self.perpage)) + if self.expandable then + footer = footer.." (Use Right/Left FW-selector keys to expand/collapse nodes)" + end + renderUtf8Text(fb.bb, self.margin_H, height-7, fface, footer, true) end if self.markerdirty then @@ -359,6 +378,15 @@ function SelectMenu:choose(ypos, height) end if self.selected_item ~= nil then + if self.expandable then + if ret_code == "expand" then + Debug("# expand "..self.selected_item) + return nil, self.selected_item + elseif ret_code == "collapse" then + Debug("# collapse "..self.selected_item) + return nil, -self.selected_item + end + end Debug("# selected "..self.selected_item) return self.selected_item, self.item_array[self.selected_item] end diff --git a/unireader.lua b/unireader.lua index 2184f4694..b2683ecba 100644 --- a/unireader.lua +++ b/unireader.lua @@ -92,6 +92,11 @@ UniReader = { bookmarks = {}, highlight = {}, toc = nil, + toc_children = nil, -- each element is the list of children for each TOC node (nil if none) + toc_curitem = 0, -- points to the current location in TOC + toc_xview = nil, -- fully expanded (and marked with '+') view of TOC + toc_cview = nil, -- current view of TOC + toc_curidx_to_x = nil, -- current view to expanded view map bbox = {}, -- override getUsedBBox @@ -1586,7 +1591,8 @@ function UniReader:screenRotate(orien) end function UniReader:cleanUpTocTitle(title) - return title:gsub("\13", "") + local s, _ = title:gsub("\13", "") + return s end function UniReader:fillToc() @@ -1628,39 +1634,148 @@ function UniReader:gotoTocEntry(entry) self:goto(entry.page) end +-- expand TOC item to one level down +function UniReader:expandTOCItem(xidx, item_no) + if string.find(self.toc_cview[item_no], "^+ ") then + for i=#self.toc_children[xidx],1,-1 do + table.insert(self.toc_cview, item_no+1, + self.toc_xview[self.toc_children[xidx][i]]) + table.insert(self.toc_curidx_to_x, item_no+1, + self.toc_children[xidx][i]) + end + self.toc_cview[item_no] = string.gsub(self.toc_cview[item_no], "^+ ", "- ", 1) + end +end + +-- collapse TOC item AND all its descendants to all levels, recursively +function UniReader:collapseTOCItem(xidx, item_no) + if string.find(self.toc_cview[item_no], "^- ") then + for i=1,#self.toc_children[xidx] do + self:collapseTOCItem(self.toc_curidx_to_x[item_no+1], item_no+1) + table.remove(self.toc_cview, item_no+1) + table.remove(self.toc_curidx_to_x, item_no+1) + end + self.toc_cview[item_no] = string.gsub(self.toc_cview[item_no], "^- ", "+ ", 1) + end +end + +-- calculate the position as index into self.toc_cview[], +-- corresponding to the current page. +function UniReader:findTOCpos() + local pos, found_pos = 0, false + + -- find the index into toc_xview first + for k,v in ipairs(self.toc) do + if v.page > self.pageno then + pos = k - 1 + break + end + end + + if pos == 0 then + pos = #self.toc + end + + -- now map it to toc_cview[] + for k,v in ipairs(self.toc_curidx_to_x) do + if v == pos then + pos = k + found_pos = true + break + elseif v > pos then + pos = k - 1 + found_pos = true + break + end + end + + if not found_pos then + pos = #self.toc_cview + end + + return pos +end + function UniReader:showToc() if not self.toc then - -- build toc if needed. - self:fillToc() - end + InfoMessage:show("Retrieving TOC...", 1) + self:fillToc() -- fill self.toc(title,page,depth) from physical TOC + self.toc_children = {} + self.toc_xview = {} + self.toc_cview = {} + self.toc_curidx_to_x = {} - -- build menu items - local menu_items, item_no = {}, -1 - for k,v in ipairs(self.toc) do - table.insert(menu_items, (" "):rep(v.depth-1)..self:cleanUpTocTitle(v.title)) - -- to find current TOC-entry - if v.page <= self.pageno then - item_no = item_no + 1 + -- To combine the forest represented by the array of depths + -- (self.toc[].depth) into a single tree we introduce a virtual head + -- of depth=0 at position index=0 (The Great Parent) + local prev, prev_depth = 0, 0 + self.toc_xview[0] = "_HEAD" + + -- the parent[] array is only needed for the calculation of + -- self.toc_children[] arrays. + local parent = {} + for k,v in ipairs(self.toc) do + table.insert(self.toc_xview, + (" "):rep(v.depth-1)..self:cleanUpTocTitle(v.title)) + if (v.depth > prev_depth) then --> k is a child of prev + if not self.toc_children[prev] then + self.toc_children[prev] = {} + end + table.insert(self.toc_children[prev], k) + parent[k] = prev + self.toc_xview[prev] = "+ "..self.toc_xview[prev] + elseif (v.depth == prev_depth) then --> k and prev are siblings + parent[k] = parent[prev] + table.insert(self.toc_children[parent[k]], k) + else --> k and prev must have a common (possibly virtual) ancestor + local par = parent[prev] + while (self.toc[par].depth > v.depth) do + par = parent[par] + end + parent[k] = parent[par] + table.insert(self.toc_children[parent[k]], k) + end + prev = k + prev_depth = self.toc[prev].depth + end -- for k,v in ipairs(self.toc) + self.toc_curidx_to_x = self.toc_children[0] + for i=1,#self.toc_children[0] do + table.insert(self.toc_cview, self.toc_xview[self.toc_children[0][i]]) end end - if #menu_items == 0 then - showInfoMsgWithDelay("This document does not have a TOC.", 2000, 1) - else + if #self.toc == 0 then + return showInfoMsgWithDelay("No Table of Contents", 1500, 1) + end + + self.toc_curitem = self:findTOCpos() + + while true do + local ret_code = -1 toc_menu = SelectMenu:new{ - menu_title = "Table of Contents", - item_array = menu_items, - -- to autoselect current TOC-entry - current_entry = item_no, + menu_title = "Table of Contents (" .. tostring(#self.toc_cview) .. "/" .. tostring(#self.toc) .. " items)", + item_array = self.toc_cview, + current_entry = self.toc_curitem-1, + expandable = true } - item_no = toc_menu:choose(0, fb.bb:getHeight()) - - if item_no then - self:gotoTocEntry(self.toc[item_no]) - else - self:redrawCurrentPage() - end - end + ret_code, item_no = toc_menu:choose(0, fb.bb:getHeight()) + if ret_code then -- normal item selection + return self:gotoTocEntry(self.toc[self.toc_curidx_to_x[ret_code]]) + elseif item_no then -- expand or collapse item + local abs_item_no = math.abs(item_no) + local xidx = self.toc_curidx_to_x[abs_item_no] + if self.toc_children[xidx] then + if item_no > 0 then + self:expandTOCItem(xidx, item_no) + else + self:collapseTOCItem(xidx, abs_item_no) + end + end + self.toc_curitem = abs_item_no + else -- return from menu via Back + return self:redrawCurrentPage() + end -- if ret_code + end -- while true end function UniReader:showJumpHist() @@ -1968,6 +2083,12 @@ function UniReader:inputLoop() -- do clean up stuff self:clearCache() self.toc = nil + self.toc_children = nil + self.toc_curitem = 0 + self.toc_xview = nil + self.toc_cview = nil + self.toc_curidx_to_x = nil + self.toc_xidx_to_cur = nil if self.doc ~= nil then self.doc:close() end @@ -1982,7 +2103,7 @@ function UniReader:inputLoop() self.settings:saveSetting("globalzoom", self.globalzoom) self.settings:saveSetting("globalzoom_mode", self.globalzoom_mode) self.settings:saveSetting("render_mode", self.render_mode) -- djvu-related only - --[[ the following parameters was already stored when user changed defaults + --[[ the following parameters were already stored when user changed defaults self.settings:saveSetting("shift_x", self.shift_x) self.settings:saveSetting("shift_y", self.shift_y) self.settings:saveSetting("step_manual_zoom", self.step_manual_zoom) @@ -2112,8 +2233,7 @@ function UniReader:addAllCommands() local re = zoom_menu:choose(0, G_height) if not re or re==(1-unireader.globalzoom_mode) or re==1 or re==8 or re==9 then -- if not proper zoom-mode unireader:redrawCurrentPage() - else -- in most cases the message is not necessary, so feel you free to comment - -- InfoMessage:show("Redrawing in new zoom mode...", 1) + else unireader:setglobalzoom_mode(1-re) end end) @@ -2178,7 +2298,7 @@ function UniReader:addAllCommands() unireader:redrawCurrentPage() end) self.commands:add(KEY_T,nil,"T", - "show table of content", + "show table of content (TOC)", function(unireader) unireader:showToc() end) diff --git a/util.h b/util.h index e18c5f092..23bf52c2d 100644 --- a/util.h +++ b/util.h @@ -1,5 +1,5 @@ /* - KindlePDFViewer: buffer for blitting muPDF data to framebuffer (blitbuffer) + KindlePDFViewer: miscellaneous utility functions for Lua Copyright (C) 2011 Hans-Werner Hilse This program is free software: you can redistribute it and/or modify