VirtualKeyboard: Revamp visibility handling (#10852)

Move as much of the state tracking as possible inside VirtualKeyboard itself.
InputDialog unfortunately needs an internal tracking of this state because it needs to know about it *before* the VK is shown, so we have to keep a bit of duplication in there, although we do try much harder to keep everything in sync (at least at function call edges), and to keep the damage contained to, essentially, the toggle button's handler.

(Followup to #10803 & #10850)
reviewable/pr10874/r1
NiLuJe 8 months ago committed by GitHub
parent 4d620d9fd2
commit 4cc620b702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -920,10 +920,10 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
end
end
end,
-- Set/save view and cursor position callback
-- Store/retrieve view and cursor position callback
view_pos_callback = function(top_line_num, charpos)
-- This same callback is called with no argument to get initial position,
-- and with arguments to give back final position when closed.
-- This same callback is called with no arguments on init to retrieve the stored initial position,
-- and with arguments to store the final position on close.
if top_line_num and charpos then
self.book_style_tweak_last_edit_pos = {top_line_num, charpos}
else

@ -141,7 +141,7 @@ local InputDialog = FocusManager:extend{
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
keyboard_hidden = false, -- start with keyboard hidden in full fullscreen mode
keyboard_visible = true, -- whether we start with the keyboard visible or not (i.e., our caller skipped onShowKeyboard)
-- needs add_nav_bar to have a Show keyboard button to get it back
scroll_by_pan = false, -- allow scrolling by lines with Pan (= Swipe, but wait a bit at end
-- of gesture before releasing) (may conflict with movable)
@ -162,8 +162,8 @@ local InputDialog = FocusManager:extend{
edited_callback = nil, -- Called on each text modification
-- For use by TextEditor plugin:
view_pos_callback = nil, -- Called with no arg to get initial top_line_num/charpos,
-- called with (top_line_num, charpos) to give back position on close.
view_pos_callback = nil, -- Called with no args on init to retrieve top_line_num/charpos (however the caller chooses to do so, e.g., some will store it in a LuaSettings),
-- called with (top_line_num, charpos) on close to let the callback do its thing so that the no args branch spits back useful data..
-- Set to false if movable gestures conflicts with subwidgets gestures
is_movable = true,
@ -189,6 +189,7 @@ local InputDialog = FocusManager:extend{
alignment_strict = false,
-- for internal use
_keyboard_was_visible = nil, -- previous kb visibility state
_text_modified = false, -- previous known modified status
_top_line_num = nil,
_charpos = nil,
@ -216,7 +217,7 @@ function InputDialog:init()
self.text_width = self.text_width or math.floor(self.width * 0.9)
end
if self.readonly then -- hide keyboard if we can't edit
self.keyboard_hidden = true
self.keyboard_visible = false
end
if self.fullscreen or self.add_nav_bar then
self.deny_keyboard_hiding = true
@ -299,10 +300,7 @@ function InputDialog:init()
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 = 0
if not self.keyboard_hidden then
keyboard_height = input_widget:getKeyboardDimen().h
end
local keyboard_height = self.keyboard_visible and input_widget:getKeyboardDimen().h or 0
input_widget:onCloseWidget() -- free() textboxwidget and keyboard
-- Find out available height
local available_height = self.screen_height
@ -331,9 +329,14 @@ function InputDialog:init()
end
end
if self.view_pos_callback then
-- Get initial cursor and top line num from callback
-- (will work in case of re-init as these are saved by onClose()
self._top_line_num, self._charpos = self.view_pos_callback()
-- Retrieve cursor position and top line num from our callback.
-- Mainly used for runtime re-inits.
-- c.f., our onClose handler for the other end of this.
-- *May* return nils, in which case, we do *not* want to override our caller's values!
local top_line_num, charpos = self.view_pos_callback()
if top_line_num and charpos then
self._top_line_num, self._charpos = top_line_num, charpos
end
end
self._input_widget = self.inputtext_class:new{
text = self.input,
@ -368,7 +371,6 @@ function InputDialog:init()
scroll_by_pan = self.scroll_by_pan,
cursor_at_end = self.cursor_at_end,
readonly = self.readonly,
manage_keyboard_state = not self.add_nav_bar, -- we handle keyboard toggle ourselve if nav_bar
parent = self,
is_text_edited = self._text_modified,
top_line_num = self._top_line_num,
@ -423,8 +425,7 @@ function InputDialog:init()
}
frame = self.movable
end
local keyboard_height = self.keyboard_hidden and 0
or self._input_widget:getKeyboardDimen().h
local keyboard_height = self.keyboard_visible and self._input_widget:getKeyboardDimen().h or 0
self[1] = CenterContainer:new{
dimen = Geom:new{
w = self.screen_width,
@ -449,6 +450,11 @@ function InputDialog:init()
self:addWidget(widget, true)
end
end
-- If we're fullscreen without a keyboard, make sure only the toggle button can show the keyboard...
if self.fullscreen and not self.keyboard_visible then
self:lockKeyboard(true)
end
end
function InputDialog:addWidget(widget, re_init)
@ -474,13 +480,13 @@ function InputDialog:getAddedWidgetAvailableWidth()
return self._input_widget.width
end
-- Close the keyboard if we tap anywhere outside of the keyboard (that isn't an input field, where it would be caught via InputText:onTapTextBox)
function InputDialog:onTap()
-- This is slightly more fine-grained than VK's own visibility lock, hence the duplication...
if self.deny_keyboard_hiding then
return
end
if self._input_widget.onCloseKeyboard then
self._input_widget:onCloseKeyboard()
end
self:onCloseKeyboard()
end
function InputDialog:getInputText()
@ -525,24 +531,79 @@ function InputDialog:onCloseWidget()
end
function InputDialog:onShowKeyboard(ignore_first_hold_release)
if not self.readonly and not self.keyboard_hidden then
self._input_widget:onShowKeyboard(ignore_first_hold_release)
end
-- NOTE: There's no VirtualKeyboard widget instantiated at all when readonly,
-- and our input widget handles that itself, so we don't need any guards here.
-- (In which case, isKeyboardVisible will return `nil`, same as if we had a VK instantiated but *never* shown).
self._input_widget:onShowKeyboard(ignore_first_hold_release)
-- There's a bit of a chicken or egg issue in init where we would like to check the actual keyboard's visibility state,
-- but the widget might not exist or be shown yet, so we'll just have to keep this in sync...
self.keyboard_visible = self._input_widget:isKeyboardVisible()
end
function InputDialog:onCloseKeyboard()
self._input_widget:onCloseKeyboard()
self.keyboard_visible = self._input_widget:isKeyboardVisible()
end
function InputDialog:isKeyboardVisible()
return self._input_widget:isKeyboardVisible()
end
function InputDialog:toggleKeyboard(force_hide)
if force_hide and self.keyboard_hidden then return end
self.keyboard_hidden = not self.keyboard_hidden
function InputDialog:lockKeyboard(toggle)
return self._input_widget:lockKeyboard(toggle)
end
-- NOTE: Only called by fullscreen and/or add_nav_bar codepaths
-- We do not currently have !fullscreen add_nav_bar callers...
function InputDialog:toggleKeyboard(force_toggle)
-- Remember the *current* visibility, as the following close will reset it
local visible = self:isKeyboardVisible()
-- When we forcibly close the keyboard, remember its current visiblity state, so that we can properly restore it later.
-- (This is used by some buttons in fullscreen mode, where we might want to keep the original keyboard hidden when popping up a new one for another InputDialog).
if force_toggle == false then
-- NOTE: visible will be nil between our own init and a show of the keyboard, which is precisely what happens when we *hide* the keyboard.
self._keyboard_was_visible = visible == true
end
self.input = self:getInputText() -- re-init with up-to-date text
self:onClose() -- will close keyboard and save view position
self:free()
if force_toggle == false and not visible then
-- Already hidden, bye!
return
end
-- Init needs to know the keyboard's visibility state *before* the widget is actually shown...
if force_toggle == true then
self.keyboard_visible = true
elseif force_toggle == false then
self.keyboard_visible = false
elseif self._keyboard_was_visible ~= nil then
self.keyboard_visible = self._keyboard_was_visible
self._keyboard_was_visible = nil
else
self.keyboard_visible = not visible
end
self:init()
if not self.keyboard_hidden then
-- NOTE: If we ever have non-fullscreen add_nav_bar callers, it might make sense *not* to lock the keyboard there?
if self.keyboard_visible then
self:lockKeyboard(false)
self:onShowKeyboard()
else
self:onCloseKeyboard()
-- Prevent InputText:onTapTextBox from opening the keyboard back up on top of our buttons
self:lockKeyboard(true)
end
-- Make sure we refresh the nav bar, as it will have moved, and it belongs to us, not to VK or our input widget...
self:refreshButtons()
end
function InputDialog:onKeyboardHeightChanged()
local visible = self:isKeyboardVisible()
self.input = self:getInputText() -- re-init with up-to-date text
self:onClose() -- will close keyboard and save view position
self._input_widget:onCloseWidget() -- proper cleanup of InputText and its keyboard
@ -555,8 +616,11 @@ function InputDialog:onKeyboardHeightChanged()
self:free()
-- Restore original text_height (or reset it if none to force recomputing it)
self.text_height = self.orig_text_height or nil
-- Same deal as in toggleKeyboard...
self.keyboard_visible = visible
self:init()
if not self.keyboard_hidden then
if self.keyboard_visible then
self:onShowKeyboard()
end
-- Our position on screen has probably changed, so have the full screen refreshed
@ -573,14 +637,16 @@ function InputDialog:onCloseDialog()
end
function InputDialog:onClose()
-- Tell our input widget to poke its text widget so that we'll pickup up to date values
self._input_widget:resyncPos()
-- Remember current view & position in case of re-init
self._top_line_num = self._input_widget.top_line_num
self._charpos = self._input_widget.charpos
if self.view_pos_callback then
-- Give back top line num and cursor position
-- This lets the caller store/process the current top line num and cursor position via this callback
self.view_pos_callback(self._top_line_num, self._charpos)
end
self._input_widget:onCloseKeyboard()
self:onCloseKeyboard()
end
function InputDialog:refreshButtons()
@ -764,7 +830,7 @@ function InputDialog:_addScrollButtons(nav_bar)
-- Also add Keyboard hide/show button if we can
if self.fullscreen and not self.readonly then
table.insert(row, {
text = self.keyboard_hidden and "↑⌨" or "",
text = self.keyboard_visible and "↓⌨" or "",
id = "keyboard",
callback = function()
self:toggleKeyboard()
@ -776,8 +842,7 @@ function InputDialog:_addScrollButtons(nav_bar)
table.insert(row, {
text = _("Find"),
callback = function()
local keyboard_hidden_state = not self.keyboard_hidden
self:toggleKeyboard(true) -- hide text editor keyboard
self:toggleKeyboard(false) -- hide text editor keyboard
local input_dialog
input_dialog = InputDialog:new{
title = _("Enter text to search for"),
@ -790,21 +855,20 @@ function InputDialog:_addScrollButtons(nav_bar)
id = "close",
callback = function()
UIManager:close(input_dialog)
self.keyboard_hidden = keyboard_hidden_state
self:toggleKeyboard()
end,
},
{
text = _("Find first"),
callback = function()
self:findCallback(keyboard_hidden_state, input_dialog, true)
self:findCallback(input_dialog, true)
end,
},
{
text = _("Find next"),
is_enter_default = true,
callback = function()
self:findCallback(keyboard_hidden_state, input_dialog)
self:findCallback(input_dialog)
end,
},
},
@ -829,8 +893,7 @@ function InputDialog:_addScrollButtons(nav_bar)
table.insert(row, {
text = _("Go"),
callback = function()
local keyboard_hidden_state = not self.keyboard_hidden
self:toggleKeyboard(true) -- hide text editor keyboard
self:toggleKeyboard(false) -- hide text editor keyboard
local cur_line_num, last_line_num = self._input_widget:getLineNums()
local input_dialog
input_dialog = InputDialog:new{
@ -846,7 +909,6 @@ function InputDialog:_addScrollButtons(nav_bar)
id = "close",
callback = function()
UIManager:close(input_dialog)
self.keyboard_hidden = keyboard_hidden_state
self:toggleKeyboard()
end,
},
@ -857,7 +919,6 @@ function InputDialog:_addScrollButtons(nav_bar)
local new_line_num = tonumber(input_dialog:getInputText())
if new_line_num and new_line_num >= 1 and new_line_num <= last_line_num then
UIManager:close(input_dialog)
self.keyboard_hidden = keyboard_hidden_state
self:toggleKeyboard()
self._input_widget:moveCursorToCharPos(self._input_widget:getLineCharPos(new_line_num))
end
@ -939,11 +1000,10 @@ function InputDialog:_addScrollButtons(nav_bar)
end
end
function InputDialog:findCallback(keyboard_hidden_state, input_dialog, find_first)
function InputDialog:findCallback(input_dialog, find_first)
self.search_value = input_dialog:getInputText()
if self.search_value == "" then return end
UIManager:close(input_dialog)
self.keyboard_hidden = keyboard_hidden_state
self:toggleKeyboard()
local start_pos = find_first and 1 or self._charpos + 1
local char_pos = util.stringSearch(self.input, self.search_value, self.case_sensitive, start_pos)

@ -32,9 +32,8 @@ local InputText = InputContainer:extend{
focused = true,
parent = nil, -- parent dialog that will be set dirty
edit_callback = nil, -- called with true when text modified, false on init or text re-set
scroll_callback = nil, -- called with (low, high) when view is scrolled (cf ScrollTextWidget)
scroll_callback = nil, -- called with (low, high) when view is scrolled (c.f., ScrollTextWidget)
scroll_by_pan = false, -- allow scrolling by lines with Pan (needs scroll=true)
manage_keyboard_state = true, -- manage keyboard hidden/shown state
width = nil,
height = nil, -- when nil, will be set to original text height (possibly
@ -54,7 +53,10 @@ local InputText = InputContainer:extend{
auto_para_direction = false,
alignment_strict = false,
readonly = nil, -- will not support a Keyboard widget if true
-- for internal use
keyboard = nil, -- Keyboard widget (either VirtualKeyboard or PhysicalKeyboard)
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
@ -65,7 +67,6 @@ local InputText = InputContainer:extend{
for_measurement_only = nil, -- When the widget is a one-off used to compute text height
do_select = false, -- to start text selection
selection_start_pos = nil, -- selection start position
is_keyboard_hidden = true, -- to be able to show the keyboard again when it was hidden. (On init, it's the caller's responsibility to call onShowKeyboard, as far as we're concerned, it's hidden)
}
-- These may be (internally) overloaded as needed, depending on Device capabilities.
@ -73,6 +74,11 @@ function InputText:initEventListener() end
function InputText:onFocus() end
function InputText:onUnfocus() end
-- Resync our position state with our text widget's actual state
function InputText:resyncPos()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
end
local function initTouchEvents()
if Device:isTouchDevice() then
function InputText:initEventListener()
@ -133,8 +139,8 @@ local function initTouchEvents()
if self.parent.onSwitchFocus then
self.parent:onSwitchFocus(self)
else
if self.is_keyboard_hidden and self.manage_keyboard_state then
self:onShowKeyboard()
if self.keyboard then
self.keyboard:showKeyboard()
end
end
-- zh keyboard with candidates shown here has _frame_textwidget.dimen = nil.
@ -144,7 +150,7 @@ local function initTouchEvents()
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()
self:resyncPos()
end
return true
end
@ -512,7 +518,7 @@ function InputText:initTextBox(text, char_added)
}
end
-- Get back possibly modified charpos and virtual_line_num
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
self._frame_textwidget = FrameContainer:new{
bordersize = self.bordersize,
@ -676,22 +682,27 @@ dbg:guard(InputText, "onTextInput",
end)
function InputText:onShowKeyboard(ignore_first_hold_release)
Device:startTextInput()
if self.is_keyboard_hidden or not self.manage_keyboard_state then
self.keyboard.ignore_first_hold_release = ignore_first_hold_release
UIManager:show(self.keyboard)
self.is_keyboard_hidden = false
if self.keyboard then
self.keyboard:showKeyboard(ignore_first_hold_release)
end
return true
end
function InputText:onCloseKeyboard()
Device:stopTextInput()
if self.keyboard then
self.keyboard:hideKeyboard()
end
end
function InputText:isKeyboardVisible()
if self.keyboard then
return self.keyboard:isVisible()
end
end
if not self.is_keyboard_hidden or not self.manage_keyboard_state then
UIManager:close(self.keyboard)
self.is_keyboard_hidden = true
function InputText:lockKeyboard(toggle)
if self.keyboard then
return self.keyboard:lockVisibility(toggle)
end
end
@ -873,71 +884,71 @@ end
function InputText:leftChar()
if self.charpos == 1 then return end
self.text_widget:moveCursorLeft()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:rightChar()
if self.charpos > #self.charlist then return end
self.text_widget:moveCursorRight()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:goToStartOfLine()
local new_pos = select(1, self:getStringPos({"\n", "\r"}, {"\n", "\r"}))
self.text_widget:moveCursorToCharPos(new_pos)
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:goToEndOfLine()
local new_pos = select(2, self:getStringPos({"\n", "\r"}, {"\n", "\r"})) + 1
self.text_widget:moveCursorToCharPos(new_pos)
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:goToHome()
self.text_widget:moveCursorHome()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:goToEnd()
self.text_widget:moveCursorEnd()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:moveCursorToCharPos(char_pos)
self.text_widget:moveCursorToCharPos(char_pos)
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:upLine()
self.text_widget:moveCursorUp()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:downLine()
if #self.charlist == 0 then return end -- Avoid cursor moving within a hint.
self.text_widget:moveCursorDown()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:scrollDown()
self.text_widget:scrollDown()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:scrollUp()
self.text_widget:scrollUp()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:scrollToTop()
self.text_widget:scrollToTop()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:scrollToBottom()
self.text_widget:scrollToBottom()
self.charpos, self.top_line_num = self.text_widget:getCharPos()
self:resyncPos()
end
function InputText:clear()

@ -218,7 +218,9 @@ end
function MultiInputDialog:onSwitchFocus(inputbox)
-- unfocus current inputbox
self._input_widget:unfocus()
self._input_widget:onCloseKeyboard()
-- and close its existing keyboard (via InputDialog's thin wrapper around _input_widget's own method)
self:onCloseKeyboard()
UIManager:setDirty(nil, function()
return "ui", self.dialog_frame.dimen
end)
@ -226,7 +228,9 @@ function MultiInputDialog:onSwitchFocus(inputbox)
-- focus new inputbox
self._input_widget = inputbox
self._input_widget:focus()
self._input_widget:onShowKeyboard()
-- Make sure we have a (new) visible keyboard
self:onShowKeyboard()
end
return MultiInputDialog

@ -169,4 +169,11 @@ function PhysicalKeyboard:setupNumericMappingUI()
self.dimen = keyboard_frame:getSize()
end
-- Match VirtualKeyboard's API to ease caller's life
function PhysicalKeyboard:lockVisibility() end
function PhysicalKeyboard:setVisibility() end
function PhysicalKeyboard:isVisible() return true end
function PhysicalKeyboard:showKeyboard() end
function PhysicalKeyboard:hideKeyboard() end
return PhysicalKeyboard

@ -755,6 +755,8 @@ end
local VirtualKeyboard = FocusManager:extend{
name = "VirtualKeyboard",
visible = nil,
lock_visibility = false,
covers_footer = true,
modal = true,
disable_double_tap = true,
@ -929,11 +931,53 @@ end
function VirtualKeyboard:onShow()
self:_refresh(true)
self.visible = true
Device:startTextInput()
return true
end
function VirtualKeyboard:onCloseWidget()
self:_refresh(true)
self.visible = false
-- NOTE: This effectively stops SDL text input when a keyboard is hidden (... but navigational stuff still works).
-- If you instead wanted it to be enabled as long as an input dialog is displayed, regardless of VK's state,
-- this could be moved to InputDialog's onShow/onCloseWidget handlers (but, it would allow input on unfocused fields).
-- NOTE: But something more complex, possibly based on an in-class ref count would have to be implemented in order to be able to deal
-- with multiple InputDialogs being shown and closed in asymmetric fashion... Ugh.
Device:stopTextInput()
end
function VirtualKeyboard:lockVisibility(toggle)
self.lock_visibility = toggle
end
function VirtualKeyboard:setVisibility(toggle)
if self.lock_visibility then
return
end
if toggle then
UIManager:show(self)
else
self:onClose()
end
end
function VirtualKeyboard:isVisible()
return self.visible
end
function VirtualKeyboard:showKeyboard(ignore_first_hold_release)
if not self:isVisible() then
self.ignore_first_hold_release = ignore_first_hold_release
self:setVisibility(true)
end
end
function VirtualKeyboard:hideKeyboard()
if self:isVisible() then
self:setVisibility(false)
end
end
function VirtualKeyboard:initLayer(layer)

@ -545,13 +545,13 @@ function TextEditor:editFile(file_path, readonly)
cursor_at_end = false,
readonly = readonly,
add_nav_bar = true,
keyboard_hidden = not self.show_keyboard_on_start,
keyboard_visible = self.show_keyboard_on_start, -- InputDialog will enforce false if readonly
scroll_by_pan = true,
buttons = {buttons_first_row},
-- Set/save view and cursor position callback
-- Store/retrieve view and cursor position callback
view_pos_callback = function(top_line_num, charpos)
-- This same callback is called with no argument to get initial position,
-- and with arguments to give back final position when closed.
-- This same callback is called with no arguments on init to retrieve the stored initial position,
-- and with arguments to store the final position on close.
if top_line_num and charpos then
self.last_view_pos[file_path] = {top_line_num, charpos}
else
@ -572,7 +572,7 @@ function TextEditor:editFile(file_path, readonly)
end,
-- File saving callback
save_callback = function(content, closing) -- Will add Save/Close buttons
if self.readonly then
if readonly then
-- We shouldn't be called if read-only, but just in case
return false, _("File is read only")
end
@ -641,8 +641,10 @@ Do you want to keep this file as empty, or do you prefer to delete it?
}
UIManager:show(input)
input:onShowKeyboard()
-- Note about self.readonly:
if self.show_keyboard_on_start and not readonly then
input:onShowKeyboard()
end
-- Note about readonly:
-- We might have liked to still show keyboard even if readonly, just
-- to use the arrow keys for line by line scrolling with cursor.
-- But it's easier to just let InputDialog and InputText do their

Loading…
Cancel
Save