Text input fixes and enhancements (#4084)

InputText, ScrollTextWidget, TextBoxWidget:
- proper line scrolling when moving cursor or inserting/deleting text
  to behave like most text editors do
- fix cursor navigation, optimize refreshes when moving only the cursor,
  don't recreate the textwidget when moving cursor up/down
- optimize refresh areas, stick to "ui" to avoid a "partial" black
  flash every 6 appended or deleted chars

InputText:
- fix issue when toggling Show password multiple times
- new option: InputText.cursor_at_end (default: true)
- if no InputText.height provided, measure the text widget height
  that we would start with, and use a ScrollTextWidget with that
  fixed height, so widget does not overflow container if we extend
  the text and increase the number of lines
- as we are using "ui" refreshes while text editing, allows refreshing
  the InputText with a diagonal swipe on it (actually, refresh the
  whole screen, which allows refreshing the keyboard too if needed)

ScrollTextWidget:
- properly align scrollbar with its TextBoxWidget

TextBoxWidget:
- some cleanup (added new properties to avoid many method calls), added
  proxy methods for upper widgets to get them
- reordered/renamed/refactored the *CharPos* methods for easier reading
  (sorry for the diff that won't help reviewing, but that was needed)

InputDialog:
- new options:
   allow_newline = false, -- allow entering new lines
   cursor_at_end = true, -- starts with cursor at end of text, ready to append
   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
- find the most adequate text height, when none provided or fullscreen, to
  not overflow screen (and not be stuck with Cancel/Save buttons hidden)
- had to disable the use of a MovableContainer (many issues like becoming
  transparent when a PathChooser comes in front, Hold to paste from
  clipboard, moving the InputDialog under the keyboard and getting stuck...)

GestureRange: fix possible crash (when event processed after widget
destruction ?)

LoginDialog: fix some ui stack increase and possible crash when switching
focus many times.
pull/4090/head
poire-z 6 years ago committed by GitHub
parent 7666644362
commit 0d66ea7555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -450,6 +450,9 @@ function ReaderBookmark:renameBookmark(item, from_highlight)
title = _("Rename bookmark"), title = _("Rename bookmark"),
input = item.text, input = item.text,
input_type = "text", input_type = "text",
allow_newline = true,
cursor_at_end = false,
add_scroll_buttons = true,
buttons = { buttons = {
{ {
{ {

@ -35,7 +35,7 @@ function GestureRange:match(gs)
else else
range = self.range range = self.range
end end
if not range:contains(gs.pos) then if not range or not range:contains(gs.pos) then
return false return false
end end
end end

@ -34,6 +34,7 @@ local Size = {
thin = Screen:scaleBySize(0.5), thin = Screen:scaleBySize(0.5),
button = Screen:scaleBySize(1.5), button = Screen:scaleBySize(1.5),
window = Screen:scaleBySize(1.5), window = Screen:scaleBySize(1.5),
inputtext = Screen:scaleBySize(2),
}, },
margin = { margin = {
default = Screen:scaleBySize(5), default = Screen:scaleBySize(5),

@ -555,6 +555,7 @@ function BookStatusWidget:onSwitchFocus(inputbox)
input_hint = "", input_hint = "",
input_type = "text", input_type = "text",
scroll = true, scroll = true,
allow_newline = true,
text_height = Screen:scaleBySize(150), text_height = Screen:scaleBySize(150),
buttons = { buttons = {
{ {

@ -37,6 +37,15 @@ Example:
UIManager:show(sample_input) UIManager:show(sample_input)
sample_input:onShowKeyboard() 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, If it would take the user more than half a minute to recover from a mistake,
a "Cancel" button <em>must</em> be added to the dialog. The cancellation button a "Cancel" button <em>must</em> be added to the dialog. The cancellation button
should be kept on the left and the button executing the action on the right. 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, buttons = nil,
input_type = nil, input_type = nil,
enter_callback = 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, width = nil,
@ -88,13 +110,29 @@ local InputDialog = InputContainer:new{
title_padding = Size.padding.default, title_padding = Size.padding.default,
title_margin = Size.margin.title, 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, input_margin = Size.margin.default,
button_padding = Size.padding.default, button_padding = Size.padding.default,
border_size = Size.border.window,
} }
function InputDialog:init() function InputDialog:init()
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 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, local title_width = RenderText:sizeUtf8Text(0, self.width,
self.title_face, self.title, true).x self.title_face, self.title, true).x
if title_width > self.width then if title_width > self.width then
@ -114,28 +152,159 @@ function InputDialog:init()
width = self.width, width = self.width,
} }
} }
self.title_bar = LineWidget:new{
dimen = Geom:new{
w = self.width,
h = Size.line.thick,
}
}
if self.description then if self.description then
self.description = FrameContainer:new{ self.description_widget = FrameContainer:new{
padding = self.title_padding, padding = self.desc_padding,
margin = self.title_margin, margin = self.desc_margin,
bordersize = 0, bordersize = 0,
TextBoxWidget:new{ TextBoxWidget:new{
text = self.description, text = self.description,
face = self.description_face, 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 else
self.description = VerticalSpan:new{ width = self.title_margin + self.title_padding } self.description_widget = VerticalSpan:new{ width = 0 }
end 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{ self._input_widget = InputText:new{
text = self.input, text = self.input,
hint = self.input_hint, hint = self.input_hint,
face = self.input_face, face = self.input_face,
width = self.text_width or self.width * 0.9, width = self.text_width,
height = self.text_height or nil, height = self.text_height or nil,
padding = self.input_padding,
margin = self.input_margin,
input_type = self.input_type, input_type = self.input_type,
text_type = self.text_type, text_type = self.text_type,
enter_callback = self.enter_callback or function() enter_callback = self.enter_callback or function()
@ -148,68 +317,54 @@ function InputDialog:init()
end end
end end
end, end,
scroll = false, scroll = true,
cursor_at_end = self.cursor_at_end,
parent = self, parent = self,
} }
self.button_table = ButtonTable:new{ if self.allow_newline then -- remove any enter_callback
width = self.width - 2*self.button_padding, self._input_widget.enter_callback = nil
button_font_face = "cfont", end
button_font_size = 20, if Device:hasKeys() then
buttons = self.buttons, --little hack to piggyback on the layout of the button_table to handle the new InputText
zero_sep = true, table.insert(self.button_table.layout, 1, {self._input_widget})
show_parent = self, end
}
self.title_bar = LineWidget:new{
dimen = Geom:new{
w = self.width,
h = Size.line.thick,
}
}
-- Final widget
self.dialog_frame = FrameContainer:new{ self.dialog_frame = FrameContainer:new{
radius = Size.radius.window, radius = self.fullscreen and 0 or Size.radius.window,
padding = 0, padding = 0,
margin = 0, margin = 0,
bordersize = self.border_size,
background = Blitbuffer.COLOR_WHITE, background = Blitbuffer.COLOR_WHITE,
VerticalGroup:new{ VerticalGroup:new{
align = "left", align = "left",
self.title, self.title,
self.title_bar, self.title_bar,
self.description, self.description_widget,
-- input vspan_before_input_text,
CenterContainer:new{ CenterContainer:new{
dimen = Geom:new{ dimen = Geom:new{
w = self.title_bar:getSize().w, w = self.width,
h = self._input_widget:getSize().h, h = self._input_widget:getSize().h,
}, },
self._input_widget, self._input_widget,
}, },
-- Add same vertical space after than before InputText vspan_after_input_text,
VerticalSpan:new{ width = self.title_margin + self.title_padding }, buttons_container,
-- buttons
CenterContainer:new{
dimen = Geom:new{
w = self.title_bar:getSize().w,
h = self.button_table:getSize().h,
},
self.button_table,
} }
} }
local frame = self.dialog_frame
if self.movable then
frame = MovableContainer:new{
self.dialog_frame,
} }
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 end
self[1] = CenterContainer:new{ self[1] = CenterContainer:new{
dimen = Geom:new{ dimen = Geom:new{
w = Screen:getWidth(), w = Screen:getWidth(),
h = Screen:getHeight() - self._input_widget:getKeyboardDimen().h, h = Screen:getHeight() - self._input_widget:getKeyboardDimen().h,
}, },
MovableContainer:new{ frame
self.dialog_frame,
},
} }
end end

@ -6,6 +6,7 @@ local Font = require("ui/font")
local GestureRange = require("ui/gesturerange") local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer") local InputContainer = require("ui/widget/container/inputcontainer")
local ScrollTextWidget = require("ui/widget/scrolltextwidget") local ScrollTextWidget = require("ui/widget/scrolltextwidget")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget") local TextBoxWidget = require("ui/widget/textboxwidget")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup") local VerticalGroup = require("ui/widget/verticalgroup")
@ -18,24 +19,30 @@ local Keyboard
local InputText = InputContainer:new{ local InputText = InputContainer:new{
text = "", text = "",
hint = "demo hint", hint = "demo hint",
charlist = nil, -- table to store input string input_type = nil, -- "number" or anything else
charpos = nil, -- position to insert a new char, or the position of the cursor text_type = nil, -- "password" or anything else
input_type = nil,
text_type = nil,
text_widget = nil, -- Text Widget for cursor movement
show_password_toggle = true, 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, width = nil,
height = nil, height = nil, -- when nil, will be set to original text height (possibly
face = Font:getFace("smallinfofont"), -- 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), face = Font:getFace("smallinfofont"),
margin = Screen:scaleBySize(5), padding = Size.padding.default,
bordersize = Screen:scaleBySize(2), margin = Size.margin.default,
bordersize = Size.border.inputtext,
parent = nil, -- parent dialog that will be set dirty -- for internal use
scroll = false, text_widget = nil, -- Text Widget for cursor movement, possibly a ScrollTextWidget
focused = true, 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 -- 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 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 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) function InputText:onTapTextBox(arg, ges)
if self.parent.onSwitchFocus then if self.parent.onSwitchFocus then
self.parent:onSwitchFocus(self) self.parent:onSwitchFocus(self)
end end
local x = ges.pos.x - self._frame_textwidget.dimen.x - self.bordersize - self.padding local textwidget_offset = self.margin + self.bordersize + self.padding
local y = ges.pos.y - self._frame_textwidget.dimen.y - self.bordersize - self.padding local x = ges.pos.x - self._frame_textwidget.dimen.x - textwidget_offset
if x > 0 and y > 0 then local y = ges.pos.y - self._frame_textwidget.dimen.y - textwidget_offset
self.charpos = self.text_widget:moveCursor(x, y) self.text_widget:moveCursorToXY(x, y, true) -- restrict_to_view=true
UIManager:setDirty(self.parent, function() self.charpos, self.top_line_num = self.text_widget:getCharPos()
return "ui", self.dimen return true
end)
end
end end
function InputText:onHoldTextBox(arg, ges) function InputText:onHoldTextBox(arg, ges)
if self.parent.onSwitchFocus then if self.parent.onSwitchFocus then
self.parent:onSwitchFocus(self) self.parent:onSwitchFocus(self)
end end
local x = ges.pos.x - self._frame_textwidget.dimen.x - self.bordersize - self.padding local textwidget_offset = self.margin + self.bordersize + self.padding
local y = ges.pos.y - self._frame_textwidget.dimen.y - self.bordersize - self.padding local x = ges.pos.x - self._frame_textwidget.dimen.x - textwidget_offset
if x > 0 and y > 0 then local y = ges.pos.y - self._frame_textwidget.dimen.y - textwidget_offset
self.charpos = self.text_widget:moveCursor(x, y) 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 if Device:hasClipboard() and Device.input.hasClipboardText() then
self:addChars(Device.input.getClipboardText()) self:addChars(Device.input.getClipboardText())
end end
UIManager:setDirty(self.parent, function() return true
return "ui", self.dimen
end)
end 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
end end
if Device.hasKeys() then if Device.hasKeys() then
if not InputText.initEventListener then if not InputText.initEventListener then
@ -115,6 +168,10 @@ else
end end
function InputText:init() 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:initTextBox(self.text)
if self.readonly ~= true then if self.readonly ~= true then
self:initKeyboard() self:initKeyboard()
@ -122,11 +179,11 @@ function InputText:init()
end end
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 self.text = text
if self.text_type == "password" then
is_password_type = true
end
local fgcolor local fgcolor
local show_charlist local show_charlist
local show_text = text local show_text = text
@ -147,11 +204,16 @@ function InputText:initTextBox(text, char_added, is_password_type)
end end
end end
self.charlist = util.splitToChars(text) self.charlist = util.splitToChars(text)
-- keep previous cursor position if charpos not nil
if self.charpos == nil then if self.charpos == nil then
if self.cursor_at_end then
self.charpos = #self.charlist + 1 self.charpos = #self.charlist + 1
else
self.charpos = 1
end end
end end
if is_password_type and self.show_password_toggle then end
if self.is_password_type and self.show_password_toggle then
self._check_button = self._check_button or CheckButton:new{ self._check_button = self._check_button or CheckButton:new{
text = _("Show password"), text = _("Show password"),
callback = function() callback = function()
@ -162,12 +224,9 @@ function InputText:initTextBox(text, char_added, is_password_type)
self.text_type = "text" self.text_type = "text"
self._check_button:check() self._check_button:check()
end end
self:setText(self:getText(), is_password_type) self:setText(self:getText())
end, end,
width = self.width,
height = self.height,
padding = self.padding, padding = self.padding,
margin = self.margin, margin = self.margin,
bordersize = self.bordersize, bordersize = self.bordersize,
@ -182,11 +241,28 @@ function InputText:initTextBox(text, char_added, is_password_type)
self._password_toggle = nil self._password_toggle = nil
end end
show_charlist = util.splitToChars(show_text) 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 if self.scroll then
self.text_widget = ScrollTextWidget:new{ self.text_widget = ScrollTextWidget:new{
text = show_text, text = show_text,
charlist = show_charlist, charlist = show_charlist,
charpos = self.charpos, charpos = self.charpos,
top_line_num = self.top_line_num,
editable = self.focused, editable = self.focused,
face = self.face, face = self.face,
fgcolor = fgcolor, fgcolor = fgcolor,
@ -199,13 +275,18 @@ function InputText:initTextBox(text, char_added, is_password_type)
text = show_text, text = show_text,
charlist = show_charlist, charlist = show_charlist,
charpos = self.charpos, charpos = self.charpos,
top_line_num = self.top_line_num,
editable = self.focused, editable = self.focused,
face = self.face, face = self.face,
fgcolor = fgcolor, fgcolor = fgcolor,
width = self.width, width = self.width,
height = self.height, height = self.height,
dialog = self.parent,
} }
end 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{ self._frame_textwidget = FrameContainer:new{
bordersize = self.bordersize, bordersize = self.bordersize,
padding = self.padding, padding = self.padding,
@ -266,17 +347,25 @@ function InputText:onCloseKeyboard()
UIManager:close(self.keyboard) UIManager:close(self.keyboard)
end end
function InputText:getTextHeight()
return self.text_widget:getTextHeight()
end
function InputText:getLineHeight()
return self.text_widget:getLineHeight()
end
function InputText:getKeyboardDimen() function InputText:getKeyboardDimen()
return self.keyboard.dimen return self.keyboard.dimen
end end
function InputText:addChars(char) function InputText:addChars(chars)
if self.enter_callback and char == '\n' then if self.enter_callback and chars == "\n" then
UIManager:scheduleIn(0.3, function() self.enter_callback() end) UIManager:scheduleIn(0.3, function() self.enter_callback() end)
return return
end end
table.insert(self.charlist, self.charpos, char) table.insert(self.charlist, self.charpos, chars)
self.charpos = self.charpos + #util.splitToChars(char) self.charpos = self.charpos + #util.splitToChars(chars)
self:initTextBox(table.concat(self.charlist), true) self:initTextBox(table.concat(self.charlist), true)
end end
@ -287,48 +376,63 @@ function InputText:delChar()
self:initTextBox(table.concat(self.charlist)) self:initTextBox(table.concat(self.charlist))
end end
-- For the following cursor/scroll methods, the text_widget deals
-- itself with setDirty'ing the appropriate regions
function InputText:leftChar() function InputText:leftChar()
if self.charpos == 1 then return end if self.charpos == 1 then return end
self.charpos = self.charpos -1 self.text_widget:moveCursorLeft()
self:initTextBox(table.concat(self.charlist)) self.charpos, self.top_line_num = self.text_widget:getCharPos()
end end
function InputText:rightChar() function InputText:rightChar()
if self.charpos > #table.concat(self.charlist) then return end if self.charpos > #self.charlist then return end
self.charpos = self.charpos +1 self.text_widget:moveCursorRight()
self:initTextBox(table.concat(self.charlist)) self.charpos, self.top_line_num = self.text_widget:getCharPos()
end end
function InputText:upLine() function InputText:upLine()
if self.text_widget.moveCursorUp then self.text_widget:moveCursorUp()
self.charpos = self.text_widget:moveCursorUp() self.charpos, self.top_line_num = self.text_widget:getCharPos()
end
end end
function InputText:downLine() function InputText:downLine()
if self.text_widget.moveCursorDown then self.text_widget:moveCursorDown()
self.charpos = 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 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 end
function InputText:clear() function InputText:clear()
self.charpos = nil self.charpos = nil
self.top_line_num = 1
self:initTextBox("") self:initTextBox("")
UIManager:setDirty(self.parent, function()
return "ui", self[1][1].dimen
end)
end end
function InputText:getText() function InputText:getText()
return self.text return self.text
end end
function InputText:setText(text, is_password_type) function InputText:setText(text)
self.charpos = nil -- Keep previous charpos and top_line_num
self:initTextBox(text, nil, is_password_type) self:initTextBox(text)
UIManager:setDirty(self.parent, function()
return "partial", self[1].dimen
end)
end end
return InputText return InputText

@ -102,6 +102,7 @@ function LoginDialog:onSwitchFocus(inputbox)
-- unfocus current inputbox -- unfocus current inputbox
self._input_widget:unfocus() self._input_widget:unfocus()
self._input_widget:onCloseKeyboard() self._input_widget:onCloseKeyboard()
UIManager:close(self)
-- focus new inputbox -- focus new inputbox
self._input_widget = inputbox self._input_widget = inputbox

@ -280,7 +280,7 @@ function MenuItem:init()
local removed_char_width= 0 local removed_char_width= 0
while removed_char_width < ellipsis_size do while removed_char_width < ellipsis_size do
-- the width of each char has already been calculated by TextBoxWidget -- 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 offset = offset - 1
end end
self.text = table.concat(item_name.charlist, '', 1, offset) .. "" self.text = table.concat(item_name.charlist, '', 1, offset) .. ""

@ -8,6 +8,7 @@ local PathChooser = FileChooser:extend{
title = _("Choose Path"), title = _("Choose Path"),
no_title = false, no_title = false,
is_popout = false, is_popout = false,
covers_fullscreen = true, -- set it to false if you set is_popout = true
is_borderless = true, is_borderless = true,
show_filesize = false, show_filesize = false,
file_filter = function() return false end, -- filter out regular files file_filter = function() return false end, -- filter out regular files

@ -19,6 +19,7 @@ local ScrollTextWidget = InputContainer:new{
text = nil, text = nil,
charlist = nil, charlist = nil,
charpos = nil, charpos = nil,
top_line_num = nil,
editable = false, editable = false,
justified = false, justified = false,
face = nil, face = nil,
@ -36,6 +37,8 @@ function ScrollTextWidget:init()
text = self.text, text = self.text,
charlist = self.charlist, charlist = self.charlist,
charpos = self.charpos, charpos = self.charpos,
top_line_num = self.top_line_num,
dialog = self.dialog,
editable = self.editable, editable = self.editable,
justified = self.justified, justified = self.justified,
face = self.face, face = self.face,
@ -52,9 +55,10 @@ function ScrollTextWidget:init()
low = 0, low = 0,
high = visible_line_count / total_line_count, high = visible_line_count / total_line_count,
width = self.scroll_bar_width, 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, self.text_widget)
table.insert(horizontal_group, HorizontalSpan:new{width=self.text_scroll_span}) table.insert(horizontal_group, HorizontalSpan:new{width=self.text_scroll_span})
table.insert(horizontal_group, self.v_scroll_bar) table.insert(horizontal_group, self.v_scroll_bar)
@ -92,38 +96,93 @@ function ScrollTextWidget:focus()
self.text_widget:focus() self.text_widget:focus()
end end
function ScrollTextWidget:moveCursor(x, y) function ScrollTextWidget:getTextHeight()
return self.text_widget:moveCursor(x, y) 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 end
function ScrollTextWidget:moveCursorUp() function ScrollTextWidget:moveCursorUp()
return self.text_widget:moveCursorUp(); self.text_widget:moveCursorUp();
self:updateScrollBar()
end end
function ScrollTextWidget:moveCursorDown() 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 end
function ScrollTextWidget:scrollText(direction) function ScrollTextWidget:scrollText(direction)
if direction == 0 then return end if direction == 0 then return end
local low, high
if direction > 0 then if direction > 0 then
low, high = self.text_widget:scrollDown() self.text_widget:scrollDown()
else else
low, high = self.text_widget:scrollUp() self.text_widget:scrollUp()
end end
self.v_scroll_bar:set(low, high) self:updateScrollBar()
UIManager:setDirty(self.dialog, function()
return "partial", self.dimen
end)
end end
function ScrollTextWidget:scrollToRatio(ratio) function ScrollTextWidget:scrollToRatio(ratio)
local low, high = self.text_widget:scrollToRatio(ratio) self.text_widget:scrollToRatio(ratio)
self.v_scroll_bar:set(low, high) self:updateScrollBar()
UIManager:setDirty(self.dialog, function()
return "partial", self.dimen
end)
end end
function ScrollTextWidget:onScrollText(arg, ges) function ScrollTextWidget:onScrollText(arg, ges)
@ -139,6 +198,10 @@ function ScrollTextWidget:onScrollText(arg, ges)
end end
function ScrollTextWidget:onTapScrollText(arg, ges) 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 -- same tests as done in TextBoxWidget:scrollUp/Down
if ges.pos.x < Screen:getWidth()/2 then if ges.pos.x < Screen:getWidth()/2 then
if self.text_widget.virtual_line_num > 1 then if self.text_widget.virtual_line_num > 1 then

@ -33,23 +33,31 @@ local Screen = require("device").screen
local TextBoxWidget = InputContainer:new{ local TextBoxWidget = InputContainer:new{
text = nil, 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. editable = false, -- Editable flag for whether drawing the cursor or not.
justified = false, -- Should text be justified (spaces widened to fill width) justified = false, -- Should text be justified (spaces widened to fill width)
alignment = "left", -- or "center", "right" 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, face = nil,
bold = nil, bold = nil,
line_height = 0.3, -- in em line_height = 0.3, -- in em
fgcolor = Blitbuffer.COLOR_BLACK, fgcolor = Blitbuffer.COLOR_BLACK,
width = Screen:scaleBySize(400), -- in pixels width = Screen:scaleBySize(400), -- in pixels
height = nil, -- nil value indicates unscrollable text widget 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, _bb = nil,
-- We can provide a list of images: each image will be displayed on each -- 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 -- 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 -- images will not be displayed at all - if more pages than images, remaining
@ -61,7 +69,7 @@ local TextBoxWidget = InputContainer:new{
-- optional: -- optional:
-- hi_width same as previous for a high-resolution version of the -- hi_width same as previous for a high-resolution version of the
-- hi_height image, to be displayed by ImageViewer when Hold on -- 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 -- title ImageViewer title
-- caption ImageViewer caption -- caption ImageViewer caption
-- --
@ -77,28 +85,46 @@ local TextBoxWidget = InputContainer:new{
} }
function TextBoxWidget:init() 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{ self.cursor_line = LineWidget:new{
dimen = Geom:new{ dimen = Geom:new{
w = Size.line.medium, w = Size.line.medium,
h = self.line_height_px, 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:_evalCharWidthList()
self:_splitCharWidthList() self:_splitCharWidthList()
if self.charpos and self.charpos > #self.charlist+1 then
self.charpos = #self.charlist+1
end
if self.height == nil then 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 else
-- luajit may segfault if we were provided with a negative height -- Show the previous displayed area in case of re-init (focus/unfocus)
if self.height < 0 then -- InputText may have re-created us, while providing the previous charlist,
self.height = 0 -- 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 end
self:_renderText(1, self:getVisLineCount())
end end
self:_renderText(self.virtual_line_num, self.virtual_line_num + self.lines_per_page - 1)
if self.editable then if self.editable then
local x, y self:moveCursorToCharPos(self.charpos or 1)
x, y = self:_findCharPos()
self.cursor_line:paintTo(self._bb, x, y)
end end
self.dimen = Geom:new(self:getSize()) self.dimen = Geom:new(self:getSize())
if Device:isTouchDevice() then if Device:isTouchDevice() then
@ -115,19 +141,21 @@ end
function TextBoxWidget:unfocus() function TextBoxWidget:unfocus()
self.editable = false self.editable = false
self:free()
self:init() self:init()
end end
function TextBoxWidget:focus() function TextBoxWidget:focus()
self.editable = true self.editable = true
self:free()
self:init() self:init()
end end
-- Split `self.text` into `self.charlist` and evaluate the width of each char in it. -- Split `self.text` into `self.charlist` and evaluate the width of each char in it.
function TextBoxWidget:_evalCharWidthList() function TextBoxWidget:_evalCharWidthList()
-- if self.charlist is provided, use it directly
if self.charlist == nil then if self.charlist == nil then
self.charlist = util.splitToChars(self.text) self.charlist = util.splitToChars(self.text)
self.charpos = #self.charlist + 1
end end
-- get width of each distinct char -- get width of each distinct char
local char_width = {} local char_width = {}
@ -149,10 +177,6 @@ function TextBoxWidget:_splitCharWidthList()
local ln = 1 local ln = 1
local offset, end_offset, cur_line_width 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 image_num = 0
local targeted_width = self.width local targeted_width = self.width
local image_lines_remaining = 0 local image_lines_remaining = 0
@ -164,8 +188,8 @@ function TextBoxWidget:_splitCharWidthList()
if self.line_num_to_image == nil then if self.line_num_to_image == nil then
self.line_num_to_image = {} self.line_num_to_image = {}
end end
if (lines_per_page and ln % lines_per_page == 1) -- first line of a scrolled page if (self.lines_per_page and ln % self.lines_per_page == 1) -- first line of a scrolled page
or (lines_per_page == nil and ln == 1) then -- first line if not scrollabled or (self.lines_per_page == nil and ln == 1) then -- first line if not scrollabled
image_num = image_num + 1 image_num = image_num + 1
if image_num <= #self.images then if image_num <= #self.images then
local image = self.images[image_num] local image = self.images[image_num]
@ -326,10 +350,6 @@ function TextBoxWidget:_getLinePads(vertical_string)
return pads return pads
end end
function TextBoxWidget:geCharWidth(idx)
return self.char_width[self.charlist[idx]]
end
function TextBoxWidget:_renderText(start_row_idx, end_row_idx) function TextBoxWidget:_renderText(start_row_idx, end_row_idx)
local font_height = self.face.size local font_height = self.face.size
if start_row_idx < 1 then start_row_idx = 1 end 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 if scheduled_for_linenum == self.virtual_line_num then
-- we are still on the same page -- we are still on the same page
self:update(true) self:update(true)
UIManager:setDirty("all", function() UIManager:setDirty(self.dialog or "all", function()
-- return "ui", self.dimen -- return "ui", self.dimen
-- We can refresh only the image area, even if we have just -- We can refresh only the image area, even if we have just
-- re-rendered the whole textbox as the text has been -- 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 -- Image loaded (or not if failure): call us again
-- with scheduled_update = true so we can draw what we got -- with scheduled_update = true so we can draw what we got
self:update(true) self:update(true)
UIManager:setDirty("all", function() UIManager:setDirty(self.dialog or "all", function()
-- return "ui", self.dimen -- return "ui", self.dimen
-- We can refresh only the image area, even if we have just -- We can refresh only the image area, even if we have just
-- re-rendered the whole textbox as the text has been -- re-rendered the whole textbox as the text has been
@ -517,79 +537,68 @@ function TextBoxWidget:_renderImage(start_row_idx)
end end
end end
-- Return the position of the cursor corresponding to `self.charpos`, function TextBoxWidget:getCharWidth(idx)
-- Be aware of virtual line number of the scorllTextWidget. return self.char_width[self.charlist[idx]]
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
function TextBoxWidget:getVisLineCount()
return self.lines_per_page
end end
-- Find the offset at the current line.
local x = 0 function TextBoxWidget:getAllLineCount()
local offset = self.vertical_string_list[ln].offset return #self.vertical_string_list
while offset < self.charpos do
x = x + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0)
offset = offset + 1
end end
return x + 1, (ln - 1) * self.line_height_px -- offset `x` by 1 to avoid overlap
function TextBoxWidget:getTextHeight()
return self.text_height
end end
function TextBoxWidget:moveCursorToCharpos(charpos) function TextBoxWidget:getLineHeight()
self.charpos = charpos return self.line_height_px
local x, y = self:_findCharPos()
self.cursor_line:paintTo(self._bb, x, y)
end end
-- Click event: Move the cursor to a new location with (x, y), in pixels. function TextBoxWidget:getVisibleHeightRatios()
-- Be aware of virtual line number of the scorllTextWidget. local low = (self.virtual_line_num - 1) / #self.vertical_string_list
function TextBoxWidget:moveCursor(x, y) local high = (self.virtual_line_num - 1 + self.lines_per_page) / #self.vertical_string_list
if x < 0 or y < 0 then return end return low, high
if #self.vertical_string_list == 0 then
-- if there's no text at all, nothing to do
return 1
end end
local w = 0
local ln = self.height == nil and 1 or self.virtual_line_num function TextBoxWidget:getCharPos()
ln = ln + math.ceil(y / self.line_height_px) - 1 -- returns virtual_line_num too
if ln > #self.vertical_string_list then return self.charpos, self.virtual_line_num
ln = #self.vertical_string_list
x = self.width
end 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 function TextBoxWidget:getSize()
while offset <= idx do if self.width and self.height then
w = w + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0) return Geom:new{ w = self.width, h = self.height}
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 else
offset = offset + 1 return Geom:new{ w = self.width, h = self._bb:getHeight()}
end
end 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 end
function TextBoxWidget:getVisLineCount() function TextBoxWidget:paintTo(bb, x, y)
return math.floor(self.height / self.line_height_px) self.dimen.x, self.dimen.y = x, y
bb:blitFrom(self._bb, x, y, 0, 0, self.width, self._bb:getHeight())
end end
function TextBoxWidget:getAllLineCount() function TextBoxWidget:free()
return #self.vertical_string_list 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 end
function TextBoxWidget:update(scheduled_update) 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 -- We set this flag so :_renderText() can know we were called from a
-- scheduled update and so not schedule another one -- scheduled update and so not schedule another one
self.scheduled_update = scheduled_update 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 self.scheduled_update = nil
end end
@ -614,7 +623,7 @@ function TextBoxWidget:onTapImage(arg, ges)
-- Toggle between image and alt_text -- Toggle between image and alt_text
self.image_show_alt_text = not self.image_show_alt_text self.image_show_alt_text = not self.image_show_alt_text
self:update() self:update()
UIManager:setDirty("all", function() UIManager:setDirty(self.dialog or "all", function()
-- return "ui", self.dimen -- return "ui", self.dimen
-- We can refresh only the image area, even if we have just -- We can refresh only the image area, even if we have just
-- re-rendered the whole textbox as the text has been -- re-rendered the whole textbox as the text has been
@ -632,100 +641,421 @@ function TextBoxWidget:onTapImage(arg, ges)
end end
end end
-- TODO: modify `charpos` so that it can render the cursor
function TextBoxWidget:scrollDown() function TextBoxWidget:scrollDown()
self.image_show_alt_text = nil -- reset image bb/alt state self.image_show_alt_text = nil -- reset image bb/alt state
local visible_line_count = self:getVisLineCount() if self.virtual_line_num + self.lines_per_page <= #self.vertical_string_list then
if self.virtual_line_num + visible_line_count <= #self.vertical_string_list then
self:free() self:free()
self.virtual_line_num = self.virtual_line_num + visible_line_count self.virtual_line_num = self.virtual_line_num + self.lines_per_page
self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) -- 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 end
return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list
end end
-- TODO: modify `charpos` so that it can render the cursor
function TextBoxWidget:scrollUp() function TextBoxWidget:scrollUp()
self.image_show_alt_text = nil self.image_show_alt_text = nil
local visible_line_count = self:getVisLineCount()
if self.virtual_line_num > 1 then if self.virtual_line_num > 1 then
self:free() 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 self.virtual_line_num = 1
else 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 + self.lines_per_page - 1)
end end
self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) 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
return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list
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
end
function TextBoxWidget:scrollToRatio(ratio) function TextBoxWidget:scrollToRatio(ratio)
self.image_show_alt_text = nil self.image_show_alt_text = nil
ratio = math.max(0, math.min(1, ratio)) -- ensure ratio is between 0 and 1 (100%) 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) / self.lines_per_page)
local page_count = 1 + math.floor((#self.vertical_string_list - 1) / visible_line_count)
local page_num = 1 + Math.round((page_count - 1) * ratio) 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 if line_num ~= self.virtual_line_num then
self:free() self:free()
self.virtual_line_num = line_num 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 end
return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list
end end
function TextBoxWidget:getSize()
if self.width and self.height then --- Cursor management
return Geom:new{ w = self.width, h = self.height}
-- 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 else
return Geom:new{ w = self.width, h = self._bb:getHeight()} 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 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 end
function TextBoxWidget:moveCursorUp() -- Return the charpos at provided coordinates (relative to current view,
if self.vertical_string_list and #self.vertical_string_list < 2 then return end -- so negative y is allowed)
local x, y function TextBoxWidget:getCharPosAtXY(x, y)
x, y = self:_findCharPos() if #self.vertical_string_list == 0 then
local charpos = self:moveCursor(x, y - self.line_height_px +1) -- if there's no text at all, nothing to do
if charpos then return 1
self:moveCursorToCharpos(charpos)
end end
return charpos 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 end_offset + 1 -- should not happen
end end
function TextBoxWidget:moveCursorDown() -- Tunables for the next function: not sure yet which combination is
if self.vertical_string_list and #self.vertical_string_list < 2 then return end -- best to get the less cursor trail - and initially got some crashes
local x, y -- when using refresh funcs. It finally feels fine with both set to true,
x, y = self:_findCharPos() -- but one can turn them to false with a setting to check how some other
local charpos = self:moveCursor(x, y + self.line_height_px +1) -- combinations do.
if charpos then local CURSOR_COMBINE_REGIONS = G_reader_settings:nilOrTrue("ui_cursor_combine_regions")
self:moveCursorToCharpos(charpos) 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 end
return charpos
end end
function TextBoxWidget:paintTo(bb, x, y) function TextBoxWidget:moveCursorToXY(x, y, restrict_to_view)
self.dimen.x, self.dimen.y = x, y if restrict_to_view then
bb:blitFrom(self._bb, x, y, 0, 0, self.width, self._bb:getHeight()) -- 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 end
function TextBoxWidget:free() -- Update self.virtual_line_num to the page containing charpos
logger.dbg("TextBoxWidget:free called") function TextBoxWidget:scrollViewToCharPos()
-- :free() is called when our parent widget is closing, and if self.top_line_num then
-- here whenever :_renderText() is being called, to display -- if previous top_line_num provided, go to that line
-- a new page: cancel any scheduled image update, as it self.virtual_line_num = self.top_line_num
-- is no more related to current page if self.virtual_line_num < 1 then
if self.image_update_action then self.virtual_line_num = 1
logger.dbg("TextBoxWidget:free: cancelling self.image_update_action")
UIManager:unschedule(self.image_update_action)
end end
if self._bb then if self.virtual_line_num > #self.vertical_string_list then
self._bb:free() self.virtual_line_num = #self.vertical_string_list
self._bb = nil 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
-- 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
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 -- Allow selection of a single word at hold position
function TextBoxWidget:onHoldWord(callback, ges) function TextBoxWidget:onHoldWord(callback, ges)
if not callback then return end if not callback then return end
@ -771,7 +1101,6 @@ function TextBoxWidget:onHoldWord(callback, ges)
return return
end end
-- Allow selection of one or more words (with no visual feedback) -- Allow selection of one or more words (with no visual feedback)
-- Gestures should be declared in widget using us (e.g dictquicklookup.lua) -- Gestures should be declared in widget using us (e.g dictquicklookup.lua)

Loading…
Cancel
Save