mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
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.
This commit is contained in:
@@ -37,6 +37,15 @@ Example:
|
||||
UIManager:show(sample_input)
|
||||
sample_input:onShowKeyboard()
|
||||
|
||||
To get a full screen text editor, use:
|
||||
fullscreen = true, -- no need to provide any height and width
|
||||
condensed = true,
|
||||
allow_newline = true,
|
||||
cursor_at_end = false,
|
||||
-- and one of these:
|
||||
add_scroll_buttons = true,
|
||||
add_nav_bar = true,
|
||||
|
||||
If it would take the user more than half a minute to recover from a mistake,
|
||||
a "Cancel" button <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.
|
||||
@@ -76,6 +85,19 @@ local InputDialog = InputContainer:new{
|
||||
buttons = nil,
|
||||
input_type = nil,
|
||||
enter_callback = nil,
|
||||
allow_newline = false, -- allow entering new lines (this disables any enter_callback)
|
||||
cursor_at_end = true, -- starts with cursor at end of text, ready for appending
|
||||
fullscreen = false, -- adjust to full screen minus keyboard
|
||||
condensed = false, -- true will prevent adding air and balance between elements
|
||||
add_scroll_buttons = false, -- add scroll Up/Down buttons to first row of buttons
|
||||
add_nav_bar = false, -- append a row of page navigation buttons
|
||||
-- note that the text widget can be scrolled with Swipe North/South even when no button
|
||||
|
||||
-- movable = true, -- set to false if movable gestures conflicts with subwidgets gestures
|
||||
-- for now, too much conflicts between InputText and MovableContainer, and
|
||||
-- there's the keyboard to exclude from move area (the InputDialog could
|
||||
-- be moved under the keyboard, and the user would be locked)
|
||||
movable = false,
|
||||
|
||||
width = nil,
|
||||
|
||||
@@ -88,13 +110,29 @@ local InputDialog = InputContainer:new{
|
||||
|
||||
title_padding = Size.padding.default,
|
||||
title_margin = Size.margin.title,
|
||||
input_padding = Size.padding.large,
|
||||
desc_padding = Size.padding.default, -- Use the same as title for their
|
||||
desc_margin = Size.margin.title, -- texts to be visually aligned
|
||||
input_padding = Size.padding.default,
|
||||
input_margin = Size.margin.default,
|
||||
button_padding = Size.padding.default,
|
||||
border_size = Size.border.window,
|
||||
}
|
||||
|
||||
function InputDialog:init()
|
||||
self.width = self.width or Screen:getWidth() * 0.8
|
||||
if self.fullscreen then
|
||||
self.movable = false
|
||||
self.border_size = 0
|
||||
self.width = Screen:getWidth() - 2*self.border_size
|
||||
else
|
||||
self.width = self.width or Screen:getWidth() * 0.8
|
||||
end
|
||||
if self.condensed then
|
||||
self.text_width = self.width - 2*(self.border_size + self.input_padding + self.input_margin)
|
||||
else
|
||||
self.text_width = self.text_width or self.width * 0.9
|
||||
end
|
||||
|
||||
-- Title & description
|
||||
local title_width = RenderText:sizeUtf8Text(0, self.width,
|
||||
self.title_face, self.title, true).x
|
||||
if title_width > self.width then
|
||||
@@ -114,28 +152,159 @@ function InputDialog:init()
|
||||
width = self.width,
|
||||
}
|
||||
}
|
||||
|
||||
self.title_bar = LineWidget:new{
|
||||
dimen = Geom:new{
|
||||
w = self.width,
|
||||
h = Size.line.thick,
|
||||
}
|
||||
}
|
||||
if self.description then
|
||||
self.description = FrameContainer:new{
|
||||
padding = self.title_padding,
|
||||
margin = self.title_margin,
|
||||
self.description_widget = FrameContainer:new{
|
||||
padding = self.desc_padding,
|
||||
margin = self.desc_margin,
|
||||
bordersize = 0,
|
||||
TextBoxWidget:new{
|
||||
text = self.description,
|
||||
face = self.description_face,
|
||||
width = self.width - 2*self.title_padding - 2*self.title_margin,
|
||||
width = self.width - 2*self.desc_padding - 2*self.desc_margin,
|
||||
}
|
||||
}
|
||||
else
|
||||
self.description = VerticalSpan:new{ width = self.title_margin + self.title_padding }
|
||||
self.description_widget = VerticalSpan:new{ width = 0 }
|
||||
end
|
||||
|
||||
-- Vertical spaces added before and after InputText
|
||||
-- (these will be adjusted later to center the input text if needed)
|
||||
local vspan_before_input_text = VerticalSpan:new{ width = 0 }
|
||||
local vspan_after_input_text = VerticalSpan:new{ width = 0 }
|
||||
-- We add the same vertical space used under description after the input widget
|
||||
-- (can be disabled by setting condensed=true)
|
||||
if not self.condensed then
|
||||
local desc_pad_height = self.desc_margin + self.desc_padding
|
||||
if self.description then
|
||||
vspan_before_input_text.width = 0 -- already provided by description_widget
|
||||
vspan_after_input_text.width = desc_pad_height
|
||||
else
|
||||
vspan_before_input_text.width = desc_pad_height
|
||||
vspan_after_input_text.width = desc_pad_height
|
||||
end
|
||||
end
|
||||
|
||||
-- Buttons
|
||||
if self.add_nav_bar then
|
||||
if not self.buttons then
|
||||
self.buttons = {}
|
||||
end
|
||||
local nav_bar = {}
|
||||
table.insert(self.buttons, nav_bar)
|
||||
table.insert(nav_bar, {
|
||||
text = "⇱",
|
||||
callback = function()
|
||||
self._input_widget:scrollToTop()
|
||||
end,
|
||||
})
|
||||
table.insert(nav_bar, {
|
||||
text = "⇲",
|
||||
callback = function()
|
||||
self._input_widget:scrollToBottom()
|
||||
end,
|
||||
})
|
||||
table.insert(nav_bar, {
|
||||
text = "△",
|
||||
callback = function()
|
||||
self._input_widget:scrollUp()
|
||||
end,
|
||||
})
|
||||
table.insert(nav_bar, {
|
||||
text = "▽",
|
||||
callback = function()
|
||||
self._input_widget:scrollDown()
|
||||
end,
|
||||
})
|
||||
elseif self.add_scroll_buttons then
|
||||
if not self.buttons then
|
||||
self.buttons = {{}}
|
||||
end
|
||||
-- Add them to the end of first row
|
||||
table.insert(self.buttons[1], {
|
||||
text = "△",
|
||||
callback = function()
|
||||
self._input_widget:scrollUp()
|
||||
end,
|
||||
})
|
||||
table.insert(self.buttons[1], {
|
||||
text = "▽",
|
||||
callback = function()
|
||||
self._input_widget:scrollDown()
|
||||
end,
|
||||
})
|
||||
end
|
||||
self.button_table = ButtonTable:new{
|
||||
width = self.width - 2*self.button_padding,
|
||||
button_font_face = "cfont",
|
||||
button_font_size = 20,
|
||||
buttons = self.buttons,
|
||||
zero_sep = true,
|
||||
show_parent = self,
|
||||
}
|
||||
local buttons_container = CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.width,
|
||||
h = self.button_table:getSize().h,
|
||||
},
|
||||
self.button_table,
|
||||
}
|
||||
|
||||
-- InputText
|
||||
if not self.text_height or self.fullscreen then
|
||||
-- We need to find the best height to avoid screen overflow
|
||||
-- Create a dummy input widget to get some metrics
|
||||
local input_widget = InputText:new{
|
||||
text = self.fullscreen and "-" or self.input,
|
||||
face = self.input_face,
|
||||
width = self.text_width,
|
||||
padding = self.input_padding,
|
||||
margin = self.input_margin,
|
||||
}
|
||||
local text_height = input_widget:getTextHeight()
|
||||
local line_height = input_widget:getLineHeight()
|
||||
local input_pad_height = input_widget:getSize().h - text_height
|
||||
local keyboard_height = input_widget:getKeyboardDimen().h
|
||||
input_widget:free()
|
||||
-- Find out available height
|
||||
local available_height = Screen:getHeight()
|
||||
- 2*self.border_size
|
||||
- self.title:getSize().h
|
||||
- self.title_bar:getSize().h
|
||||
- self.description_widget:getSize().h
|
||||
- vspan_before_input_text:getSize().h
|
||||
- input_pad_height
|
||||
- vspan_after_input_text:getSize().h
|
||||
- buttons_container:getSize().h
|
||||
- keyboard_height
|
||||
if self.fullscreen or text_height > available_height then
|
||||
-- Don't leave unusable space in the text widget, as the user could think
|
||||
-- it's an empty line: move that space in pads after and below (for centering)
|
||||
self.text_height = math.floor(available_height / line_height) * line_height
|
||||
local pad_height = available_height - self.text_height
|
||||
local pad_before = math.ceil(pad_height / 2)
|
||||
local pad_after = pad_height - pad_before
|
||||
vspan_before_input_text.width = vspan_before_input_text.width + pad_before
|
||||
vspan_after_input_text.width = vspan_after_input_text.width + pad_after
|
||||
self.cursor_at_end = false -- stay at start if overflowed
|
||||
else
|
||||
-- Don't leave unusable space in the text widget
|
||||
self.text_height = text_height
|
||||
end
|
||||
end
|
||||
self._input_widget = InputText:new{
|
||||
text = self.input,
|
||||
hint = self.input_hint,
|
||||
face = self.input_face,
|
||||
width = self.text_width or self.width * 0.9,
|
||||
width = self.text_width,
|
||||
height = self.text_height or nil,
|
||||
padding = self.input_padding,
|
||||
margin = self.input_margin,
|
||||
input_type = self.input_type,
|
||||
text_type = self.text_type,
|
||||
enter_callback = self.enter_callback or function()
|
||||
@@ -148,68 +317,54 @@ function InputDialog:init()
|
||||
end
|
||||
end
|
||||
end,
|
||||
scroll = false,
|
||||
scroll = true,
|
||||
cursor_at_end = self.cursor_at_end,
|
||||
parent = self,
|
||||
}
|
||||
self.button_table = ButtonTable:new{
|
||||
width = self.width - 2*self.button_padding,
|
||||
button_font_face = "cfont",
|
||||
button_font_size = 20,
|
||||
buttons = self.buttons,
|
||||
zero_sep = true,
|
||||
show_parent = self,
|
||||
}
|
||||
|
||||
self.title_bar = LineWidget:new{
|
||||
dimen = Geom:new{
|
||||
w = self.width,
|
||||
h = Size.line.thick,
|
||||
}
|
||||
}
|
||||
|
||||
self.dialog_frame = FrameContainer:new{
|
||||
radius = Size.radius.window,
|
||||
padding = 0,
|
||||
margin = 0,
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_bar,
|
||||
self.description,
|
||||
-- input
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self._input_widget:getSize().h,
|
||||
},
|
||||
self._input_widget,
|
||||
},
|
||||
-- Add same vertical space after than before InputText
|
||||
VerticalSpan:new{ width = self.title_margin + self.title_padding },
|
||||
-- buttons
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self.button_table:getSize().h,
|
||||
},
|
||||
self.button_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.allow_newline then -- remove any enter_callback
|
||||
self._input_widget.enter_callback = nil
|
||||
end
|
||||
if Device:hasKeys() then
|
||||
--little hack to piggyback on the layout of the button_table to handle the new InputText
|
||||
table.insert(self.button_table.layout, 1, {self._input_widget})
|
||||
end
|
||||
|
||||
-- Final widget
|
||||
self.dialog_frame = FrameContainer:new{
|
||||
radius = self.fullscreen and 0 or Size.radius.window,
|
||||
padding = 0,
|
||||
margin = 0,
|
||||
bordersize = self.border_size,
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_bar,
|
||||
self.description_widget,
|
||||
vspan_before_input_text,
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.width,
|
||||
h = self._input_widget:getSize().h,
|
||||
},
|
||||
self._input_widget,
|
||||
},
|
||||
vspan_after_input_text,
|
||||
buttons_container,
|
||||
}
|
||||
}
|
||||
local frame = self.dialog_frame
|
||||
if self.movable then
|
||||
frame = MovableContainer:new{
|
||||
self.dialog_frame,
|
||||
}
|
||||
end
|
||||
self[1] = CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = Screen:getWidth(),
|
||||
h = Screen:getHeight() - self._input_widget:getKeyboardDimen().h,
|
||||
},
|
||||
MovableContainer:new{
|
||||
self.dialog_frame,
|
||||
},
|
||||
frame
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user