diff --git a/frontend/apps/filemanager/filemanagermenu.lua b/frontend/apps/filemanager/filemanagermenu.lua index 4b8f01f21..5ccbad444 100644 --- a/frontend/apps/filemanager/filemanagermenu.lua +++ b/frontend/apps/filemanager/filemanagermenu.lua @@ -179,6 +179,16 @@ function FileManagerMenu:setUpdateItemTable() } UIManager:show(items_font) end + }, + { + text = _("Reduce font size to show more text"), + keep_menu_open = true, + checked_func = function() + return G_reader_settings:isTrue("items_multilines_show_more_text") + end, + callback = function() + G_reader_settings:flipNilOrFalse("items_multilines_show_more_text") + end } } } diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 632cd1e86..618a6bdff 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -19,7 +19,6 @@ 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 RenderText = require("ui/rendertext") local RightContainer = require("ui/widget/container/rightcontainer") local Size = require("ui/size") local TextBoxWidget = require("ui/widget/textboxwidget") @@ -174,6 +173,32 @@ function MenuItem:init() } 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{ @@ -191,6 +216,11 @@ function MenuItem:init() } } + -- 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 @@ -201,37 +231,36 @@ function MenuItem:init() 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() - -- 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) - + local available_width = self.content_width - state_button_width - text_mandatory_padding - mandatory_w local item_name - local mandatory_widget + + -- 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 - 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() -- No font size change: text will be truncated if it overflows - -- (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_base = self.content_width - state_button_width - mandatory_w - local text_max_width = text_max_width_base - text_mandatory_padding - local text_max_width_if_ellipsis = text_max_width_base - text_ellipsis_mandatory_padding item_name = TextWidget:new{ - text = self.text, + 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 > text_max_width then + 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 @@ -250,175 +279,79 @@ function MenuItem:init() } end end - else - while true do - -- Free previously made widgets to avoid memory leaks - if mandatory_widget then - mandatory_widget:free() - end - mandatory_widget = TextWidget:new { - text = mandatory, - face = Font:getFace(self.infont, self.infont_size), - bold = self.bold, - fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil, - } - local height = mandatory_widget:getSize().h - - if height < self.dimen.h - 2 * self.linesize then -- we fit ! - break - end - -- Don't go too low - if self.infont_size < 12 then - break; - else - -- If we don't fit, decrease font size - self.infont_size = self.infont_size - 1 - end - end - self.info_face = Font:getFace(self.infont, self.infont_size) - local mandatory_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.info_face, "" .. mandatory, true, self.bold).x - local max_item_height = self.dimen.h - 2 * self.linesize - local flag_fit = false - local flag_add_ellipsis = false - local num_lines, offset - local item_name_orig = nil - -- first: try to decrease number of lines in TextBoxWidget - -- this loop ends only when text fits or we have only one line of text - while true do - -- Free previously made widgets to avoid memory leaks + 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 = self.text, - face = Font:getFace(self.font, self.font_size), - width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding, + 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, } - if not item_name_orig then - -- remember original item_name - item_name_orig = item_name - offset = #item_name.charlist - end - local height = item_name:getSize().h - if height < max_item_height then -- we fit ! - flag_fit = true - break - end - flag_add_ellipsis = true - num_lines = item_name:getAllLineCount() - if num_lines == 1 then -- widget doesn't fit and we have only one line of text - break - end - -- remove last line and try again to fit - offset = item_name.vertical_string_list[num_lines].offset - 1 - -- remove ending "\n" (new line) to prevent infinity loop - if item_name.charlist[offset] == "\n" then - offset = offset - 1 - end - self.text = table.concat(item_name.charlist, "", 1, offset) + -- return true if we fit + return item_name:getSize().h <= max_item_height end - - -- second: decrease font size (we have now only one line) - while true and not flag_fit do - -- Free previously made widgets to avoid memory leaks - if item_name then + 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() - end - self.font_size = self.font_size - 1 - item_name = TextBoxWidget:new{ - text = self.text, - face = Font:getFace(self.font, self.font_size), - width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding, - alignment = "left", - bold = self.bold, - fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil, - } - local height = item_name:getSize().h - if height < max_item_height then -- we fit ! - offset = #item_name.charlist - break - end - - -- if text is too height with that really small font we don't show text at all - -- this shouldn't happen - if self.font_size <= 8 then - item_name = TextBoxWidget:new{ - text = "…", - face = Font:getFace(self.font, self.font_size), - width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding, - alignment = "left", - bold = self.bold, - fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil, - } - flag_add_ellipsis = false - break - end - end - -- add ellipsis when text was truncated - if flag_add_ellipsis then - local text_last_line - -- when lines is more than 1 we see only for last visible line - if num_lines > 1 then - local offset_prev = item_name_orig.vertical_string_list[num_lines - 1].offset - text_last_line = table.concat(item_name_orig.charlist, "", offset_prev, offset) - else - text_last_line = self.text - end - local text_size = RenderText:sizeUtf8Text(0, self.content_width, - Font:getFace(self.font, self.font_size), text_last_line, true, self.bold).x - local ellipsis_size = RenderText:sizeUtf8Text(0, self.content_width, - Font:getFace(self.font, self.font_size), "…", true, self.bold).x - - local text_size_increase = text_size - local max_offset = #item_name_orig.charlist - -- try to add chars to better align - while item_name.width > text_size_increase + ellipsis_size and offset < max_offset - and item_name_orig.charlist[offset] ~= "\n" do - text_size_increase = text_size_increase + item_name_orig:getCharWidth(offset + 1) - if text_size_increase + ellipsis_size < item_name.width then - -- add one char to text - offset = offset + 1 - end - end - -- remove chars when text is too long - while item_name.width <= text_size + ellipsis_size do - text_size = text_size - item_name:getCharWidth(offset) - -- remove one char from text - offset = offset - 1 - end - if offset == max_offset then - -- when finally after manipulation we have all original text we don't need to add ellipsis - self.text = table.concat(item_name_orig.charlist, "", 1, offset) + item_name.height = max_item_height + item_name.height_adjust = true + item_name.height_overflow_show_ellipsis = true + item_name:init() else - -- remove ending '\n' (new line) to prevent increase number of lines - if item_name_orig.charlist[offset] == "\n" then - offset = offset - 1 + -- 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 - -- add ellipsis to show that text was truncated - self.text = table.concat(item_name_orig.charlist, "", 1, offset) .. "…" - end - - if item_name then - item_name:free() - end - if item_name_orig then - item_name_orig:free() end - --final item_name that fits - item_name = TextBoxWidget:new { - text = self.text, - face = Font:getFace(self.font, self.font_size), - width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding, - alignment = "left", - bold = self.bold, - fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil, - } end - self.face = Font:getFace(self.font, self.font_size) + 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{ diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index e38d76cba..7fa8a4b03 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -43,6 +43,8 @@ local TextBoxWidget = InputContainer:new{ fgcolor = Blitbuffer.COLOR_BLACK, width = Screen:scaleBySize(400), -- in pixels height = nil, -- nil value indicates unscrollable text widget + height_adjust = false, -- if true, reduce height to a multiple of line_height (for nicer centering) + height_overflow_show_ellipsis = false, -- if height overflow, append ellipsis to last shown line top_line_num = nil, -- original virtual_line_num to scroll to charpos = nil, -- idx of char to draw the cursor on its left (can exceed #charlist by 1) @@ -131,6 +133,15 @@ function TextBoxWidget:init() self.text_height = self.lines_per_page * self.line_height_px self.virtual_line_num = 1 else + if self.height_overflow_show_ellipsis and #self.vertical_string_list > self.lines_per_page then + self.line_with_ellipsis = self.lines_per_page + end + if self.height_adjust then + self.height = self.text_height + if #self.vertical_string_list < self.lines_per_page then + self.height = #self.vertical_string_list * self.line_height_px + end + end -- Show the previous displayed area in case of re-init (focus/unfocus) -- InputText may have re-created us, while providing the previous charlist, -- charpos and top_line_num. @@ -448,7 +459,8 @@ function TextBoxWidget:_shapeLine(line) -- Get glyphs, shaped and possibly substituted by Harfbuzz and re-ordered by FriBiDi. -- We'll add to 'line' this table of glyphs, with some additional -- computed x and advance keys - local xshaping = self._xtext:shapeLine(line.offset, line.end_offset) + local xshaping = self._xtext:shapeLine(line.offset, line.end_offset, + line.idx_to_substitute_with_ellipsis) -- logger.dbg(xshaping) -- We get an array of tables looking like this: -- [1] = { @@ -594,6 +606,23 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx) if self.use_xtext then for i = start_row_idx, end_row_idx do local line = self.vertical_string_list[i] + if self.line_with_ellipsis and i == self.line_with_ellipsis and not line.ellipsis_added then + -- Requested to add an ellipsis on this line + local ellipsis_width = RenderText:getEllipsisWidth(self.face) + -- no bold: xtext does synthetized bold with normal metrics + if line.width + ellipsis_width > line.targeted_width then + -- The ellipsis would overflow: we need to re-makeLine() + -- this line with a smaller targeted_width + line = self._xtext:makeLine(line.offset, line.targeted_width - ellipsis_width) + self.vertical_string_list[i] = line -- replace the former one + end + if line.end_offset and line.end_offset < #self._xtext then + -- We'll have shapeLine add the ellipsis to the returned glyphs + line.end_offset = line.end_offset + 1 + line.idx_to_substitute_with_ellipsis = line.end_offset + end + line.ellipsis_added = true -- No need to redo it next time + end self:_shapeLine(line) if line.xglyphs then -- non-empty line for __, xglyph in ipairs(line.xglyphs) do @@ -628,7 +657,19 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx) end -- Note: we use kerning=true in all RenderText calls -- (But kerning should probably not be used with monospaced fonts.) - RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, self:_getLineText(line), true, self.bold, self.fgcolor, nil, self:_getLinePads(line)) + local line_text = self:_getLineText(line) + if self.line_with_ellipsis and i == self.line_with_ellipsis then + -- Requested to add an ellipsis on this line + local ellipsis_width = RenderText:getEllipsisWidth(self.face, self.bold) + if line.width + ellipsis_width > self.width then + -- We could try to find the last break point (space, CJK) to + -- truncate there and add the ellipsis, but well... + line_text = RenderText:truncateTextByWidth(line_text, self.face, self.width, true, self.bold) + else + line_text = line_text .. "…" + end + end + RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, line_text, true, self.bold, self.fgcolor, nil, self:_getLinePads(line)) y = y + self.line_height_px end @@ -813,6 +854,25 @@ function TextBoxWidget:getVisibleHeightRatios() return low, high end +-- Helper function to be used before intanstiating a TextBoxWidget instance +function TextBoxWidget:getFontSizeToFitHeight(height_px, nb_lines, line_height_em) + -- Get a font size that would fit nb_lines in height_px. + -- A font with the returned size should then be provided + -- to TextBoxWidget:new() (as well as the line_height_em given + -- here, as the line_height= property, if not the default). + if not nb_lines then + nb_lines = 1 -- default to 1 line + end + if not line_height_em then + line_height_em = self.line_height -- (TextBoxWidget default above: 0.3) + end + -- We do the revert of what's done in :init(): + -- self.line_height_px = Math.round( (1 + self.line_height) * self.face.size ) + local font_size = height_px / nb_lines / (1 + line_height_em) + font_size = font_size * 1000000 / Screen:scaleBySize(1000000) -- invert scaleBySize + return math.floor(font_size) +end + function TextBoxWidget:getCharPos() -- returns virtual_line_num too return self.charpos, self.virtual_line_num