diff --git a/frontend/apps/reader/modules/readerbookmark.lua b/frontend/apps/reader/modules/readerbookmark.lua index f0414b2e7..74223155d 100644 --- a/frontend/apps/reader/modules/readerbookmark.lua +++ b/frontend/apps/reader/modules/readerbookmark.lua @@ -450,6 +450,9 @@ function ReaderBookmark:renameBookmark(item, from_highlight) title = _("Rename bookmark"), input = item.text, input_type = "text", + allow_newline = true, + cursor_at_end = false, + add_scroll_buttons = true, buttons = { { { diff --git a/frontend/ui/gesturerange.lua b/frontend/ui/gesturerange.lua index e03fcdb9a..43d47345c 100644 --- a/frontend/ui/gesturerange.lua +++ b/frontend/ui/gesturerange.lua @@ -35,7 +35,7 @@ function GestureRange:match(gs) else range = self.range end - if not range:contains(gs.pos) then + if not range or not range:contains(gs.pos) then return false end end diff --git a/frontend/ui/size.lua b/frontend/ui/size.lua index cebf246fa..cb3f22466 100644 --- a/frontend/ui/size.lua +++ b/frontend/ui/size.lua @@ -34,6 +34,7 @@ local Size = { thin = Screen:scaleBySize(0.5), button = Screen:scaleBySize(1.5), window = Screen:scaleBySize(1.5), + inputtext = Screen:scaleBySize(2), }, margin = { default = Screen:scaleBySize(5), diff --git a/frontend/ui/widget/bookstatuswidget.lua b/frontend/ui/widget/bookstatuswidget.lua index bc94d8ab8..6a1685e7d 100644 --- a/frontend/ui/widget/bookstatuswidget.lua +++ b/frontend/ui/widget/bookstatuswidget.lua @@ -555,6 +555,7 @@ function BookStatusWidget:onSwitchFocus(inputbox) input_hint = "", input_type = "text", scroll = true, + allow_newline = true, text_height = Screen:scaleBySize(150), buttons = { { diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 103792235..113ca7b88 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -37,6 +37,15 @@ Example: UIManager:show(sample_input) sample_input:onShowKeyboard() +To get a full screen text editor, use: + fullscreen = true, -- no need to provide any height and width + condensed = true, + allow_newline = true, + cursor_at_end = false, + -- and one of these: + add_scroll_buttons = true, + add_nav_bar = true, + If it would take the user more than half a minute to recover from a mistake, a "Cancel" button must be added to the dialog. The cancellation button should be kept on the left and the button executing the action on the right. @@ -76,6 +85,19 @@ local InputDialog = InputContainer:new{ buttons = nil, input_type = nil, enter_callback = nil, + allow_newline = false, -- allow entering new lines (this disables any enter_callback) + cursor_at_end = true, -- starts with cursor at end of text, ready for appending + fullscreen = false, -- adjust to full screen minus keyboard + condensed = false, -- true will prevent adding air and balance between elements + add_scroll_buttons = false, -- add scroll Up/Down buttons to first row of buttons + add_nav_bar = false, -- append a row of page navigation buttons + -- note that the text widget can be scrolled with Swipe North/South even when no button + + -- movable = true, -- set to false if movable gestures conflicts with subwidgets gestures + -- for now, too much conflicts between InputText and MovableContainer, and + -- there's the keyboard to exclude from move area (the InputDialog could + -- be moved under the keyboard, and the user would be locked) + movable = false, width = nil, @@ -88,13 +110,29 @@ local InputDialog = InputContainer:new{ title_padding = Size.padding.default, title_margin = Size.margin.title, - input_padding = Size.padding.large, + desc_padding = Size.padding.default, -- Use the same as title for their + desc_margin = Size.margin.title, -- texts to be visually aligned + input_padding = Size.padding.default, input_margin = Size.margin.default, button_padding = Size.padding.default, + border_size = Size.border.window, } function InputDialog:init() - self.width = self.width or Screen:getWidth() * 0.8 + if self.fullscreen then + self.movable = false + self.border_size = 0 + self.width = Screen:getWidth() - 2*self.border_size + else + self.width = self.width or Screen:getWidth() * 0.8 + end + if self.condensed then + self.text_width = self.width - 2*(self.border_size + self.input_padding + self.input_margin) + else + self.text_width = self.text_width or self.width * 0.9 + end + + -- Title & description local title_width = RenderText:sizeUtf8Text(0, self.width, self.title_face, self.title, true).x if title_width > self.width then @@ -114,28 +152,159 @@ function InputDialog:init() width = self.width, } } - + self.title_bar = LineWidget:new{ + dimen = Geom:new{ + w = self.width, + h = Size.line.thick, + } + } if self.description then - self.description = FrameContainer:new{ - padding = self.title_padding, - margin = self.title_margin, + self.description_widget = FrameContainer:new{ + padding = self.desc_padding, + margin = self.desc_margin, bordersize = 0, TextBoxWidget:new{ text = self.description, face = self.description_face, - width = self.width - 2*self.title_padding - 2*self.title_margin, + width = self.width - 2*self.desc_padding - 2*self.desc_margin, } } else - self.description = VerticalSpan:new{ width = self.title_margin + self.title_padding } + self.description_widget = VerticalSpan:new{ width = 0 } + end + + -- Vertical spaces added before and after InputText + -- (these will be adjusted later to center the input text if needed) + local vspan_before_input_text = VerticalSpan:new{ width = 0 } + local vspan_after_input_text = VerticalSpan:new{ width = 0 } + -- We add the same vertical space used under description after the input widget + -- (can be disabled by setting condensed=true) + if not self.condensed then + local desc_pad_height = self.desc_margin + self.desc_padding + if self.description then + vspan_before_input_text.width = 0 -- already provided by description_widget + vspan_after_input_text.width = desc_pad_height + else + vspan_before_input_text.width = desc_pad_height + vspan_after_input_text.width = desc_pad_height + end end + -- Buttons + if self.add_nav_bar then + if not self.buttons then + self.buttons = {} + end + local nav_bar = {} + table.insert(self.buttons, nav_bar) + table.insert(nav_bar, { + text = "⇱", + callback = function() + self._input_widget:scrollToTop() + end, + }) + table.insert(nav_bar, { + text = "⇲", + callback = function() + self._input_widget:scrollToBottom() + end, + }) + table.insert(nav_bar, { + text = "△", + callback = function() + self._input_widget:scrollUp() + end, + }) + table.insert(nav_bar, { + text = "▽", + callback = function() + self._input_widget:scrollDown() + end, + }) + elseif self.add_scroll_buttons then + if not self.buttons then + self.buttons = {{}} + end + -- Add them to the end of first row + table.insert(self.buttons[1], { + text = "△", + callback = function() + self._input_widget:scrollUp() + end, + }) + table.insert(self.buttons[1], { + text = "▽", + callback = function() + self._input_widget:scrollDown() + end, + }) + end + self.button_table = ButtonTable:new{ + width = self.width - 2*self.button_padding, + button_font_face = "cfont", + button_font_size = 20, + buttons = self.buttons, + zero_sep = true, + show_parent = self, + } + local buttons_container = CenterContainer:new{ + dimen = Geom:new{ + w = self.width, + h = self.button_table:getSize().h, + }, + self.button_table, + } + + -- InputText + if not self.text_height or self.fullscreen then + -- We need to find the best height to avoid screen overflow + -- Create a dummy input widget to get some metrics + local input_widget = InputText:new{ + text = self.fullscreen and "-" or self.input, + face = self.input_face, + width = self.text_width, + padding = self.input_padding, + margin = self.input_margin, + } + local text_height = input_widget:getTextHeight() + local line_height = input_widget:getLineHeight() + local input_pad_height = input_widget:getSize().h - text_height + local keyboard_height = input_widget:getKeyboardDimen().h + input_widget:free() + -- Find out available height + local available_height = Screen:getHeight() + - 2*self.border_size + - self.title:getSize().h + - self.title_bar:getSize().h + - self.description_widget:getSize().h + - vspan_before_input_text:getSize().h + - input_pad_height + - vspan_after_input_text:getSize().h + - buttons_container:getSize().h + - keyboard_height + if self.fullscreen or text_height > available_height then + -- Don't leave unusable space in the text widget, as the user could think + -- it's an empty line: move that space in pads after and below (for centering) + self.text_height = math.floor(available_height / line_height) * line_height + local pad_height = available_height - self.text_height + local pad_before = math.ceil(pad_height / 2) + local pad_after = pad_height - pad_before + vspan_before_input_text.width = vspan_before_input_text.width + pad_before + vspan_after_input_text.width = vspan_after_input_text.width + pad_after + self.cursor_at_end = false -- stay at start if overflowed + else + -- Don't leave unusable space in the text widget + self.text_height = text_height + end + end self._input_widget = InputText:new{ text = self.input, hint = self.input_hint, face = self.input_face, - width = self.text_width or self.width * 0.9, + width = self.text_width, height = self.text_height or nil, + padding = self.input_padding, + margin = self.input_margin, input_type = self.input_type, text_type = self.text_type, enter_callback = self.enter_callback or function() @@ -148,68 +317,54 @@ function InputDialog:init() end end end, - scroll = false, + scroll = true, + cursor_at_end = self.cursor_at_end, parent = self, } - self.button_table = ButtonTable:new{ - width = self.width - 2*self.button_padding, - button_font_face = "cfont", - button_font_size = 20, - buttons = self.buttons, - zero_sep = true, - show_parent = self, - } - - self.title_bar = LineWidget:new{ - dimen = Geom:new{ - w = self.width, - h = Size.line.thick, - } - } + if self.allow_newline then -- remove any enter_callback + self._input_widget.enter_callback = nil + end + if Device:hasKeys() then + --little hack to piggyback on the layout of the button_table to handle the new InputText + table.insert(self.button_table.layout, 1, {self._input_widget}) + end + -- Final widget self.dialog_frame = FrameContainer:new{ - radius = Size.radius.window, + radius = self.fullscreen and 0 or Size.radius.window, padding = 0, margin = 0, + bordersize = self.border_size, background = Blitbuffer.COLOR_WHITE, VerticalGroup:new{ align = "left", self.title, self.title_bar, - self.description, - -- input + self.description_widget, + vspan_before_input_text, CenterContainer:new{ dimen = Geom:new{ - w = self.title_bar:getSize().w, + w = self.width, h = self._input_widget:getSize().h, }, self._input_widget, }, - -- Add same vertical space after than before InputText - VerticalSpan:new{ width = self.title_margin + self.title_padding }, - -- buttons - CenterContainer:new{ - dimen = Geom:new{ - w = self.title_bar:getSize().w, - h = self.button_table:getSize().h, - }, - self.button_table, - } + vspan_after_input_text, + buttons_container, } } - if Device:hasKeys() then - --little hack to piggyback on the layout of the button_table to handle the new InputText - table.insert(self.button_table.layout, 1, {self._input_widget}) + local frame = self.dialog_frame + if self.movable then + frame = MovableContainer:new{ + self.dialog_frame, + } end - self[1] = CenterContainer:new{ dimen = Geom:new{ w = Screen:getWidth(), h = Screen:getHeight() - self._input_widget:getKeyboardDimen().h, }, - MovableContainer:new{ - self.dialog_frame, - }, + frame } end diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index 9892763f8..371b6ef57 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -6,6 +6,7 @@ local Font = require("ui/font") local GestureRange = require("ui/gesturerange") local InputContainer = require("ui/widget/container/inputcontainer") local ScrollTextWidget = require("ui/widget/scrolltextwidget") +local Size = require("ui/size") local TextBoxWidget = require("ui/widget/textboxwidget") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") @@ -18,24 +19,30 @@ local Keyboard local InputText = InputContainer:new{ text = "", hint = "demo hint", - charlist = nil, -- table to store input string - charpos = nil, -- position to insert a new char, or the position of the cursor - input_type = nil, - text_type = nil, - text_widget = nil, -- Text Widget for cursor movement + input_type = nil, -- "number" or anything else + text_type = nil, -- "password" or anything else show_password_toggle = true, + cursor_at_end = true, -- starts with cursor at end of text, ready for appending + scroll = false, -- whether to allow scrolling (will be set to true if no height provided) + focused = true, + parent = nil, -- parent dialog that will be set dirty width = nil, - height = nil, - face = Font:getFace("smallinfofont"), + height = nil, -- when nil, will be set to original text height (possibly + -- less if screen would be overflowed) and made scrollable to + -- not overflow if some text is appended and add new lines - padding = Screen:scaleBySize(5), - margin = Screen:scaleBySize(5), - bordersize = Screen:scaleBySize(2), + face = Font:getFace("smallinfofont"), + padding = Size.padding.default, + margin = Size.margin.default, + bordersize = Size.border.inputtext, - parent = nil, -- parent dialog that will be set dirty - scroll = false, - focused = true, + -- for internal use + text_widget = nil, -- Text Widget for cursor movement, possibly a ScrollTextWidget + charlist = nil, -- table of individual chars from input string + 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" } -- only use PhysicalKeyboard if the device does not have touch screen @@ -56,39 +63,85 @@ if Device.isTouchDevice() or Device.hasDPad() then range = self.dimen } }, + SwipeTextBox = { + GestureRange:new{ + ges = "swipe", + range = self.dimen + } + }, + -- These are just to stop propagation of the event to + -- parents in case there's a MovableContainer among them + -- Commented for now, as this needs work + -- HoldPanTextBox = { + -- GestureRange:new{ ges = "hold_pan", range = self.dimen } + -- }, + -- HoldReleaseTextBox = { + -- GestureRange:new{ ges = "hold_release", range = self.dimen } + -- }, + -- PanTextBox = { + -- GestureRange:new{ ges = "pan", range = self.dimen } + -- }, + -- PanReleaseTextBox = { + -- GestureRange:new{ ges = "pan_release", range = self.dimen } + -- }, + -- TouchTextBox = { + -- GestureRange:new{ ges = "touch", range = self.dimen } + -- }, } end + -- For MovableContainer to work fully, some of these should + -- do more check before disabling the event or not + -- Commented for now, as this needs work + -- local function _disableEvent() return true end + -- InputText.onHoldPanTextBox = _disableEvent + -- InputText.onHoldReleaseTextBox = _disableEvent + -- InputText.onPanTextBox = _disableEvent + -- InputText.onPanReleaseTextBox = _disableEvent + -- InputText.onTouchTextBox = _disableEvent + function InputText:onTapTextBox(arg, ges) if self.parent.onSwitchFocus then self.parent:onSwitchFocus(self) end - local x = ges.pos.x - self._frame_textwidget.dimen.x - self.bordersize - self.padding - local y = ges.pos.y - self._frame_textwidget.dimen.y - self.bordersize - self.padding - if x > 0 and y > 0 then - self.charpos = self.text_widget:moveCursor(x, y) - UIManager:setDirty(self.parent, function() - return "ui", self.dimen - end) - end + local textwidget_offset = self.margin + self.bordersize + self.padding + local x = ges.pos.x - self._frame_textwidget.dimen.x - textwidget_offset + local y = ges.pos.y - self._frame_textwidget.dimen.y - textwidget_offset + self.text_widget:moveCursorToXY(x, y, true) -- restrict_to_view=true + self.charpos, self.top_line_num = self.text_widget:getCharPos() + return true end function InputText:onHoldTextBox(arg, ges) if self.parent.onSwitchFocus then self.parent:onSwitchFocus(self) end - local x = ges.pos.x - self._frame_textwidget.dimen.x - self.bordersize - self.padding - local y = ges.pos.y - self._frame_textwidget.dimen.y - self.bordersize - self.padding - if x > 0 and y > 0 then - self.charpos = self.text_widget:moveCursor(x, y) - if Device:hasClipboard() and Device.input.hasClipboardText() then - self:addChars(Device.input.getClipboardText()) - end - UIManager:setDirty(self.parent, function() - return "ui", self.dimen - end) + local textwidget_offset = self.margin + self.bordersize + self.padding + local x = ges.pos.x - self._frame_textwidget.dimen.x - textwidget_offset + local y = ges.pos.y - self._frame_textwidget.dimen.y - textwidget_offset + self.text_widget:moveCursorToXY(x, y, true) -- restrict_to_view=true + self.charpos, self.top_line_num = self.text_widget:getCharPos() + if Device:hasClipboard() and Device.input.hasClipboardText() then + self:addChars(Device.input.getClipboardText()) end + return true end + + function InputText:onSwipeTextBox(arg, ges) + -- Allow refreshing the widget (actually, the screen) with the classic + -- Diagonal Swipe, as we're only using the quick "ui" mode while editing + if ges.direction == "northeast" or ges.direction == "northwest" + or ges.direction == "southeast" or ges.direction == "southwest" then + if self.refresh_callback then self.refresh_callback() end + -- Trigger a full-screen HQ flashing refresh so + -- the keyboard can also be fully redrawn + UIManager:setDirty(nil, "full") + end + -- Let it propagate in any case (a long diagonal swipe may also be + -- used for taking a screenshot) + return false + end + end if Device.hasKeys() then if not InputText.initEventListener then @@ -115,6 +168,10 @@ else 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) if self.readonly ~= true then self:initKeyboard() @@ -122,11 +179,11 @@ function InputText:init() end end -function InputText:initTextBox(text, char_added, is_password_type) +-- This will be called when we add or del chars, as we need to recreate +-- the text widget to have the new text splittted into possibly different +-- lines than before +function InputText:initTextBox(text, char_added) self.text = text - if self.text_type == "password" then - is_password_type = true - end local fgcolor local show_charlist local show_text = text @@ -147,11 +204,16 @@ function InputText:initTextBox(text, char_added, is_password_type) end end self.charlist = util.splitToChars(text) + -- keep previous cursor position if charpos not nil if self.charpos == nil then - self.charpos = #self.charlist + 1 + if self.cursor_at_end then + self.charpos = #self.charlist + 1 + else + self.charpos = 1 + end end end - if is_password_type and self.show_password_toggle then + if self.is_password_type and self.show_password_toggle then self._check_button = self._check_button or CheckButton:new{ text = _("Show password"), callback = function() @@ -162,12 +224,9 @@ function InputText:initTextBox(text, char_added, is_password_type) self.text_type = "text" self._check_button:check() end - self:setText(self:getText(), is_password_type) + self:setText(self:getText()) end, - width = self.width, - height = self.height, - padding = self.padding, margin = self.margin, bordersize = self.bordersize, @@ -182,11 +241,28 @@ function InputText:initTextBox(text, char_added, is_password_type) self._password_toggle = nil end show_charlist = util.splitToChars(show_text) + + if not self.height then + -- 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 + local text_widget = TextBoxWidget:new{ + text = show_text, + charlist = show_charlist, + face = self.face, + width = self.width, + } + self.height = text_widget:getTextHeight() + self.scroll = true + text_widget:free() + end if self.scroll then self.text_widget = ScrollTextWidget:new{ text = show_text, charlist = show_charlist, charpos = self.charpos, + top_line_num = self.top_line_num, editable = self.focused, face = self.face, fgcolor = fgcolor, @@ -199,13 +275,18 @@ function InputText:initTextBox(text, char_added, is_password_type) text = show_text, charlist = show_charlist, charpos = self.charpos, + top_line_num = self.top_line_num, editable = self.focused, face = self.face, fgcolor = fgcolor, width = self.width, height = self.height, + dialog = self.parent, } end + -- Get back possibly modified charpos and virtual_line_num + self.charpos, self.top_line_num = self.text_widget:getCharPos() + self._frame_textwidget = FrameContainer:new{ bordersize = self.bordersize, padding = self.padding, @@ -266,17 +347,25 @@ function InputText:onCloseKeyboard() UIManager:close(self.keyboard) end +function InputText:getTextHeight() + return self.text_widget:getTextHeight() +end + +function InputText:getLineHeight() + return self.text_widget:getLineHeight() +end + function InputText:getKeyboardDimen() return self.keyboard.dimen end -function InputText:addChars(char) - if self.enter_callback and char == '\n' then +function InputText:addChars(chars) + if self.enter_callback and chars == "\n" then UIManager:scheduleIn(0.3, function() self.enter_callback() end) return end - table.insert(self.charlist, self.charpos, char) - self.charpos = self.charpos + #util.splitToChars(char) + table.insert(self.charlist, self.charpos, chars) + self.charpos = self.charpos + #util.splitToChars(chars) self:initTextBox(table.concat(self.charlist), true) end @@ -287,48 +376,63 @@ function InputText:delChar() 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() if self.charpos == 1 then return end - self.charpos = self.charpos -1 - self:initTextBox(table.concat(self.charlist)) + self.text_widget:moveCursorLeft() + self.charpos, self.top_line_num = self.text_widget:getCharPos() end function InputText:rightChar() - if self.charpos > #table.concat(self.charlist) then return end - self.charpos = self.charpos +1 - self:initTextBox(table.concat(self.charlist)) + if self.charpos > #self.charlist then return end + self.text_widget:moveCursorRight() + self.charpos, self.top_line_num = self.text_widget:getCharPos() end function InputText:upLine() - if self.text_widget.moveCursorUp then - self.charpos = self.text_widget:moveCursorUp() - end + self.text_widget:moveCursorUp() + self.charpos, self.top_line_num = self.text_widget:getCharPos() end function InputText:downLine() - if self.text_widget.moveCursorDown then - self.charpos = self.text_widget:moveCursorDown() - end + self.text_widget:moveCursorDown() + self.charpos, self.top_line_num = self.text_widget:getCharPos() +end + +function InputText:scrollDown() + self.text_widget:scrollDown() + self.charpos, self.top_line_num = self.text_widget:getCharPos() +end + +function InputText:scrollUp() + self.text_widget:scrollUp() + self.charpos, self.top_line_num = self.text_widget:getCharPos() +end + +function InputText:scrollToTop() + self.text_widget:scrollToTop() + self.charpos, self.top_line_num = self.text_widget:getCharPos() +end + +function InputText:scrollToBottom() + self.text_widget:scrollToBottom() + self.charpos, self.top_line_num = self.text_widget:getCharPos() end function InputText:clear() self.charpos = nil + self.top_line_num = 1 self:initTextBox("") - UIManager:setDirty(self.parent, function() - return "ui", self[1][1].dimen - end) end function InputText:getText() return self.text end -function InputText:setText(text, is_password_type) - self.charpos = nil - self:initTextBox(text, nil, is_password_type) - UIManager:setDirty(self.parent, function() - return "partial", self[1].dimen - end) +function InputText:setText(text) + -- Keep previous charpos and top_line_num + self:initTextBox(text) end return InputText diff --git a/frontend/ui/widget/logindialog.lua b/frontend/ui/widget/logindialog.lua index a1b60c40b..0f9d2e633 100644 --- a/frontend/ui/widget/logindialog.lua +++ b/frontend/ui/widget/logindialog.lua @@ -102,6 +102,7 @@ function LoginDialog:onSwitchFocus(inputbox) -- unfocus current inputbox self._input_widget:unfocus() self._input_widget:onCloseKeyboard() + UIManager:close(self) -- focus new inputbox self._input_widget = inputbox diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index c68fba57f..9dc174944 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -280,7 +280,7 @@ function MenuItem:init() local removed_char_width= 0 while removed_char_width < ellipsis_size do -- the width of each char has already been calculated by TextBoxWidget - removed_char_width = removed_char_width + item_name:geCharWidth(offset) + removed_char_width = removed_char_width + item_name:getCharWidth(offset) offset = offset - 1 end self.text = table.concat(item_name.charlist, '', 1, offset) .. "…" diff --git a/frontend/ui/widget/pathchooser.lua b/frontend/ui/widget/pathchooser.lua index 6f7694486..98d27c081 100644 --- a/frontend/ui/widget/pathchooser.lua +++ b/frontend/ui/widget/pathchooser.lua @@ -8,6 +8,7 @@ local PathChooser = FileChooser:extend{ title = _("Choose Path"), no_title = false, is_popout = false, + covers_fullscreen = true, -- set it to false if you set is_popout = true is_borderless = true, show_filesize = false, file_filter = function() return false end, -- filter out regular files diff --git a/frontend/ui/widget/scrolltextwidget.lua b/frontend/ui/widget/scrolltextwidget.lua index 2a23223e2..e1f98c3b2 100644 --- a/frontend/ui/widget/scrolltextwidget.lua +++ b/frontend/ui/widget/scrolltextwidget.lua @@ -19,6 +19,7 @@ local ScrollTextWidget = InputContainer:new{ text = nil, charlist = nil, charpos = nil, + top_line_num = nil, editable = false, justified = false, face = nil, @@ -36,6 +37,8 @@ function ScrollTextWidget:init() text = self.text, charlist = self.charlist, charpos = self.charpos, + top_line_num = self.top_line_num, + dialog = self.dialog, editable = self.editable, justified = self.justified, face = self.face, @@ -52,9 +55,10 @@ function ScrollTextWidget:init() low = 0, high = visible_line_count / total_line_count, width = self.scroll_bar_width, - height = self.height, + height = self.text_widget:getTextHeight(), } - local horizontal_group = HorizontalGroup:new{} + self:updateScrollBar() + local horizontal_group = HorizontalGroup:new{ align = "top" } table.insert(horizontal_group, self.text_widget) table.insert(horizontal_group, HorizontalSpan:new{width=self.text_scroll_span}) table.insert(horizontal_group, self.v_scroll_bar) @@ -92,38 +96,93 @@ function ScrollTextWidget:focus() self.text_widget:focus() end -function ScrollTextWidget:moveCursor(x, y) - return self.text_widget:moveCursor(x, y) +function ScrollTextWidget:getTextHeight() + return self.text_widget:getTextHeight() +end + +function ScrollTextWidget:getLineHeight() + return self.text_widget:getLineHeight() +end + +function ScrollTextWidget:getCharPos() + return self.text_widget:getCharPos() +end + +function ScrollTextWidget:updateScrollBar() + local low, high = self.text_widget:getVisibleHeightRatios() + if low ~= self.prev_low or high ~= self.prev_high then + self.prev_low = low + self.prev_high = high + self.v_scroll_bar:set(low, high) + UIManager:setDirty(self.dialog, function() + return "partial", self.dimen + end) + end +end + +function ScrollTextWidget:moveCursorToCharPos(charpos) + self.text_widget:moveCursorToCharPos(charpos) + self:updateScrollBar() +end + +function ScrollTextWidget:moveCursorToXY(x, y, no_overflow) + self.text_widget:moveCursorToXY(x, y, no_overflow) + self:updateScrollBar() +end + +function ScrollTextWidget:moveCursorLeft() + self.text_widget:moveCursorLeft(); + self:updateScrollBar() +end + +function ScrollTextWidget:moveCursorRight() + self.text_widget:moveCursorRight(); + self:updateScrollBar() end function ScrollTextWidget:moveCursorUp() - return self.text_widget:moveCursorUp(); + self.text_widget:moveCursorUp(); + self:updateScrollBar() end function ScrollTextWidget:moveCursorDown() - return self.text_widget:moveCursorDown(); + self.text_widget:moveCursorDown(); + self:updateScrollBar() +end + +function ScrollTextWidget:scrollDown() + self.text_widget:scrollDown(); + self:updateScrollBar() +end + +function ScrollTextWidget:scrollUp() + self.text_widget:scrollUp(); + self:updateScrollBar() +end + +function ScrollTextWidget:scrollToTop() + self.text_widget:scrollToTop(); + self:updateScrollBar() +end + +function ScrollTextWidget:scrollToBottom() + self.text_widget:scrollToBottom(); + self:updateScrollBar() end function ScrollTextWidget:scrollText(direction) if direction == 0 then return end - local low, high if direction > 0 then - low, high = self.text_widget:scrollDown() + self.text_widget:scrollDown() else - low, high = self.text_widget:scrollUp() + self.text_widget:scrollUp() end - self.v_scroll_bar:set(low, high) - UIManager:setDirty(self.dialog, function() - return "partial", self.dimen - end) + self:updateScrollBar() end function ScrollTextWidget:scrollToRatio(ratio) - local low, high = self.text_widget:scrollToRatio(ratio) - self.v_scroll_bar:set(low, high) - UIManager:setDirty(self.dialog, function() - return "partial", self.dimen - end) + self.text_widget:scrollToRatio(ratio) + self:updateScrollBar() end function ScrollTextWidget:onScrollText(arg, ges) @@ -139,6 +198,10 @@ function ScrollTextWidget:onScrollText(arg, ges) end function ScrollTextWidget:onTapScrollText(arg, ges) + if self.editable then + -- Tap is used to position cursor + return false + end -- same tests as done in TextBoxWidget:scrollUp/Down if ges.pos.x < Screen:getWidth()/2 then if self.text_widget.virtual_line_num > 1 then diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index efe6c27f8..82be75258 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -33,23 +33,31 @@ local Screen = require("device").screen local TextBoxWidget = InputContainer:new{ text = nil, - charpos = nil, - charlist = nil, -- idx => char - char_width = nil, -- char => width - idx_pad = nil, -- idx => pad for char at idx, if non zero - vertical_string_list = nil, editable = false, -- Editable flag for whether drawing the cursor or not. justified = false, -- Should text be justified (spaces widened to fill width) alignment = "left", -- or "center", "right" - cursor_line = nil, -- LineWidget to draw the vertical cursor. + dialog = nil, -- parent dialog that will be set dirty face = nil, bold = nil, line_height = 0.3, -- in em fgcolor = Blitbuffer.COLOR_BLACK, width = Screen:scaleBySize(400), -- in pixels height = nil, -- nil value indicates unscrollable text widget - virtual_line_num = 1, -- used by scroll bar + 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) + + -- for internal use + charlist = nil, -- idx => char + char_width = nil, -- char => width + idx_pad = nil, -- idx => pad for char at idx, if non zero + vertical_string_list = nil, + virtual_line_num = 1, -- index of the top displayed line + line_height_px = nil, -- height of a line in px + lines_per_page = nil, -- number of visible lines + text_height = nil, -- adjusted height to visible text (lines_per_page*line_height_px) + cursor_line = nil, -- LineWidget to draw the vertical cursor. _bb = nil, + -- We can provide a list of images: each image will be displayed on each -- scrolled page, in its top right corner (if more images than pages, remaining -- images will not be displayed at all - if more pages than images, remaining @@ -61,7 +69,7 @@ local TextBoxWidget = InputContainer:new{ -- optional: -- hi_width same as previous for a high-resolution version of the -- hi_height image, to be displayed by ImageViewer when Hold on - -- hi_bb the low-resolution image + -- hi_bb blitbuffer of high-resolution image -- title ImageViewer title -- caption ImageViewer caption -- @@ -77,28 +85,46 @@ local TextBoxWidget = InputContainer:new{ } function TextBoxWidget:init() - self.line_height_px = (1 + self.line_height) * self.face.size + self.line_height_px = Math.round( (1 + self.line_height) * self.face.size ) self.cursor_line = LineWidget:new{ dimen = Geom:new{ w = Size.line.medium, h = self.line_height_px, } } + if self.height then + -- luajit may segfault if we were provided with a negative height + -- also ensure we display at least one line + if self.height < self.line_height_px then + self.height = self.line_height_px + end + -- if no self.height, these will be set just after self:_splitCharWidthList() + self.lines_per_page = math.floor(self.height / self.line_height_px) + self.text_height = self.lines_per_page * self.line_height_px + end self:_evalCharWidthList() self:_splitCharWidthList() + if self.charpos and self.charpos > #self.charlist+1 then + self.charpos = #self.charlist+1 + end + if self.height == nil then - self:_renderText(1, #self.vertical_string_list) + self.lines_per_page = #self.vertical_string_list + self.text_height = self.lines_per_page * self.line_height_px + self.virtual_line_num = 1 else - -- luajit may segfault if we were provided with a negative height - if self.height < 0 then - self.height = 0 + -- 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. + -- We need to show the line containing charpos, while trying to + -- keep the previous top_line_num + if self.editable and self.charpos then + self:scrollViewToCharPos() end - self:_renderText(1, self:getVisLineCount()) end + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) if self.editable then - local x, y - x, y = self:_findCharPos() - self.cursor_line:paintTo(self._bb, x, y) + self:moveCursorToCharPos(self.charpos or 1) end self.dimen = Geom:new(self:getSize()) if Device:isTouchDevice() then @@ -115,19 +141,21 @@ end function TextBoxWidget:unfocus() self.editable = false + self:free() self:init() end function TextBoxWidget:focus() self.editable = true + self:free() self:init() end -- Split `self.text` into `self.charlist` and evaluate the width of each char in it. function TextBoxWidget:_evalCharWidthList() + -- if self.charlist is provided, use it directly if self.charlist == nil then self.charlist = util.splitToChars(self.text) - self.charpos = #self.charlist + 1 end -- get width of each distinct char local char_width = {} @@ -149,10 +177,6 @@ function TextBoxWidget:_splitCharWidthList() local ln = 1 local offset, end_offset, cur_line_width - local lines_per_page - if self.height then - lines_per_page = self:getVisLineCount() - end local image_num = 0 local targeted_width = self.width local image_lines_remaining = 0 @@ -164,8 +188,8 @@ function TextBoxWidget:_splitCharWidthList() if self.line_num_to_image == nil then self.line_num_to_image = {} end - if (lines_per_page and ln % lines_per_page == 1) -- first line of a scrolled page - or (lines_per_page == nil and ln == 1) then -- first line if not scrollabled + if (self.lines_per_page and ln % self.lines_per_page == 1) -- first line of a scrolled page + or (self.lines_per_page == nil and ln == 1) then -- first line if not scrollabled image_num = image_num + 1 if image_num <= #self.images then local image = self.images[image_num] @@ -326,10 +350,6 @@ function TextBoxWidget:_getLinePads(vertical_string) return pads end -function TextBoxWidget:geCharWidth(idx) - return self.char_width[self.charlist[idx]] -end - function TextBoxWidget:_renderText(start_row_idx, end_row_idx) local font_height = self.face.size if start_row_idx < 1 then start_row_idx = 1 end @@ -478,7 +498,7 @@ function TextBoxWidget:_renderImage(start_row_idx) if scheduled_for_linenum == self.virtual_line_num then -- we are still on the same page self:update(true) - UIManager:setDirty("all", function() + UIManager:setDirty(self.dialog or "all", function() -- return "ui", self.dimen -- We can refresh only the image area, even if we have just -- re-rendered the whole textbox as the text has been @@ -496,7 +516,7 @@ function TextBoxWidget:_renderImage(start_row_idx) -- Image loaded (or not if failure): call us again -- with scheduled_update = true so we can draw what we got self:update(true) - UIManager:setDirty("all", function() + UIManager:setDirty(self.dialog or "all", function() -- return "ui", self.dimen -- We can refresh only the image area, even if we have just -- re-rendered the whole textbox as the text has been @@ -517,79 +537,68 @@ function TextBoxWidget:_renderImage(start_row_idx) end end --- Return the position of the cursor corresponding to `self.charpos`, --- Be aware of virtual line number of the scorllTextWidget. -function TextBoxWidget:_findCharPos() - if self.text == nil or string.len(self.text) == 0 then - return 0, 0 - end - -- Find the line number. - local ln = self.height == nil and 1 or self.virtual_line_num - while ln + 1 <= #self.vertical_string_list do - if self.vertical_string_list[ln + 1].offset > self.charpos then - break - else - ln = ln + 1 - end - end - -- Find the offset at the current line. - local x = 0 - local offset = self.vertical_string_list[ln].offset - while offset < self.charpos do - x = x + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0) - offset = offset + 1 - end - return x + 1, (ln - 1) * self.line_height_px -- offset `x` by 1 to avoid overlap +function TextBoxWidget:getCharWidth(idx) + return self.char_width[self.charlist[idx]] end -function TextBoxWidget:moveCursorToCharpos(charpos) - self.charpos = charpos - local x, y = self:_findCharPos() - self.cursor_line:paintTo(self._bb, x, y) +function TextBoxWidget:getVisLineCount() + return self.lines_per_page end --- Click event: Move the cursor to a new location with (x, y), in pixels. --- Be aware of virtual line number of the scorllTextWidget. -function TextBoxWidget:moveCursor(x, y) - if x < 0 or y < 0 then return end - if #self.vertical_string_list == 0 then - -- if there's no text at all, nothing to do - return 1 - end - local w = 0 - local ln = self.height == nil and 1 or self.virtual_line_num - ln = ln + math.ceil(y / self.line_height_px) - 1 - if ln > #self.vertical_string_list then - ln = #self.vertical_string_list - x = self.width - end - local offset = self.vertical_string_list[ln].offset - local idx = ln == #self.vertical_string_list and #self.charlist or self.vertical_string_list[ln + 1].offset - 1 - while offset <= idx do - w = w + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0) - if w > x then break else offset = offset + 1 end - end - if w > x then - local w_prev = w - self.char_width[self.charlist[offset]] - (self.idx_pad[offset] or 0) - if x - w_prev < w - x then -- the previous one is more closer - w = w_prev - else - offset = offset + 1 - end +function TextBoxWidget:getAllLineCount() + return #self.vertical_string_list +end + +function TextBoxWidget:getTextHeight() + return self.text_height +end + +function TextBoxWidget:getLineHeight() + return self.line_height_px +end + +function TextBoxWidget:getVisibleHeightRatios() + local low = (self.virtual_line_num - 1) / #self.vertical_string_list + local high = (self.virtual_line_num - 1 + self.lines_per_page) / #self.vertical_string_list + return low, high +end + +function TextBoxWidget:getCharPos() + -- returns virtual_line_num too + return self.charpos, self.virtual_line_num +end + +function TextBoxWidget:getSize() + if self.width and self.height then + return Geom:new{ w = self.width, h = self.height} + else + return Geom:new{ w = self.width, h = self._bb:getHeight()} end - self:free() - self:_renderText(1, #self.vertical_string_list) - self.cursor_line:paintTo(self._bb, w + 1, - (ln - self.virtual_line_num) * self.line_height_px) - return offset end -function TextBoxWidget:getVisLineCount() - return math.floor(self.height / self.line_height_px) +function TextBoxWidget:paintTo(bb, x, y) + self.dimen.x, self.dimen.y = x, y + bb:blitFrom(self._bb, x, y, 0, 0, self.width, self._bb:getHeight()) end -function TextBoxWidget:getAllLineCount() - return #self.vertical_string_list +function TextBoxWidget:free() + logger.dbg("TextBoxWidget:free called") + -- :free() is called when our parent widget is closing, and + -- here whenever :_renderText() is being called, to display + -- a new page: cancel any scheduled image update, as it + -- is no longer related to current page + if self.image_update_action then + logger.dbg("TextBoxWidget:free: cancelling self.image_update_action") + UIManager:unschedule(self.image_update_action) + end + if self._bb then + self._bb:free() + self._bb = nil + end + if self.cursor_restore_bb then + self.cursor_restore_bb:free() + self.cursor_restore_bb = nil + end end function TextBoxWidget:update(scheduled_update) @@ -597,7 +606,7 @@ function TextBoxWidget:update(scheduled_update) -- We set this flag so :_renderText() can know we were called from a -- scheduled update and so not schedule another one self.scheduled_update = scheduled_update - self:_renderText(self.virtual_line_num, self.virtual_line_num + self:getVisLineCount() - 1) + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) self.scheduled_update = nil end @@ -614,7 +623,7 @@ function TextBoxWidget:onTapImage(arg, ges) -- Toggle between image and alt_text self.image_show_alt_text = not self.image_show_alt_text self:update() - UIManager:setDirty("all", function() + UIManager:setDirty(self.dialog or "all", function() -- return "ui", self.dimen -- We can refresh only the image area, even if we have just -- re-rendered the whole textbox as the text has been @@ -632,100 +641,421 @@ function TextBoxWidget:onTapImage(arg, ges) end end --- TODO: modify `charpos` so that it can render the cursor function TextBoxWidget:scrollDown() self.image_show_alt_text = nil -- reset image bb/alt state - local visible_line_count = self:getVisLineCount() - if self.virtual_line_num + visible_line_count <= #self.vertical_string_list then + if self.virtual_line_num + self.lines_per_page <= #self.vertical_string_list then self:free() - self.virtual_line_num = self.virtual_line_num + visible_line_count - self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) + self.virtual_line_num = self.virtual_line_num + self.lines_per_page + -- If last line shown, set it to be the last line of view + -- (only if editable, as this would be confusing when reading + -- a dictionary result or a wikipedia page) + if self.editable then + if self.virtual_line_num > #self.vertical_string_list - self.lines_per_page + 1 then + self.virtual_line_num = #self.vertical_string_list - self.lines_per_page + 1 + if self.virtual_line_num < 1 then + self.virtual_line_num = 1 + end + end + end + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) + end + if self.editable then + -- move cursor to first line of visible area + local ln = self.height == nil and 1 or self.virtual_line_num + self:moveCursorToCharPos(self.vertical_string_list[ln] and self.vertical_string_list[ln].offset or 1) end - return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list end --- TODO: modify `charpos` so that it can render the cursor function TextBoxWidget:scrollUp() self.image_show_alt_text = nil - local visible_line_count = self:getVisLineCount() if self.virtual_line_num > 1 then self:free() - if self.virtual_line_num <= visible_line_count then + if self.virtual_line_num <= self.lines_per_page then self.virtual_line_num = 1 else - self.virtual_line_num = self.virtual_line_num - visible_line_count + self.virtual_line_num = self.virtual_line_num - self.lines_per_page end - self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) + end + if self.editable then + -- move cursor to first line of visible area + local ln = self.height == nil and 1 or self.virtual_line_num + self:moveCursorToCharPos(self.vertical_string_list[ln] and self.vertical_string_list[ln].offset or 1) + end +end + +function TextBoxWidget:scrollToTop() + self.image_show_alt_text = nil + if self.virtual_line_num > 1 then + self:free() + self.virtual_line_num = 1 + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) + end + if self.editable then + -- move cursor to first char + self:moveCursorToCharPos(1) + end +end + +function TextBoxWidget:scrollToBottom() + self.image_show_alt_text = nil + -- Show last line of text on last line of view + local ln = #self.vertical_string_list - self.lines_per_page + 1 + if ln < 1 then + ln = 1 + end + if self.virtual_line_num ~= ln then + self:free() + self.virtual_line_num = ln + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) + end + if self.editable then + -- move cursor to last char + self:moveCursorToCharPos(#self.charlist + 1) end - return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list end + function TextBoxWidget:scrollToRatio(ratio) self.image_show_alt_text = nil ratio = math.max(0, math.min(1, ratio)) -- ensure ratio is between 0 and 1 (100%) - local visible_line_count = self:getVisLineCount() - local page_count = 1 + math.floor((#self.vertical_string_list - 1) / visible_line_count) + local page_count = 1 + math.floor((#self.vertical_string_list - 1) / self.lines_per_page) local page_num = 1 + Math.round((page_count - 1) * ratio) - local line_num = 1 + (page_num - 1) * visible_line_count + local line_num = 1 + (page_num - 1) * self.lines_per_page if line_num ~= self.virtual_line_num then self:free() self.virtual_line_num = line_num - self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) + end + if self.editable then + -- move cursor to first line of visible area + local ln = self.height == nil and 1 or self.virtual_line_num + self:moveCursorToCharPos(self.vertical_string_list[ln].offset) end - return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list end -function TextBoxWidget:getSize() - if self.width and self.height then - return Geom:new{ w = self.width, h = self.height} - else - return Geom:new{ w = self.width, h = self._bb:getHeight()} + +--- Cursor management + +-- Return the coordinates (relative to current view, so negative y is possible) +-- of the left of char at charpos (use self.charpos if none provided) +function TextBoxWidget:_getXYForCharPos(charpos) + if not charpos then + charpos = self.charpos + end + if self.text == nil or string.len(self.text) == 0 then + return 0, 0 + end + -- Find the line number: scan up/down from current virtual_line_num + local ln = self.height == nil and 1 or self.virtual_line_num + if charpos > self.vertical_string_list[ln].offset then -- after first line + while ln < #self.vertical_string_list do + if self.vertical_string_list[ln + 1].offset > charpos then + break + else + ln = ln + 1 + end + end + elseif charpos < self.vertical_string_list[ln].offset then -- before first line + while ln > 1 do + ln = ln - 1 + if self.vertical_string_list[ln].offset <= charpos then + break + end + end + end + local y = (ln - self.virtual_line_num) * self.line_height_px + -- Find the x offset in the current line. + local x = 0 + local offset = self.vertical_string_list[ln].offset + local nbchars = #self.charlist + while offset < charpos do + if offset <= nbchars then -- charpos may exceed #self.charlist + x = x + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0) + end + offset = offset + 1 end + -- Cursor can be drawn at x, it will be on the left of the char pointed by charpos + -- (x=0 for first char of line - for end of line, it will be before the \n, the \n + -- itself being not displayed) + return x, y end -function TextBoxWidget:moveCursorUp() - if self.vertical_string_list and #self.vertical_string_list < 2 then return end - local x, y - x, y = self:_findCharPos() - local charpos = self:moveCursor(x, y - self.line_height_px +1) - if charpos then - self:moveCursorToCharpos(charpos) +-- Return the charpos at provided coordinates (relative to current view, +-- so negative y is allowed) +function TextBoxWidget:getCharPosAtXY(x, y) + if #self.vertical_string_list == 0 then + -- if there's no text at all, nothing to do + return 1 + end + local ln = self.height == nil and 1 or self.virtual_line_num + ln = ln + math.floor(y / self.line_height_px) + if ln < 1 then + return 1 -- return start of first line + elseif ln > #self.vertical_string_list then + return #self.charlist + 1 -- return end of last line + end + if x > self.vertical_string_list[ln].width then -- no need to loop thru chars + local pos = self.vertical_string_list[ln].end_offset + if not pos then -- empty line + pos = self.vertical_string_list[ln].offset + end + return pos + 1 -- after last char + end + local idx = self.vertical_string_list[ln].offset + local end_offset = self.vertical_string_list[ln].end_offset + if not end_offset then -- empty line + return idx + end + local w = 0 + local w_prev + while idx <= end_offset do + w_prev = w + w = w + self.char_width[self.charlist[idx]] + (self.idx_pad[idx] or 0) + if w > x then -- we're on this char at idx + if x - w_prev < w - x then -- nearest to char start + return idx + else -- nearest to char end: draw cursor before next char + return idx + 1 + end + break + end + idx = idx + 1 end - return charpos + return end_offset + 1 -- should not happen end -function TextBoxWidget:moveCursorDown() - if self.vertical_string_list and #self.vertical_string_list < 2 then return end - local x, y - x, y = self:_findCharPos() - local charpos = self:moveCursor(x, y + self.line_height_px +1) - if charpos then - self:moveCursorToCharpos(charpos) +-- Tunables for the next function: not sure yet which combination is +-- best to get the less cursor trail - and initially got some crashes +-- when using refresh funcs. It finally feels fine with both set to true, +-- but one can turn them to false with a setting to check how some other +-- combinations do. +local CURSOR_COMBINE_REGIONS = G_reader_settings:nilOrTrue("ui_cursor_combine_regions") +local CURSOR_USE_REFRESH_FUNCS = G_reader_settings:nilOrTrue("ui_cursor_use_refresh_funcs") + +-- Update charpos to the one provided; if out of current view, update +-- virtual_line_num to move it to view, and draw the cursor +function TextBoxWidget:moveCursorToCharPos(charpos) + if not self.editable then + -- we shouldn't have been called if not editable + logger.warn("TextBoxWidget:moveCursorToCharPos called, but not editable") + return + end + self.charpos = charpos + self.prev_virtual_line_num = self.virtual_line_num + local x, y = self:_getXYForCharPos() -- we can get y outside current view + -- adjust self.virtual_line_num for overflowed y to have y in current view + if y < 0 then + local scroll_lines = math.ceil( -y / self.line_height_px ) + self.virtual_line_num = self.virtual_line_num - scroll_lines + if self.virtual_line_num < 1 then + self.virtual_line_num = 1 + end + y = y + scroll_lines * self.line_height_px + end + if y >= self.text_height then + local scroll_lines = math.floor( (y-self.text_height) / self.line_height_px ) + 1 + self.virtual_line_num = self.virtual_line_num + scroll_lines + -- needs to deal with possible overflow ? + y = y - scroll_lines * self.line_height_px + end + if not self._bb then + return -- no bb yet to render the cursor too + end + if self.virtual_line_num ~= self.prev_virtual_line_num then + -- We scrolled the view: full render and refresh needed + self:free() + self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1) + -- Store the original image of where we will draw the cursor, for a + -- quick restore and two small refreshes when moving only the cursor + self.cursor_restore_x = x + self.cursor_restore_y = y + self.cursor_restore_bb = Blitbuffer.new(self.cursor_line.dimen.w, self.cursor_line.dimen.h, self._bb:getType()) + self.cursor_restore_bb:blitFrom(self._bb, 0, 0, x, y, self.cursor_line.dimen.w, self.cursor_line.dimen.h) + -- Paint the cursor, and refresh the whole widget + self.cursor_line:paintTo(self._bb, x, y) + UIManager:setDirty(self.dialog or "all", function() + return "ui", self.dimen + end) + elseif self._bb then + if CURSOR_USE_REFRESH_FUNCS then + -- We didn't scroll the view, only the cursor was moved + local restore_x, restore_y + if self.cursor_restore_bb then + -- Restore the previous cursor position content, and do + -- a small ui refresh of the old cursor area + self._bb:blitFrom(self.cursor_restore_bb, self.cursor_restore_x, self.cursor_restore_y, + 0, 0, self.cursor_line.dimen.w, self.cursor_line.dimen.h) + -- remember current values for use in the setDirty funcs, as + -- we will have overriden them when these are called + restore_x = self.cursor_restore_x + restore_y = self.cursor_restore_y + if not CURSOR_COMBINE_REGIONS then + UIManager:setDirty(self.dialog or "all", function() + return "ui", Geom:new{ + x = self.dimen.x + restore_x, + y = self.dimen.y + restore_y, + w = self.cursor_line.dimen.w, + h = self.cursor_line.dimen.h, + } + end) + end + self.cursor_restore_bb:free() + self.cursor_restore_bb = nil + end + -- Store the original image of where we will draw the new cursor + self.cursor_restore_x = x + self.cursor_restore_y = y + self.cursor_restore_bb = Blitbuffer.new(self.cursor_line.dimen.w, self.cursor_line.dimen.h, self._bb:getType()) + self.cursor_restore_bb:blitFrom(self._bb, 0, 0, x, y, self.cursor_line.dimen.w, self.cursor_line.dimen.h) + -- Paint the cursor, and do a small ui refresh of the new cursor area + self.cursor_line:paintTo(self._bb, x, y) + UIManager:setDirty(self.dialog or "all", function() + local cursor_region = Geom:new{ + x = self.dimen.x + x, + y = self.dimen.y + y, + w = self.cursor_line.dimen.w, + h = self.cursor_line.dimen.h, + } + if CURSOR_COMBINE_REGIONS and restore_x and restore_y then + local restore_region = Geom:new{ + x = self.dimen.x + restore_x, + y = self.dimen.y + restore_y, + w = self.cursor_line.dimen.w, + h = self.cursor_line.dimen.h, + } + cursor_region = cursor_region:combine(restore_region) + end + return "ui", cursor_region + end) + else -- CURSOR_USE_REFRESH_FUNCS = false + -- We didn't scroll the view, only the cursor was moved + local restore_region + if self.cursor_restore_bb then + -- Restore the previous cursor position content, and do + -- a small ui refresh of the old cursor area + self._bb:blitFrom(self.cursor_restore_bb, self.cursor_restore_x, self.cursor_restore_y, + 0, 0, self.cursor_line.dimen.w, self.cursor_line.dimen.h) + if self.dimen then + restore_region = Geom:new{ + x = self.dimen.x + self.cursor_restore_x, + y = self.dimen.y + self.cursor_restore_y, + w = self.cursor_line.dimen.w, + h = self.cursor_line.dimen.h, + } + if not CURSOR_COMBINE_REGIONS then + UIManager:setDirty(self.dialog or "all", "ui", restore_region) + end + end + self.cursor_restore_bb:free() + self.cursor_restore_bb = nil + end + -- Store the original image of where we will draw the new cursor + self.cursor_restore_x = x + self.cursor_restore_y = y + self.cursor_restore_bb = Blitbuffer.new(self.cursor_line.dimen.w, self.cursor_line.dimen.h, self._bb:getType()) + self.cursor_restore_bb:blitFrom(self._bb, 0, 0, x, y, self.cursor_line.dimen.w, self.cursor_line.dimen.h) + -- Paint the cursor, and do a small ui refresh of the new cursor area + self.cursor_line:paintTo(self._bb, x, y) + if self.dimen then + local cursor_region = Geom:new{ + x = self.dimen.x + x, + y = self.dimen.y + y, + w = self.cursor_line.dimen.w, + h = self.cursor_line.dimen.h, + } + if CURSOR_COMBINE_REGIONS and restore_region then + cursor_region = cursor_region:combine(restore_region) + end + UIManager:setDirty(self.dialog or "all", "ui", cursor_region) + end + end end - return charpos end -function TextBoxWidget:paintTo(bb, x, y) - self.dimen.x, self.dimen.y = x, y - bb:blitFrom(self._bb, x, y, 0, 0, self.width, self._bb:getHeight()) +function TextBoxWidget:moveCursorToXY(x, y, restrict_to_view) + if restrict_to_view then + -- Wrap y to current view (when getting coordinates from gesture) + -- (no real need to check for x, getCharPosAtXY() is ok with any x) + if y < 0 then + y = 0 + end + if y >= self.text_height then + y = self.text_height - 1 + end + end + local charpos = self:getCharPosAtXY(x, y) + self:moveCursorToCharPos(charpos) end -function TextBoxWidget:free() - logger.dbg("TextBoxWidget:free called") - -- :free() is called when our parent widget is closing, and - -- here whenever :_renderText() is being called, to display - -- a new page: cancel any scheduled image update, as it - -- is no more related to current page - if self.image_update_action then - logger.dbg("TextBoxWidget:free: cancelling self.image_update_action") - UIManager:unschedule(self.image_update_action) +-- Update self.virtual_line_num to the page containing charpos +function TextBoxWidget:scrollViewToCharPos() + if self.top_line_num then + -- if previous top_line_num provided, go to that line + self.virtual_line_num = self.top_line_num + if self.virtual_line_num < 1 then + self.virtual_line_num = 1 + end + if self.virtual_line_num > #self.vertical_string_list then + self.virtual_line_num = #self.vertical_string_list + end + -- Ensure we don't show too much blank at end (when deleting last lines) + -- local max_empty_lines = math.floor(self.lines_per_page / 2) + -- Best to not allow any, for initially non-scrolled widgets + local max_empty_lines = 0 + local max_virtual_line_num = #self.vertical_string_list - self.lines_per_page + 1 + max_empty_lines + if self.virtual_line_num > max_virtual_line_num then + self.virtual_line_num = max_virtual_line_num + if self.virtual_line_num < 1 then + self.virtual_line_num = 1 + end + end + -- and adjust if cursor is out of view + self:moveCursorToCharPos(self.charpos) + return end - if self._bb then - self._bb:free() - self._bb = nil + -- Otherwise, find the "hard" page containing charpos + local ln = 1 + while true do + local lend = ln + self.lines_per_page - 1 + if lend >= #self.vertical_string_list then + break -- last page + end + if self.vertical_string_list[lend+1].offset >= self.charpos then + break + end + ln = ln + self.lines_per_page + end + self.virtual_line_num = ln +end + +function TextBoxWidget:moveCursorLeft() + if self.charpos > 1 then + self:moveCursorToCharPos(self.charpos-1) end end +function TextBoxWidget:moveCursorRight() + if self.charpos < #self.charlist + 1 then -- we can move after last char + self:moveCursorToCharPos(self.charpos+1) + end +end + +function TextBoxWidget:moveCursorUp() + if self.vertical_string_list and #self.vertical_string_list < 2 then return end + local x, y = self:_getXYForCharPos() + self:moveCursorToXY(x, y - self.line_height_px) +end + +function TextBoxWidget:moveCursorDown() + if self.vertical_string_list and #self.vertical_string_list < 2 then return end + local x, y = self:_getXYForCharPos() + self:moveCursorToXY(x, y + self.line_height_px) +end + + +--- Text selection with Hold + -- Allow selection of a single word at hold position function TextBoxWidget:onHoldWord(callback, ges) if not callback then return end @@ -771,7 +1101,6 @@ function TextBoxWidget:onHoldWord(callback, ges) return end - -- Allow selection of one or more words (with no visual feedback) -- Gestures should be declared in widget using us (e.g dictquicklookup.lua)