From 620542b0556eade82b039a6b24a628e85a9acbd5 Mon Sep 17 00:00:00 2001 From: poire-z Date: Wed, 1 Aug 2018 18:33:52 +0200 Subject: [PATCH] Text input related fixes & enhancements (#4124) InputText: checks whether provided content can be given back unaltered, which may not be the case after it is splitted to UTF8 chars if the text is binary content. Prevent editing text if that is the case. Adds InputText and InputDialog :isEditable() and :isEdited() methods. Also accounts for the scrollbar width when measuring text to prevent it from being displayed when not needed. Also ensure a minimal size of the scrollbar thumb so it is rendered when huge text with many lines is displayed. Virtual keyboard: Hold on Backspace: delete from cursor to start of line instead of clearing all text content. --- frontend/ui/rendertext.lua | 15 +++- frontend/ui/widget/inputdialog.lua | 8 +++ frontend/ui/widget/inputtext.lua | 92 ++++++++++++++++++++++-- frontend/ui/widget/verticalscrollbar.lua | 6 +- frontend/ui/widget/virtualkeyboard.lua | 7 +- frontend/util.lua | 2 +- 6 files changed, 121 insertions(+), 9 deletions(-) diff --git a/frontend/ui/rendertext.lua b/frontend/ui/rendertext.lua index 1732048f0..2211bd4df 100644 --- a/frontend/ui/rendertext.lua +++ b/frontend/ui/rendertext.lua @@ -238,8 +238,19 @@ function RenderText:renderUtf8Text(dest_bb, x, baseline, face, text, kerning, bo end -- if pen_x < text_width if char_pads then char_idx = char_idx + 1 - pen_x = pen_x + char_pads[char_idx] -- or 0 - -- will fail if we didnt count the same number of chars, we'll see + pen_x = pen_x + (char_pads[char_idx] or 0) + -- We used to use: + -- pen_x = pen_x + char_pads[char_idx] + -- above will fail if we didnt count the same number of chars, we'll see + -- We saw, and it's pretty robust: it never failed before we tried to + -- render some binary content, which messes the utf8 sequencing: the + -- split to UTF8 is only reversible if text is valid UTF8 (or nearly UTF8). + -- TextBoxWidget did this sequencing, counted the number of chars + -- and made out 'char_pads', and gave us back the concatenated utf8 + -- chars as 'text', that we sequenced again above: we may not get the + -- same number of chars as we did previously to make char_pads. + -- We'd rather not crash (and have binary stuff displayed, even if + -- badly). The mess in char_pads is negligeable when that happens. end end diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 113ca7b88..c992eb27c 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -385,6 +385,14 @@ function InputDialog:setInputText(text) self._input_widget:setText(text) end +function InputDialog:isTextEditable() + return self._input_widget:isTextEditable() +end + +function InputDialog:isTextEdited() + return self._input_widget:isTextEdited() +end + function InputDialog:onShow() UIManager:setDirty(self, function() return "ui", self.dialog_frame.dimen diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index 371b6ef57..7a6f8ddde 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -3,8 +3,10 @@ local CheckButton = require("ui/widget/checkbutton") local Device = require("device") local FrameContainer = require("ui/widget/container/framecontainer") local Font = require("ui/font") +local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") local InputContainer = require("ui/widget/container/inputcontainer") +local Notification = require("ui/widget/notification") local ScrollTextWidget = require("ui/widget/scrolltextwidget") local Size = require("ui/size") local TextBoxWidget = require("ui/widget/textboxwidget") @@ -43,6 +45,8 @@ local InputText = InputContainer:new{ charpos = nil, -- position of the cursor, where a new char would be inserted top_line_num = nil, -- virtual_line_num of the text_widget (index of the displayed top line) is_password_type = false, -- set to true if original text_type == "password" + is_text_editable = true, -- whether text is utf8 reversible and editing won't mess content + is_text_edited = false, -- whether text has been updated } -- only use PhysicalKeyboard if the device does not have touch screen @@ -167,12 +171,47 @@ else function InputText:initEventListener() end end +function InputText:checkTextEditability() + -- The split of the 'text' string to a table of utf8 chars may not be + -- reversible to the same string, if 'text' comes from a binary file + -- (it looks like it does not necessarily need to be proper UTF8 to + -- be reversible, some text with latin1 chars is reversible). + -- As checking that may be costly, we do that only in init(), setText(), + -- and clear(). + -- When not reversible, we prevent adding and deleting chars to not + -- corrupt the original self.text. + self.is_text_editable = true + if self.text then + -- We check that the text obtained from the UTF8 split done + -- in :initTextBox(), when concatenated back to a string, matches + -- the original text. (If this turns out too expensive, we could + -- just compare their lengths) + self.is_text_editable = table.concat(self.charlist, "") == self.text + end +end + +function InputText:isTextEditable(show_warning) + if show_warning and not self.is_text_editable then + UIManager:show(Notification:new{ + text = _("Text may be binary content, and is not editable"), + timeout = 2 + }) + end + return self.is_text_editable +end + +function InputText:isTextEdited() + return self.is_text_edited +end + function InputText:init() if self.text_type == "password" then -- text_type changes from "password" to "text" when we toggle password self.is_password_type = true end self:initTextBox(self.text) + self:checkTextEditability() + self.is_text_edited = false if self.readonly ~= true then self:initKeyboard() self:initEventListener() @@ -224,7 +263,7 @@ function InputText:initTextBox(text, char_added) self.text_type = "text" self._check_button:check() end - self:setText(self:getText()) + self:setText(self:getText(), true) end, padding = self.padding, @@ -246,12 +285,18 @@ function InputText:initTextBox(text, char_added) -- If no height provided, measure the text widget height -- we would start with, and use a ScrollTextWidget with that -- height, so widget does not overflow container if we extend - -- the text and increase the number of lines + -- the text and increase the number of lines. + local text_width = self.width + if text_width then + -- Account for the scrollbar that will be used + local scroll_bar_width = ScrollTextWidget.scroll_bar_width + ScrollTextWidget.text_scroll_span + text_width = text_width - scroll_bar_width + end local text_widget = TextBoxWidget:new{ text = show_text, charlist = show_charlist, face = self.face, - width = self.width, + width = text_width, } self.height = text_widget:getTextHeight() self.scroll = true @@ -356,6 +401,9 @@ function InputText:getLineHeight() end function InputText:getKeyboardDimen() + if self.readonly then + return Geom:new{w = 0, h = 0} + end return self.keyboard.dimen end @@ -364,18 +412,47 @@ function InputText:addChars(chars) UIManager:scheduleIn(0.3, function() self.enter_callback() end) return end + if not self:isTextEditable(true) then + return + end + self.is_text_edited = true table.insert(self.charlist, self.charpos, chars) self.charpos = self.charpos + #util.splitToChars(chars) self:initTextBox(table.concat(self.charlist), true) end function InputText:delChar() + if not self:isTextEditable(true) then + return + end if self.charpos == 1 then return end self.charpos = self.charpos - 1 + self.is_text_edited = true table.remove(self.charlist, self.charpos) self:initTextBox(table.concat(self.charlist)) end +function InputText:delToStartOfLine() + if not self:isTextEditable(true) then + return + end + if self.charpos == 1 then return end + -- self.charlist[self.charpos] is the char after the cursor + if self.charlist[self.charpos-1] == "\n" then + -- If at start of line, just remove the \n and join the previous line + self.charpos = self.charpos - 1 + table.remove(self.charlist, self.charpos) + else + -- If not, remove chars until first found \n (but keeping it) + while self.charpos > 1 and self.charlist[self.charpos-1] ~= "\n" do + self.charpos = self.charpos - 1 + table.remove(self.charlist, self.charpos) + end + end + self.is_text_edited = true + self:initTextBox(table.concat(self.charlist)) +end + -- For the following cursor/scroll methods, the text_widget deals -- itself with setDirty'ing the appropriate regions function InputText:leftChar() @@ -423,16 +500,23 @@ end function InputText:clear() self.charpos = nil self.top_line_num = 1 + self.is_text_edited = true self:initTextBox("") + self:checkTextEditability() end function InputText:getText() return self.text end -function InputText:setText(text) +function InputText:setText(text, keep_edited_state) -- Keep previous charpos and top_line_num self:initTextBox(text) + if not keep_edited_state then + -- assume new text is set by caller, and we start fresh + self.is_text_edited = false + self:checkTextEditability() + end end return InputText diff --git a/frontend/ui/widget/verticalscrollbar.lua b/frontend/ui/widget/verticalscrollbar.lua index d2d661d90..120e3c0c7 100644 --- a/frontend/ui/widget/verticalscrollbar.lua +++ b/frontend/ui/widget/verticalscrollbar.lua @@ -13,6 +13,9 @@ local VerticalScrollBar = Widget:new{ bordercolor = Blitbuffer.COLOR_BLACK, radius = 0, rectcolor = Blitbuffer.COLOR_BLACK, + -- minimal height of the thumb/knob/grip (usually showing the current + -- view size and position relative to the whole scrollable height): + min_thumb_size = Size.line.thick, } function VerticalScrollBar:getSize() @@ -33,7 +36,8 @@ function VerticalScrollBar:paintTo(bb, x, y) self.bordersize, self.bordercolor, self.radius) bb:paintRect(x + self.bordersize, y + self.bordersize + self.low * self.height, self.width - 2 * self.bordersize, - (self.height - 2 * self.bordersize) * (self.high - self.low), self.rectcolor) + math.max((self.height - 2 * self.bordersize) * (self.high - self.low), self.min_thumb_size), + self.rectcolor) end return VerticalScrollBar diff --git a/frontend/ui/widget/virtualkeyboard.lua b/frontend/ui/widget/virtualkeyboard.lua index 39a74326a..c10e074a9 100644 --- a/frontend/ui/widget/virtualkeyboard.lua +++ b/frontend/ui/widget/virtualkeyboard.lua @@ -52,7 +52,7 @@ function VirtualKey:init() self.skiptap = true elseif self.label == "Backspace" then self.callback = function () self.keyboard:delChar() end - self.hold_callback = function () self.keyboard:clear() end + self.hold_callback = function () self.keyboard:delToStartOfLine() end --self.skiphold = true elseif self.label =="←" then self.callback = function() self.keyboard:leftChar() end @@ -373,6 +373,11 @@ function VirtualKeyboard:delChar() self.inputbox:delChar() end +function VirtualKeyboard:delToStartOfLine() + logger.dbg("delete to start of line") + self.inputbox:delToStartOfLine() +end + function VirtualKeyboard:leftChar() self.inputbox:leftChar() end diff --git a/frontend/util.lua b/frontend/util.lua index dd9c1bd64..f2e15c2df 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -463,7 +463,7 @@ function util.getMenuText(item) else text = item.text end - if item.sub_item_table ~= nil then + if item.sub_item_table ~= nil or item.sub_item_table_func then text = text .. " \226\150\184" end return text