mirror of
https://github.com/koreader/koreader
synced 2024-11-10 01:10:34 +00:00
9b2201a438
* Added a new plugin external-keyboard. It listens to USB events. When keyboard is plugged in or plugged out, it updates device and input configuration accordingly. * Added new fake events UsbDevicePlugIn and UsbDevicePlugOut that are emitted when a device is connected to a book reader that plays the role of USB host. The usage of the existing events UsbPlugIn and UsbPlugOut has not changed - they are used when a reader is connected to a host. The koreader-base has a related PR for those events. * Did a small refactoring of initialization for the modules FocusManager and InputText. They check device keyboard capabilities on their when the module is first loaded and store it. Some of the initialization code has been extracted into functions, so that we can re-initialize them when keyboard is (dis)connected. * Initial implementation centered around text input, and tested with USB keyboards on devices with OTG support. * Said OTG shenanigans are so far supported on devices with debugfs & the chipidea driver, or sunxi devices.
964 lines
35 KiB
Lua
964 lines
35 KiB
Lua
local Blitbuffer = require("ffi/blitbuffer")
|
|
local CheckButton = require("ui/widget/checkbutton")
|
|
local Device = require("device")
|
|
local FocusManager = require("ui/widget/focusmanager")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
local Font = require("ui/font")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local Notification = require("ui/widget/notification")
|
|
local ScrollTextWidget = require("ui/widget/scrolltextwidget")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
local UIManager = require("ui/uimanager")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local dbg = require("dbg")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
local Screen = Device.screen
|
|
|
|
local Keyboard -- Conditional instantiation
|
|
local FocusManagerInstance -- Delayed instantiation
|
|
|
|
local InputText = InputContainer:extend{
|
|
text = "",
|
|
hint = "demo hint",
|
|
input_type = nil, -- "number" or anything else
|
|
text_type = nil, -- "password" or anything else
|
|
show_password_toggle = true,
|
|
cursor_at_end = true, -- starts with cursor at end of text, ready for appending
|
|
scroll = false, -- whether to allow scrolling (will be set to true if no height provided)
|
|
focused = true,
|
|
parent = nil, -- parent dialog that will be set dirty
|
|
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_by_pan = false, -- allow scrolling by lines with Pan (needs scroll=true)
|
|
|
|
width = nil,
|
|
height = nil, -- when nil, will be set to original text height (possibly
|
|
-- less if screen would be overflowed) and made scrollable to
|
|
-- not overflow if some text is appended and add new lines
|
|
|
|
face = Font:getFace("smallinfofont"),
|
|
padding = Size.padding.default,
|
|
margin = Size.margin.default,
|
|
bordersize = Size.border.inputtext,
|
|
|
|
-- See TextBoxWidget for details about these options
|
|
alignment = "left",
|
|
justified = false,
|
|
lang = nil,
|
|
para_direction_rtl = nil,
|
|
auto_para_direction = false,
|
|
alignment_strict = false,
|
|
|
|
-- for internal use
|
|
text_widget = nil, -- Text Widget for cursor movement, possibly a ScrollTextWidget
|
|
charlist = nil, -- table of individual chars from input string
|
|
charpos = nil, -- position of the cursor, where a new char would be inserted
|
|
top_line_num = nil, -- virtual_line_num of the text_widget (index of the displayed top line)
|
|
is_password_type = false, -- set to true if original text_type == "password"
|
|
is_text_editable = true, -- whether text is utf8 reversible and editing won't mess content
|
|
is_text_edited = false, -- whether text has been updated
|
|
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 = false, -- to be able to show the keyboard again when it was hidden
|
|
}
|
|
|
|
-- These may be (internally) overloaded as needed, depending on Device capabilities.
|
|
function InputText:initEventListener() end
|
|
function InputText:onFocus() end
|
|
function InputText:onUnfocus() end
|
|
|
|
local function initTouchEvents()
|
|
if Device:isTouchDevice() then
|
|
function InputText:initEventListener()
|
|
self.ges_events = {
|
|
TapTextBox = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = function() return self.dimen end
|
|
}
|
|
},
|
|
HoldTextBox = {
|
|
GestureRange:new{
|
|
ges = "hold",
|
|
range = function() return self.dimen end
|
|
}
|
|
},
|
|
HoldReleaseTextBox = {
|
|
GestureRange:new{
|
|
ges = "hold_release",
|
|
range = function() return self.dimen end
|
|
}
|
|
},
|
|
SwipeTextBox = {
|
|
GestureRange:new{
|
|
ges = "swipe",
|
|
range = function() return self.dimen end
|
|
}
|
|
},
|
|
-- 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 }
|
|
-- },
|
|
-- PanTextBox = {
|
|
-- GestureRange:new{ ges = "pan", range = self.dimen }
|
|
-- },
|
|
-- PanReleaseTextBox = {
|
|
-- GestureRange:new{ ges = "pan_release", range = self.dimen }
|
|
-- },
|
|
-- TouchTextBox = {
|
|
-- GestureRange:new{ ges = "touch", range = self.dimen }
|
|
-- },
|
|
}
|
|
end
|
|
|
|
-- For MovableContainer to work fully, some of these should
|
|
-- do more check before disabling the event or not
|
|
-- Commented for now, as this needs work
|
|
-- local function _disableEvent() return true end
|
|
-- InputText.onHoldPanTextBox = _disableEvent
|
|
-- InputText.onHoldReleaseTextBox = _disableEvent
|
|
-- InputText.onPanTextBox = _disableEvent
|
|
-- InputText.onPanReleaseTextBox = _disableEvent
|
|
-- InputText.onTouchTextBox = _disableEvent
|
|
|
|
function InputText:onTapTextBox(arg, ges)
|
|
if self.parent.onSwitchFocus then
|
|
self.parent:onSwitchFocus(self)
|
|
else
|
|
if self.is_keyboard_hidden == true then
|
|
self:onShowKeyboard()
|
|
self.is_keyboard_hidden = false
|
|
end
|
|
end
|
|
-- zh keyboard with candidates shown here has _frame_textwidget.dimen = nil.
|
|
-- Check to avoid crash.
|
|
if #self.charlist > 0 and self._frame_textwidget.dimen then -- Avoid cursor moving within a hint.
|
|
local textwidget_offset = self.margin + self.bordersize + self.padding
|
|
local x = ges.pos.x - self._frame_textwidget.dimen.x - textwidget_offset
|
|
local y = ges.pos.y - self._frame_textwidget.dimen.y - textwidget_offset
|
|
self.text_widget:moveCursorToXY(x, y, true) -- restrict_to_view=true
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function InputText:onHoldTextBox(arg, ges)
|
|
if self.parent.onSwitchFocus then
|
|
self.parent:onSwitchFocus(self)
|
|
end
|
|
-- clipboard dialog
|
|
self._hold_handled = nil
|
|
if Device:hasClipboard() then
|
|
if self.do_select then -- select mode on
|
|
if self.selection_start_pos then -- select end
|
|
local selection_end_pos = self.charpos - 1
|
|
if self.selection_start_pos > selection_end_pos then
|
|
self.selection_start_pos, selection_end_pos = selection_end_pos + 1, self.selection_start_pos - 1
|
|
end
|
|
local txt = table.concat(self.charlist, "", self.selection_start_pos, selection_end_pos)
|
|
Device.input.setClipboardText(txt)
|
|
UIManager:show(Notification:new{
|
|
text = _("Selection copied to clipboard."),
|
|
})
|
|
self.selection_start_pos = nil
|
|
self.do_select = false
|
|
return true
|
|
else -- select start
|
|
self.selection_start_pos = self.charpos
|
|
UIManager:show(Notification:new{
|
|
text = _("Set cursor to end of selection, then hold."),
|
|
})
|
|
return true
|
|
end
|
|
end
|
|
local clipboard_value = Device.input.getClipboardText()
|
|
local is_clipboard_empty = clipboard_value == nil or clipboard_value == ""
|
|
local clipboard_dialog
|
|
clipboard_dialog = require("ui/widget/textviewer"):new{
|
|
title = _("Clipboard"),
|
|
text = is_clipboard_empty and _("(empty)") or clipboard_value,
|
|
fgcolor = is_clipboard_empty and Blitbuffer.COLOR_DARK_GRAY or Blitbuffer.COLOR_BLACK,
|
|
width = math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * 0.8),
|
|
height = math.floor(math.max(Screen:getWidth(), Screen:getHeight()) * 0.4),
|
|
justified = false,
|
|
modal = true,
|
|
stop_events_propagation = true,
|
|
buttons_table = {
|
|
{
|
|
{
|
|
text = _("Copy all"),
|
|
callback = function()
|
|
UIManager:close(clipboard_dialog)
|
|
Device.input.setClipboardText(table.concat(self.charlist))
|
|
UIManager:show(Notification:new{
|
|
text = _("All text copied to clipboard."),
|
|
})
|
|
end,
|
|
},
|
|
{
|
|
text = _("Copy line"),
|
|
callback = function()
|
|
UIManager:close(clipboard_dialog)
|
|
local txt = table.concat(self.charlist, "", self:getStringPos({"\n", "\r"}, {"\n", "\r"}))
|
|
Device.input.setClipboardText(txt)
|
|
UIManager:show(Notification:new{
|
|
text = _("Line copied to clipboard."),
|
|
})
|
|
end,
|
|
},
|
|
{
|
|
text = _("Copy word"),
|
|
callback = function()
|
|
UIManager:close(clipboard_dialog)
|
|
local txt = table.concat(self.charlist, "", self:getStringPos({"\n", "\r", " "}, {"\n", "\r", " "}))
|
|
Device.input.setClipboardText(txt)
|
|
UIManager:show(Notification:new{
|
|
text = _("Word copied to clipboard."),
|
|
})
|
|
end,
|
|
},
|
|
},
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
callback = function()
|
|
UIManager:close(clipboard_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Select"),
|
|
callback = function()
|
|
UIManager:close(clipboard_dialog)
|
|
UIManager:show(Notification:new{
|
|
text = _("Set cursor to start of selection, then hold."),
|
|
})
|
|
self.do_select = true
|
|
end,
|
|
},
|
|
{
|
|
text = _("Paste"),
|
|
enabled = not is_clipboard_empty,
|
|
callback = function()
|
|
UIManager:close(clipboard_dialog)
|
|
self:addChars(clipboard_value)
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(clipboard_dialog)
|
|
end
|
|
self._hold_handled = true
|
|
return true
|
|
end
|
|
|
|
function InputText:onHoldReleaseTextBox(arg, ges)
|
|
if self._hold_handled then
|
|
self._hold_handled = nil
|
|
return true
|
|
end
|
|
return false
|
|
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
|
|
|
|
local function initDPadEvents()
|
|
if Device:hasDPad() then
|
|
function InputText:onFocus()
|
|
-- Event called by the focusmanager
|
|
if self.parent.onSwitchFocus then
|
|
self.parent:onSwitchFocus(self)
|
|
else
|
|
self:onShowKeyboard()
|
|
end
|
|
self:focus()
|
|
return true
|
|
end
|
|
|
|
function InputText:onUnfocus()
|
|
-- Event called by the focusmanager
|
|
self:unfocus()
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- only use PhysicalKeyboard if the device does not support touch input
|
|
function InputText.initInputEvents()
|
|
FocusManagerInstance = nil
|
|
|
|
if Device:isTouchDevice() or Device:hasDPad() then
|
|
Keyboard = require("ui/widget/virtualkeyboard")
|
|
initTouchEvents()
|
|
initDPadEvents()
|
|
else
|
|
Keyboard = require("ui/widget/physicalkeyboard")
|
|
end
|
|
end
|
|
|
|
InputText.initInputEvents()
|
|
|
|
function InputText:checkTextEditability()
|
|
-- The split of the 'text' string to a table of utf8 chars may not be
|
|
-- reversible to the same string, if 'text' comes from a binary file
|
|
-- (it looks like it does not necessarily need to be proper UTF8 to
|
|
-- be reversible, some text with latin1 chars is reversible).
|
|
-- As checking that may be costly, we do that only in init(), setText(),
|
|
-- and clear().
|
|
-- When not reversible, we prevent adding and deleting chars to not
|
|
-- corrupt the original self.text.
|
|
self.is_text_editable = true
|
|
if self.text then
|
|
-- We check that the text obtained from the UTF8 split done
|
|
-- in :initTextBox(), when concatenated back to a string, matches
|
|
-- the original text. (If this turns out too expensive, we could
|
|
-- just compare their lengths)
|
|
self.is_text_editable = table.concat(self.charlist, "") == self.text
|
|
end
|
|
end
|
|
|
|
function InputText:isTextEditable(show_warning)
|
|
if show_warning and not self.is_text_editable then
|
|
UIManager:show(Notification:new{
|
|
text = _("Text may be binary content, and is not editable"),
|
|
})
|
|
end
|
|
return self.is_text_editable
|
|
end
|
|
|
|
function InputText:isTextEdited()
|
|
return self.is_text_edited
|
|
end
|
|
|
|
function InputText:init()
|
|
if Device:isTouchDevice() then
|
|
if self.text_type == "password" then
|
|
-- text_type changes from "password" to "text" when we toggle password
|
|
self.is_password_type = true
|
|
end
|
|
else
|
|
-- focus move does not work with textbox and show password checkbox
|
|
-- force show password for non-touch device
|
|
self.text_type = "text"
|
|
self.is_password_type = false
|
|
end
|
|
-- Beware other cases where implicit conversion to text may be done
|
|
-- at some point, but checkTextEditability() would say "not editable".
|
|
if self.input_type == "number" then
|
|
if type(self.text) == "number" then
|
|
-- checkTextEditability() fails if self.text stays not a string
|
|
self.text = tostring(self.text)
|
|
end
|
|
if type(self.hint) == "number" then
|
|
self.hint = tostring(self.hint)
|
|
end
|
|
end
|
|
self:initTextBox(self.text)
|
|
self:checkTextEditability()
|
|
if self.readonly ~= true then
|
|
self:initKeyboard()
|
|
self:initEventListener()
|
|
end
|
|
end
|
|
|
|
-- 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)
|
|
if self.text_widget then
|
|
self.text_widget:free(true)
|
|
end
|
|
self.text = text
|
|
local fgcolor
|
|
local show_charlist
|
|
local show_text = text
|
|
if show_text == "" or show_text == nil then
|
|
-- no preset value, use hint text if set
|
|
show_text = self.hint
|
|
fgcolor = Blitbuffer.COLOR_DARK_GRAY
|
|
self.charlist = {}
|
|
self.charpos = 1
|
|
else
|
|
fgcolor = Blitbuffer.COLOR_BLACK
|
|
if self.text_type == "password" then
|
|
show_text = self.text:gsub(
|
|
"(.-).", function() return "*" end)
|
|
if char_added then
|
|
show_text = show_text:gsub(
|
|
"(.)$", function() return self.text:sub(-1) end)
|
|
end
|
|
end
|
|
self.charlist = util.splitToChars(text)
|
|
-- keep previous cursor position if charpos not nil
|
|
if self.charpos == nil then
|
|
if self.cursor_at_end then
|
|
self.charpos = #self.charlist + 1
|
|
else
|
|
self.charpos = 1
|
|
end
|
|
end
|
|
end
|
|
if self.is_password_type and self.show_password_toggle then
|
|
self._check_button = self._check_button or CheckButton:new{
|
|
text = _("Show password"),
|
|
parent = self,
|
|
width = self.width,
|
|
callback = function()
|
|
self.text_type = self._check_button.checked and "text" or "password"
|
|
self:setText(self:getText(), true)
|
|
end,
|
|
}
|
|
self._password_toggle = FrameContainer:new{
|
|
bordersize = 0,
|
|
padding = self.padding,
|
|
padding_top = 0,
|
|
padding_bottom = 0,
|
|
margin = self.margin,
|
|
self._check_button,
|
|
}
|
|
else
|
|
self._password_toggle = nil
|
|
end
|
|
show_charlist = util.splitToChars(show_text)
|
|
|
|
if not self.height then
|
|
-- If no height provided, measure the text widget height
|
|
-- we would start with, and use a ScrollTextWidget with that
|
|
-- height, so widget does not overflow container if we extend
|
|
-- the text and increase the number of lines.
|
|
local text_width = self.width
|
|
if text_width then
|
|
-- Account for the scrollbar that will be used
|
|
local scroll_bar_width = ScrollTextWidget.scroll_bar_width + ScrollTextWidget.text_scroll_span
|
|
text_width = text_width - scroll_bar_width
|
|
end
|
|
local text_widget = TextBoxWidget:new{
|
|
text = show_text,
|
|
charlist = show_charlist,
|
|
face = self.face,
|
|
width = text_width,
|
|
lang = self.lang, -- these might influence height
|
|
para_direction_rtl = self.para_direction_rtl,
|
|
auto_para_direction = self.auto_para_direction,
|
|
for_measurement_only = true, -- flag it as a dummy, so it won't trigger any bogus repaint/refresh...
|
|
}
|
|
self.height = text_widget:getTextHeight()
|
|
self.scroll = true
|
|
text_widget:free(true)
|
|
end
|
|
if self.scroll then
|
|
self.text_widget = ScrollTextWidget:new{
|
|
text = show_text,
|
|
charlist = show_charlist,
|
|
charpos = self.charpos,
|
|
top_line_num = self.top_line_num,
|
|
editable = self.focused,
|
|
face = self.face,
|
|
fgcolor = fgcolor,
|
|
alignment = self.alignment,
|
|
justified = self.justified,
|
|
lang = self.lang,
|
|
para_direction_rtl = self.para_direction_rtl,
|
|
auto_para_direction = self.auto_para_direction,
|
|
alignment_strict = self.alignment_strict,
|
|
width = self.width,
|
|
height = self.height,
|
|
dialog = self.parent,
|
|
scroll_callback = self.scroll_callback,
|
|
scroll_by_pan = self.scroll_by_pan,
|
|
for_measurement_only = self.for_measurement_only,
|
|
}
|
|
else
|
|
self.text_widget = TextBoxWidget:new{
|
|
text = show_text,
|
|
charlist = show_charlist,
|
|
charpos = self.charpos,
|
|
top_line_num = self.top_line_num,
|
|
editable = self.focused,
|
|
face = self.face,
|
|
fgcolor = fgcolor,
|
|
alignment = self.alignment,
|
|
justified = self.justified,
|
|
lang = self.lang,
|
|
para_direction_rtl = self.para_direction_rtl,
|
|
auto_para_direction = self.auto_para_direction,
|
|
alignment_strict = self.alignment_strict,
|
|
width = self.width,
|
|
height = self.height,
|
|
dialog = self.parent,
|
|
for_measurement_only = self.for_measurement_only,
|
|
}
|
|
end
|
|
-- Get back possibly modified charpos and virtual_line_num
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
|
|
self._frame_textwidget = FrameContainer:new{
|
|
bordersize = self.bordersize,
|
|
padding = self.padding,
|
|
margin = self.margin,
|
|
color = self.focused and Blitbuffer.COLOR_BLACK or Blitbuffer.COLOR_DARK_GRAY,
|
|
self.text_widget,
|
|
}
|
|
self._verticalgroup = VerticalGroup:new{
|
|
align = "left",
|
|
self._frame_textwidget,
|
|
self._password_toggle,
|
|
}
|
|
self._frame = FrameContainer:new{
|
|
bordersize = 0,
|
|
margin = 0,
|
|
padding = 0,
|
|
self._verticalgroup,
|
|
}
|
|
self[1] = self._frame
|
|
self.dimen = self._frame:getSize()
|
|
--- @fixme self.parent is not always in the widget stack (BookStatusWidget)
|
|
-- Don't even try to refresh dummy widgets used for text height computations...
|
|
if not self.for_measurement_only then
|
|
UIManager:setDirty(self.parent, function()
|
|
return "ui", self.dimen
|
|
end)
|
|
end
|
|
if self.edit_callback then
|
|
self.edit_callback(self.is_text_edited)
|
|
end
|
|
end
|
|
dbg:guard(InputText, "initTextBox",
|
|
function(self, text, char_added)
|
|
assert(type(text) == "string",
|
|
"Wrong text type (expected string)")
|
|
end)
|
|
|
|
function InputText:initKeyboard()
|
|
local keyboard_layer = 2
|
|
if self.input_type == "number" then
|
|
keyboard_layer = 4
|
|
end
|
|
self.key_events = {}
|
|
self.keyboard = Keyboard:new{
|
|
keyboard_layer = keyboard_layer,
|
|
inputbox = self,
|
|
}
|
|
end
|
|
|
|
function InputText:unfocus()
|
|
self.focused = false
|
|
self.text_widget:unfocus()
|
|
self._frame_textwidget.color = Blitbuffer.COLOR_DARK_GRAY
|
|
end
|
|
|
|
function InputText:focus()
|
|
self.focused = true
|
|
self.text_widget:focus()
|
|
self._frame_textwidget.color = Blitbuffer.COLOR_BLACK
|
|
end
|
|
|
|
-- Handle real keypresses from a physical keyboard, even if the virtual keyboard
|
|
-- is shown. Mostly likely to be in the emulator, but could be Android + BT
|
|
-- keyboard, or a "coder's keyboard" Android input method.
|
|
function InputText:onKeyPress(key)
|
|
-- only handle key on focused status, otherwise there are more than one InputText
|
|
-- the first one always handle key pressed
|
|
if not self.focused then
|
|
return false
|
|
end
|
|
local handled = true
|
|
|
|
if not key["Ctrl"] and not key["Shift"] and not key["Alt"] then
|
|
if key["Backspace"] then
|
|
self:delChar()
|
|
elseif key["Del"] then
|
|
self:delNextChar()
|
|
elseif key["Left"] then
|
|
self:leftChar()
|
|
elseif key["Right"] then
|
|
self:rightChar()
|
|
elseif key["Up"] then
|
|
self:upLine()
|
|
elseif key["Down"] then
|
|
self:downLine()
|
|
elseif key["End"] then
|
|
self:goToEnd()
|
|
elseif key["Home"] then
|
|
self:goToHome()
|
|
elseif key["Press"] then
|
|
self:addChars("\n")
|
|
elseif key["Tab"] then
|
|
self:addChars(" ")
|
|
elseif key["Back"] then
|
|
if self.focused then
|
|
self:unfocus()
|
|
end
|
|
else
|
|
handled = false
|
|
end
|
|
elseif key["Ctrl"] and not key["Shift"] and not key["Alt"] then
|
|
if key["U"] then
|
|
self:delToStartOfLine()
|
|
elseif key["H"] then
|
|
self:delChar()
|
|
else
|
|
handled = false
|
|
end
|
|
else
|
|
handled = false
|
|
end
|
|
if not handled and Device:hasDPad() then
|
|
-- FocusManager may turn on alternative key maps.
|
|
-- These key map maybe single text keys.
|
|
-- It will cause unexpected focus move instead of enter text to InputText
|
|
if not FocusManagerInstance then
|
|
FocusManagerInstance = FocusManager:new{}
|
|
end
|
|
local is_alternative_key = FocusManagerInstance:isAlternativeKey(key)
|
|
if not is_alternative_key and Device:isSDL() then
|
|
-- SDL already insert char via TextInput event
|
|
-- Stop event propagate to FocusManager
|
|
return true
|
|
end
|
|
-- if it is single text char, insert it
|
|
local key_code = key.key -- is in upper case
|
|
if not Device.isSDL() and #key_code == 1 then
|
|
if not key["Shift"] then
|
|
key_code = string.lower(key_code)
|
|
end
|
|
for modifier, flag in pairs(key.modifiers) do
|
|
if modifier ~= "Shift" and flag then -- Other modifier: not a single char insert
|
|
return true
|
|
end
|
|
end
|
|
self:addChars(key_code)
|
|
return true
|
|
end
|
|
if is_alternative_key then
|
|
return true -- Stop event propagate to FocusManager to void focus move
|
|
end
|
|
end
|
|
return handled
|
|
end
|
|
|
|
-- Handle text coming directly as text from the Device layer (eg. soft keyboard
|
|
-- or via SDL's keyboard mapping).
|
|
function InputText:onTextInput(text)
|
|
-- for more than one InputText, let the focused one add chars
|
|
if self.focused then
|
|
self:addChars(text)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
dbg:guard(InputText, "onTextInput",
|
|
function(self, text)
|
|
assert(type(text) == "string",
|
|
"Wrong text type (expected string)")
|
|
end)
|
|
|
|
function InputText:onShowKeyboard(ignore_first_hold_release)
|
|
Device:startTextInput()
|
|
self.keyboard.ignore_first_hold_release = ignore_first_hold_release
|
|
UIManager:show(self.keyboard)
|
|
return true
|
|
end
|
|
|
|
function InputText:onCloseKeyboard()
|
|
UIManager:close(self.keyboard)
|
|
Device:stopTextInput()
|
|
self.is_keyboard_hidden = true
|
|
end
|
|
|
|
function InputText:onCloseWidget()
|
|
if self.keyboard then
|
|
self.keyboard:free()
|
|
end
|
|
self:free()
|
|
end
|
|
|
|
function InputText:getTextHeight()
|
|
return self.text_widget:getTextHeight()
|
|
end
|
|
|
|
function InputText:getLineHeight()
|
|
return self.text_widget:getLineHeight()
|
|
end
|
|
|
|
function InputText:getKeyboardDimen()
|
|
if self.readonly then
|
|
return Geom:new{w = 0, h = 0}
|
|
end
|
|
return self.keyboard.dimen
|
|
end
|
|
|
|
-- calculate current and last (original) line numbers
|
|
function InputText:getLineNums()
|
|
local cur_line_num, last_line_num = 1, 1
|
|
for i = 1, #self.charlist do
|
|
if self.text_widget.charlist[i] == "\n" then
|
|
if i < self.charpos then
|
|
cur_line_num = cur_line_num + 1
|
|
end
|
|
last_line_num = last_line_num + 1
|
|
end
|
|
end
|
|
return cur_line_num, last_line_num
|
|
end
|
|
|
|
-- calculate charpos for the beginning of (original) line
|
|
function InputText:getLineCharPos(line_num)
|
|
local char_pos = 1
|
|
if line_num > 1 then
|
|
local j = 1
|
|
for i = 1, #self.charlist do
|
|
if self.charlist[i] == "\n" then
|
|
j = j + 1
|
|
if j == line_num then
|
|
char_pos = i + 1
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return char_pos
|
|
end
|
|
|
|
-- Get start and end positions of the substring
|
|
-- delimited with the delimiters and containing char_pos.
|
|
-- If char_pos not set, current charpos assumed.
|
|
function InputText:getStringPos(left_delimiter, right_delimiter, char_pos)
|
|
char_pos = char_pos and char_pos or self.charpos
|
|
local start_pos, end_pos = 1, #self.charlist
|
|
local done = false
|
|
if char_pos > 1 then
|
|
for i = char_pos, 2, -1 do
|
|
for j = 1, #left_delimiter do
|
|
if self.charlist[i-1] == left_delimiter[j] then
|
|
start_pos = i
|
|
done = true
|
|
break
|
|
end
|
|
end
|
|
if done then break end
|
|
end
|
|
end
|
|
done = false
|
|
if char_pos < #self.charlist then
|
|
for i = char_pos, #self.charlist do
|
|
for j = 1, #right_delimiter do
|
|
if self.charlist[i] == right_delimiter[j] then
|
|
end_pos = i - 1
|
|
done = true
|
|
break
|
|
end
|
|
end
|
|
if done then break end
|
|
end
|
|
end
|
|
return start_pos, end_pos
|
|
end
|
|
|
|
--- Return the character at the given offset. If is_absolute is truthy then the
|
|
-- offset is the absolute position, otherwise the offset is added to the current
|
|
-- cursor position (negative offsets are allowed).
|
|
function InputText:getChar(offset, is_absolute)
|
|
local idx
|
|
if is_absolute then
|
|
idx = offset
|
|
else
|
|
idx = self.charpos + offset
|
|
end
|
|
if idx < 1 or idx > #self.charlist then return end
|
|
return self.charlist[idx]
|
|
end
|
|
|
|
function InputText:addChars(chars)
|
|
if not chars then
|
|
-- VirtualKeyboard:addChar(key) gave us 'nil' once (?!)
|
|
-- which would crash table.concat()
|
|
return
|
|
end
|
|
if self.enter_callback and chars == "\n" then
|
|
UIManager:scheduleIn(0.3, function() self.enter_callback() end)
|
|
return
|
|
end
|
|
if self.readonly or not self:isTextEditable(true) then
|
|
return
|
|
end
|
|
|
|
self.is_text_edited = true
|
|
if #self.charlist == 0 then -- widget text is empty or a hint text is displayed
|
|
self.charpos = 1 -- move cursor to the first position
|
|
end
|
|
table.insert(self.charlist, self.charpos, chars)
|
|
self.charpos = self.charpos + #util.splitToChars(chars)
|
|
self:initTextBox(table.concat(self.charlist), true)
|
|
end
|
|
dbg:guard(InputText, "addChars",
|
|
function(self, chars)
|
|
assert(type(chars) == "string",
|
|
"Wrong chars value type (expected string)!")
|
|
end)
|
|
|
|
function InputText:delChar()
|
|
if self.readonly or not self:isTextEditable(true) then
|
|
return
|
|
end
|
|
if self.charpos == 1 then return end
|
|
self.charpos = self.charpos - 1
|
|
self.is_text_edited = true
|
|
table.remove(self.charlist, self.charpos)
|
|
self:initTextBox(table.concat(self.charlist))
|
|
end
|
|
|
|
function InputText:delNextChar()
|
|
if self.readonly or not self:isTextEditable(true) then
|
|
return
|
|
end
|
|
if self.charpos > #self.charlist then return end
|
|
self.is_text_edited = true
|
|
table.remove(self.charlist, self.charpos)
|
|
self:initTextBox(table.concat(self.charlist))
|
|
end
|
|
|
|
function InputText:delToStartOfLine()
|
|
if self.readonly or not self:isTextEditable(true) then
|
|
return
|
|
end
|
|
if self.charpos == 1 then return end
|
|
-- self.charlist[self.charpos] is the char after the cursor
|
|
if self.charlist[self.charpos-1] == "\n" then
|
|
-- If at start of line, just remove the \n and join the previous line
|
|
self.charpos = self.charpos - 1
|
|
table.remove(self.charlist, self.charpos)
|
|
else
|
|
-- If not, remove chars until first found \n (but keeping it)
|
|
while self.charpos > 1 and self.charlist[self.charpos-1] ~= "\n" do
|
|
self.charpos = self.charpos - 1
|
|
table.remove(self.charlist, self.charpos)
|
|
end
|
|
end
|
|
self.is_text_edited = true
|
|
self:initTextBox(table.concat(self.charlist))
|
|
end
|
|
|
|
-- For the following cursor/scroll methods, the text_widget deals
|
|
-- itself with setDirty'ing the appropriate regions
|
|
function InputText:leftChar()
|
|
if self.charpos == 1 then return end
|
|
self.text_widget:moveCursorLeft()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
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()
|
|
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()
|
|
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()
|
|
end
|
|
|
|
function InputText:goToHome()
|
|
self.text_widget:moveCursorHome()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:goToEnd()
|
|
self.text_widget:moveCursorEnd()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:moveCursorToCharPos(char_pos)
|
|
self.text_widget:moveCursorToCharPos(char_pos)
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:upLine()
|
|
self.text_widget:moveCursorUp()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
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()
|
|
end
|
|
|
|
function InputText:scrollDown()
|
|
self.text_widget:scrollDown()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:scrollUp()
|
|
self.text_widget:scrollUp()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:scrollToTop()
|
|
self.text_widget:scrollToTop()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:scrollToBottom()
|
|
self.text_widget:scrollToBottom()
|
|
self.charpos, self.top_line_num = self.text_widget:getCharPos()
|
|
end
|
|
|
|
function InputText:clear()
|
|
self.charpos = nil
|
|
self.top_line_num = 1
|
|
self.is_text_edited = true
|
|
self:initTextBox("")
|
|
self:checkTextEditability()
|
|
end
|
|
|
|
function InputText:getText()
|
|
return self.text
|
|
end
|
|
|
|
function InputText:setText(text, keep_edited_state)
|
|
-- Keep previous charpos and top_line_num
|
|
self:initTextBox(text)
|
|
if not keep_edited_state then
|
|
-- assume new text is set by caller, and we start fresh
|
|
self.is_text_edited = false
|
|
self:checkTextEditability()
|
|
end
|
|
end
|
|
dbg:guard(InputText, "setText",
|
|
function(self, text, keep_edited_state)
|
|
assert(type(text) == "string",
|
|
"Wrong text type (expected string)")
|
|
end)
|
|
|
|
return InputText
|