diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index 78ae76ab3..ecb7325b0 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -62,13 +62,6 @@ local symbol_prefix = { } } -local MODE_NB = 0 -local MODE_INDEX = {} -for k,v in pairs(MODE) do - MODE_INDEX[v] = k - MODE_NB = MODE_NB + 1 -end - -- functions that generates footer text for each mode local footerTextGeneratorMap = { empty = function() return "" end, @@ -218,6 +211,33 @@ function ReaderFooter:init() item_prefix = "icons" } + if not self.settings.order then + self.mode_nb = 0 + self.mode_index = {} + local mode_tbl = {} + for k,v in pairs(MODE) do + mode_tbl[v] = k + end + local mode_name + for i = 0, #mode_tbl do + mode_name = mode_tbl[i] + if mode_name == "wifi_status" and not Device:isAndroid() then + do end -- luacheck: ignore 541 + elseif mode_name == "frontlight" and not Device:hasFrontlight() then + do end -- luacheck: ignore 541 + else + self.mode_index[self.mode_nb] = mode_name + self.mode_nb = self.mode_nb + 1 + end + end + else + self.mode_index = self.settings.order + self.mode_nb = #self.mode_index + end + self.mode_list = {} + for i = 0, #self.mode_index do + self.mode_list[self.mode_index[i]] = i + end if self.settings.disabled then -- footer featuren disabled completely, stop initialization now self:disableFooter() @@ -227,7 +247,7 @@ function ReaderFooter:init() self.pageno = self.view.state.page self.has_no_mode = true self.reclaim_height = self.settings.reclaim_height or false - for _, m in ipairs(MODE_INDEX) do + for _, m in ipairs(self.mode_index) do if self.settings[m] then self.has_no_mode = false break @@ -265,12 +285,12 @@ function ReaderFooter:init() self.mode = G_reader_settings:readSetting("reader_footer_mode") or self.mode if self.has_no_mode and self.settings.disable_progress_bar then - self.mode = MODE.off + self.mode = self.mode_list.off self.view.footer_visible = false self:resetLayout() end if self.settings.all_at_once then - self.view.footer_visible = (self.mode ~= MODE.off) + self.view.footer_visible = (self.mode ~= self.mode_list.off) self:updateFooterTextGenerator() else self:applyFooterMode() @@ -414,18 +434,19 @@ function ReaderFooter:disableFooter() self.onPosUpdate = function() end self.onUpdatePos = function() end self.onSetStatusLine = function() end - self.mode = MODE.off + self.mode = self.mode_list.off self.view.footer_visible = false end function ReaderFooter:updateFooterTextGenerator() local footerTextGenerators = {} - for _, m in pairs(MODE_INDEX) do + for i, m in pairs(self.mode_index) do if self.settings[m] then table.insert(footerTextGenerators, footerTextGeneratorMap[m]) if not self.settings.all_at_once then -- if not show all at once, then one is enough + self.mode = i break end end @@ -520,7 +541,7 @@ function ReaderFooter:addToMainMenu(menu_items) local prev_has_no_mode = self.has_no_mode local prev_reclaim_height = self.reclaim_height self.has_no_mode = true - for mode_num, m in pairs(MODE_INDEX) do + for mode_num, m in pairs(self.mode_index) do if self.settings[m] then first_enabled_mode_num = mode_num self.has_no_mode = false @@ -532,7 +553,7 @@ function ReaderFooter:addToMainMenu(menu_items) if self.has_no_mode then self.ui:handleEvent(Event:new("SetPageBottomMargin", self.view.document.configurable.b_page_margin)) self.genFooterText = footerTextGeneratorMap.empty - self.mode = MODE.off + self.mode = self.mode_list.off elseif prev_has_no_mode then self.ui:handleEvent(Event:new("SetPageBottomMargin", self.view.document.configurable.b_page_margin)) G_reader_settings:saveSetting("reader_footer_mode", first_enabled_mode_num) @@ -544,7 +565,7 @@ function ReaderFooter:addToMainMenu(menu_items) should_update = callback(self) elseif self.settings.all_at_once then should_update = self:updateFooterTextGenerator() - elseif (MODE[option] == self.mode and self.settings[option] == false) + elseif (self.mode_list[option] == self.mode and self.settings[option] == false) or (prev_has_no_mode ~= self.has_no_mode) then -- current mode got disabled, redraw footer with other -- enabled modes. if all modes are disabled, then only show @@ -565,6 +586,32 @@ function ReaderFooter:addToMainMenu(menu_items) table.insert(sub_items, { text = _("Settings"), sub_item_table = { + { + text = _("Sort items"), + callback = function() + local item_table = {} + for i=1, #self.mode_index do + table.insert(item_table, {text = self:textOptionTitles(self.mode_index[i]), label = self.mode_index[i]}) + end + local SortWidget = require("ui/widget/sortwidget") + local sort_item + sort_item = SortWidget:new{ + title = _("Sort footer items"), + item_table = item_table, + callback = function() + for i=1, #sort_item.item_table do + self.mode_index[i] = sort_item.item_table[i].label + end + self.settings.order = self.mode_index + G_reader_settings:saveSetting("footer", self.settings) + self:updateFooterTextGenerator() + self:updateFooter() + UIManager:setDirty(nil, "ui") + end + } + UIManager:show(sort_item) + end, + }, getMinibarOption("all_at_once", self.updateFooterTextGenerator), getMinibarOption("reclaim_height"), { @@ -1071,7 +1118,7 @@ function ReaderFooter:applyFooterMode(mode) -- 9 for memory usage -- 10 for wifi status if mode ~= nil then self.mode = mode end - self.view.footer_visible = (self.mode ~= MODE.off) + self.view.footer_visible = (self.mode ~= self.mode_list.off) -- If all-at-once is enabled, just hide, but the text will keep being processed... if self.settings.all_at_once then @@ -1083,7 +1130,7 @@ function ReaderFooter:applyFooterMode(mode) return end - local mode_name = MODE_INDEX[self.mode] + local mode_name = self.mode_index[self.mode] if not self.settings[mode_name] or self.has_no_mode then -- all modes disabled, only show progress bar mode_name = "empty" @@ -1093,7 +1140,7 @@ end function ReaderFooter:onEnterFlippingMode() self.orig_mode = self.mode - self:applyFooterMode(MODE.page_progress) + self:applyFooterMode(self.mode_list.page_progress) end function ReaderFooter:onExitFlippingMode() @@ -1115,19 +1162,19 @@ function ReaderFooter:onTapFooter(ges) else if self.settings.all_at_once or self.has_no_mode then if self.mode >= 1 then - self.mode = MODE.off + self.mode = self.mode_list.off else - self.mode = MODE.page_progress + self.mode = self.mode_list.page_progress end else - self.mode = (self.mode + 1) % MODE_NB - for i, m in ipairs(MODE_INDEX) do - if self.mode == MODE.off then break end + self.mode = (self.mode + 1) % self.mode_nb + for i, m in ipairs(self.mode_index) do + if self.mode == self.mode_list.off then break end if self.mode == i then if self.settings[m] then break else - self.mode = (self.mode + 1) % MODE_NB + self.mode = (self.mode + 1) % self.mode_nb end end end @@ -1140,7 +1187,7 @@ function ReaderFooter:onTapFooter(ges) end function ReaderFooter:onHoldFooter() - if self.mode == MODE.off then return end + if self.mode == self.mode_list.off then return end self.ui:handleEvent(Event:new("ShowSkimtoDialog")) return true end @@ -1150,12 +1197,12 @@ function ReaderFooter:setVisible(visible) -- If it was off, just do as if we tap'ed on it (so we don't -- duplicate onTapFooter() code - not if flipping_visible as in -- this case, a ges.pos argument to onTapFooter(ges) is required) - if self.mode == MODE.off and not self.view.flipping_visible then + if self.mode == self.mode_list.off and not self.view.flipping_visible then self:onTapFooter() end - self.view.footer_visible = (self.mode ~= MODE.off) + self.view.footer_visible = (self.mode ~= self.mode_list.off) else - self:applyFooterMode(MODE.off) + self:applyFooterMode(self.mode_list.off) end end diff --git a/frontend/ui/size.lua b/frontend/ui/size.lua index cb3f22466..8cd396512 100644 --- a/frontend/ui/size.lua +++ b/frontend/ui/size.lua @@ -63,6 +63,7 @@ local Size = { }, item = { height_default = Screen:scaleBySize(30), + height_big = Screen:scaleBySize(40), height_large = Screen:scaleBySize(50), }, span = { diff --git a/frontend/ui/widget/button.lua b/frontend/ui/widget/button.lua index e729d4952..415f6f036 100644 --- a/frontend/ui/widget/button.lua +++ b/frontend/ui/widget/button.lua @@ -222,6 +222,10 @@ function Button:onTapSelectButton() -- And we also often have to delay the callback to both see the flash and/or avoid tearing artefacts w/ fast refreshes... UIManager:tickAfterNext(function() self.callback() + if not self[1] or not self[1].invert or not self[1].dimen then + -- widget no more there (destroyed, re-init'ed by setText(), or not inverted: nothing to invert back + return + end self[1].invert = false UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) if self.text then diff --git a/frontend/ui/widget/sortwidget.lua b/frontend/ui/widget/sortwidget.lua new file mode 100644 index 000000000..4923431dc --- /dev/null +++ b/frontend/ui/widget/sortwidget.lua @@ -0,0 +1,499 @@ +local Blitbuffer = require("ffi/blitbuffer") +local BottomContainer = require("ui/widget/container/bottomcontainer") +local Button = require("ui/widget/button") +local CloseButton = require("ui/widget/closebutton") +local Device = require("device") +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 InputContainer = require("ui/widget/container/inputcontainer") +local LeftContainer = require("ui/widget/container/leftcontainer") +local LineWidget = require("ui/widget/linewidget") +local OverlapGroup = require("ui/widget/overlapgroup") +local RenderText = require("ui/rendertext") +local Size = require("ui/size") +local TextWidget = require("ui/widget/textwidget") +local UIManager = require("ui/uimanager") +local VerticalGroup = require("ui/widget/verticalgroup") +local VerticalSpan = require("ui/widget/verticalspan") +local Screen = Device.screen +local T = require("ffi/util").template +local _ = require("gettext") + +local SortTitleWidget = VerticalGroup:new{ + sort_page = nil, + title = "", + tface = Font:getFace("tfont"), + align = "left", + use_top_page_count = false, +} + +function SortTitleWidget:init() + self.close_button = CloseButton:new{ window = self } + local btn_width = self.close_button:getSize().w + local title_txt_width = RenderText:sizeUtf8Text( + 0, self.width, self.tface, self.title).x + local show_title_txt + if self.width < (title_txt_width + btn_width) then + show_title_txt = RenderText:truncateTextByWidth( + self.title, self.tface, self.width-btn_width) + else + show_title_txt = self.title + end + -- title and close button + table.insert(self, OverlapGroup:new{ + dimen = { w = self.width }, + TextWidget:new{ + text = show_title_txt, + face = self.tface, + }, + self.close_button, + }) + -- page count and separation line + self.title_bottom = OverlapGroup:new{ + dimen = { w = self.width, h = Size.line.thick }, + LineWidget:new{ + dimen = Geom:new{ w = self.width, h = Size.line.thick }, + background = Blitbuffer.COLOR_DARK_GRAY, + style = "solid", + }, + } + if self.use_top_page_count then + self.page_cnt = FrameContainer:new{ + padding = Size.padding.default, + margin = 0, + bordersize = 0, + background = Blitbuffer.COLOR_WHITE, + -- overlap offset x will be updated in setPageCount method + overlap_offset = {0, -15}, + TextWidget:new{ + text = "", -- page count + fgcolor = Blitbuffer.COLOR_DARK_GRAY, + face = Font:getFace("smallffont"), + }, + } + table.insert(self.title_bottom, self.page_cnt) + end + table.insert(self, self.title_bottom) + table.insert(self, VerticalSpan:new{ width = Size.span.vertical_large }) +end + +function SortTitleWidget:setPageCount(curr, total) + if total == 1 then + -- remove page count if there is only one page + table.remove(self.title_bottom, 2) + return + end + self.page_cnt[1]:setText(curr .. "/" .. total) + self.page_cnt.overlap_offset[1] = (self.width - self.page_cnt:getSize().w - 10) + self.title_bottom[2] = self.page_cnt +end + +function SortTitleWidget:onClose() + self.sort_page:onClose() + return true +end + + +local SortItemWidget = InputContainer:new{ + key = nil, + cface = Font:getFace("smallinfofont"), + tface = Font:getFace("smallinfofontbold"), + width = nil, + height = nil, +} + +function SortItemWidget:init() + self.dimen = Geom:new{w = self.width, h = self.height} + if Device:isTouchDevice() then + self.ges_events.Tap = { + GestureRange:new{ + ges = "tap", + range = self.dimen, + } + } + self.ges_events.Hold = { + GestureRange:new{ + ges = "hold", + range = self.dimen, + } + } + end + + local frame_padding = Size.padding.default + local frame_internal_width = self.width - frame_padding * 2 + local text_rendered = RenderText:sizeUtf8Text(0, self.width, self.tface, self.text).x + if text_rendered > frame_internal_width then + self.text = RenderText:truncateTextByWidth(self.text, self.tface, frame_internal_width) + end + + self[1] = FrameContainer:new{ + padding = 0, + bordersize = 0, + LeftContainer:new{ + dimen = { + w = frame_internal_width, + h = self.height + }, + TextWidget:new{ + text = self.text, + face = self.tface, + } + }, + } + self[1].invert = self.invert +end + +function SortItemWidget:onTap() + if self.show_parent.marked == self.index then + self.show_parent.marked = 0 + else + self.show_parent.marked = self.index + end + self.show_parent:_populateItems() + return true +end + +function SortItemWidget:onHold() + return true +end + +local SortWidget = InputContainer:new{ + title = "", + width = nil, + height = nil, + -- index for the first item to show + show_page = 1, + use_top_page_count = false, + -- table of items to sort + item_table = {}, + callback = nil, +} + +function SortWidget:init() + -- no item is selected on start + self.marked = 0 + self.dimen = Geom:new{ + w = self.width or Screen:getWidth(), + h = self.height or Screen:getHeight(), + } + if Device:isTouchDevice() then + self.ges_events.Swipe = { + GestureRange:new{ + ges = "swipe", + range = self.dimen, + } + } + end + local padding = Size.padding.large + self.width_widget = self.dimen.w - 2 * padding + self.item_width = self.dimen.w - 2 * padding + self.item_height = Size.item.height_big + + -- group for footer + self.footer_left = Button:new{ + text = "◀", + width = self.width_widget * 13 / 100, + callback = function() self:prevPage() end, + text_font_size = 28, + bordersize = 0, + padding = 0, + radius = 0, + } + self.footer_right = Button:new{ + text = "▶", + width = self.width_widget * 13 / 100, + callback = function() self:nextPage() end, + text_font_size = 28, + bordersize = 0, + padding = 0, + radius = 0, + } + self.footer_first_up = Button:new{ + text = "◀◀", + width = self.width_widget * 13 / 100, + callback = function() + if self.marked > 0 then + self:moveItem(-1) + else + self:goToPage(1) + end + end, + text_font_size = 28, + bordersize = 0, + padding = 0, + radius = 0, + } + self.footer_last_down = Button:new{ + text = "▶▶", + width = self.width_widget * 13 / 100, + callback = function() + if self.marked > 0 then + self:moveItem(1) + else + self:goToPage(self.pages) + end + end, + text_font_size = 28, + bordersize = 0, + padding = 0, + radius = 0, + } + self.footer_cancel = Button:new{ + text = "✘", + width = self.width_widget * 13 / 100, + callback = function() self:onClose() end, + bordersize = 0, + text_font_size = 28, + padding = 0, + radius = 0, + } + + self.footer_ok = Button:new{ + text= "✓", + width = self.width_widget * 13 / 100, + callback = function() self:onReturn() end, + bordersize = 0, + padding = 0, + radius = 0, + text_font_size = 28, + } + + self.footer_page = Button:new{ + text = "", + tap_input = { + title = _("Enter page number"), + type = "number", + hint_func = function() + return "(" .. "1 - " .. self.pages .. ")" + end, + callback = function(input) + local page = tonumber(input) + if page and page >= 1 and page <= self.pages then + self:goToPage(page) + end + end, + }, + bordersize = 0, + margin = 0, + text_font_face = "pgfont", + text_font_bold = false, + width = self.width_widget * 22 / 100, + } + local button_vertical_line = LineWidget:new{ + dimen = Geom:new{ w = Size.line.thick, h = self.item_height * 1.25 }, + background = Blitbuffer.COLOR_DARK_GRAY, + style = "solid", + } + self.page_info = HorizontalGroup:new{ + self.footer_cancel, + button_vertical_line, + self.footer_first_up, + button_vertical_line, + self.footer_left, + button_vertical_line, + self.footer_page, + button_vertical_line, + self.footer_right, + button_vertical_line, + self.footer_last_down, + button_vertical_line, + self.footer_ok, + } + local bottom_line = LineWidget:new{ + dimen = Geom:new{ w = self.item_width, h = Size.line.thick }, + background = Blitbuffer.COLOR_DARK_GRAY, + style = "solid", + } + local vertical_footer = VerticalGroup:new{ + bottom_line, + self.page_info + } + local footer = BottomContainer:new{ + dimen = self.dimen:copy(), + vertical_footer, + } + -- setup title bar + self.title_bar = SortTitleWidget:new{ + title = self.title, + width = self.item_width, + height = self.item_height, + use_top_page_count = self.use_top_page_count, + sort_page = self, + } + -- setup main content + self.item_margin = self.item_height / 8 + local line_height = self.item_height + self.item_margin + local content_height = self.dimen.h - self.title_bar:getSize().h - vertical_footer:getSize().h - padding + self.items_per_page = math.floor(content_height / line_height) + self.pages = math.ceil(#self.item_table / self.items_per_page) + self.main_content = VerticalGroup:new{} + + self:_populateItems() + + local frame_content = FrameContainer:new{ + height = self.dimen.h, + padding = padding, + bordersize = 0, + background = Blitbuffer.COLOR_WHITE, + VerticalGroup:new{ + align = "left", + self.title_bar, + self.main_content, + }, + } + local content = OverlapGroup:new{ + dimen = self.dimen:copy(), + frame_content, + footer, + } + -- assemble page + self[1] = FrameContainer:new{ + height = self.dimen.h, + padding = 0, + bordersize = 0, + background = Blitbuffer.COLOR_WHITE, + content + } +end + +function SortWidget:nextPage() + local new_page = math.min(self.show_page+1, self.pages) + if new_page > self.show_page then + self.show_page = new_page + if self.marked > 0 then + self:moveItem(self.items_per_page * (self.show_page - 1) + 1 - self.marked) + end + self:_populateItems() + end +end + +function SortWidget:prevPage() + local new_page = math.max(self.show_page-1, 1) + if new_page < self.show_page then + self.show_page = new_page + if self.marked > 0 then + self:moveItem(self.items_per_page * (self.show_page - 1) + 1 - self.marked) + end + self:_populateItems() + end +end + +function SortWidget:goToPage(page) + self.show_page = page + self:_populateItems() +end + +function SortWidget:moveItem(diff) + local move_to = self.marked + diff + if move_to > 0 and move_to <= #self.item_table then + self.show_page = math.ceil(move_to/self.items_per_page) + self:swapItems(self.marked, move_to) + self:_populateItems() + end +end + +-- make sure self.item_margin and self.item_height are set before calling this +function SortWidget:_populateItems() + self.main_content:clear() + local idx_offset = (self.show_page - 1) * self.items_per_page + local page_last + if idx_offset + self.items_per_page <= #self.item_table then + page_last = idx_offset + self.items_per_page + else + page_last = #self.item_table + end + + for idx = idx_offset + 1, page_last do + table.insert(self.main_content, VerticalSpan:new{ width = self.item_margin }) + local invert_status = false + if idx == self.marked then + invert_status = true + end + table.insert( + self.main_content, + SortItemWidget:new{ + height = self.item_height, + width = self.item_width, + text = self.item_table[idx].text, + lable = self.item_table[idx].label, + invert = invert_status, + index = idx, + show_parent = self, + } + ) + end + + self.footer_page:setText(T(_("%1/%2"), self.show_page, self.pages), self.width_widget * 22 / 100) + if self.marked > 0 then + self.footer_first_up:setText("▲", self.width_widget * 13 / 100) + self.footer_last_down:setText("▼", self.width_widget * 13 / 100) + else + self.footer_first_up:setText("◀◀", self.width_widget * 13 / 100) + self.footer_last_down:setText("▶▶", self.width_widget * 13 / 100) + end + self.footer_left:enableDisable(self.show_page > 1) + self.footer_right:enableDisable(self.show_page < self.pages) + self.footer_first_up:enableDisable(self.show_page > 1 or self.marked > 0) + self.footer_last_down:enableDisable(self.show_page < self.pages or (self.marked > 0 and self.marked < #self.item_table)) + self.footer_first_up:enableDisable(self.marked > 1) + + UIManager:setDirty(self, function() + return "ui", self.dimen + end) +end + +function SortWidget:swapItems(pos1, pos2) + if pos1 > 0 or pos2 <= #self.item_table then + local entry = self.item_table[pos1] + self.marked = pos2 + self.item_table[pos1] = self.item_table[pos2] + self.item_table[pos2] = entry + end +end + +function SortWidget:onNextPage() + self:nextPage() + return true +end + +function SortWidget:onPrevPage() + self:prevPage() + return true +end + +function SortWidget: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 + -- Allow easier closing with swipe down + self:onClose() + 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") + -- a long diagonal swipe may also be used for taking a screenshot, + -- so let it propagate + return false + end +end + +function SortWidget:onClose() + UIManager:close(self) + UIManager:setDirty(nil, "ui") + return true +end + +function SortWidget:onReturn() + UIManager:close(self) + if self.callback then self:callback() end + return true +end + +return SortWidget