mirror of
https://github.com/koreader/koreader
synced 2024-11-16 06:12:56 +00:00
52c45ef6db
When not provided, the cursor stays at its initial position, which might not be the best if replacing the whole content, where we would prefer to have it at start or end.
1028 lines
41 KiB
Lua
1028 lines
41 KiB
Lua
--[[--
|
|
Widget for taking user input.
|
|
|
|
Example:
|
|
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local @{ui.uimanager|UIManager} = require("ui/uimanager")
|
|
local @{logger} = require("logger")
|
|
local @{gettext|_} = require("gettext")
|
|
|
|
local sample_input
|
|
sample_input = InputDialog:new{
|
|
title = _("Dialog title"),
|
|
input = "default value",
|
|
-- A placeholder text shown in the text box.
|
|
input_hint = _("Hint text"),
|
|
-- input_type = nil, -- default for text
|
|
-- A description shown above the input.
|
|
description = _("Some more description."),
|
|
-- text_type = "password",
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(sample_input)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Save"),
|
|
-- button with is_enter_default set to true will be
|
|
-- triggered after user press the enter key from keyboard
|
|
is_enter_default = true,
|
|
callback = function()
|
|
logger.dbg("Got user input as raw text:", sample_input:getInputText())
|
|
logger.dbg("Got user input as value:", sample_input:getInputValue())
|
|
end,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
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,
|
|
|
|
To add |Save|Close| buttons, use:
|
|
save_callback = function(content, closing)
|
|
-- ...Deal with the edited content...
|
|
if closing then
|
|
UIManager:nextTick(
|
|
-- Stuff to do when InputDialog is closed, if anything.
|
|
)
|
|
end
|
|
return nil -- sucess, default notification shown
|
|
return true, success_notif_text
|
|
return false, error_infomsg_text
|
|
end
|
|
|
|
To additionally add a Reset button and have |Reset|Save|Close|, use:
|
|
reset_callback = function()
|
|
return original_content -- success
|
|
return original_content, success_notif_text
|
|
return nil, error_infomsg_text
|
|
end
|
|
|
|
If you don't need more buttons than these, use these options for consistency
|
|
between dialogs, and don't provide any buttons.
|
|
Text used on these buttons and their messages and notifications can be
|
|
changed by providing alternative text with these additional options:
|
|
reset_button_text
|
|
save_button_text
|
|
close_button_text
|
|
close_unsaved_confirm_text
|
|
close_cancel_button_text
|
|
close_discard_button_text
|
|
close_save_button_text
|
|
close_discarded_notif_text
|
|
|
|
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.
|
|
|
|
It is strongly recommended to use a text describing the action to be
|
|
executed, as demonstrated in the example above. If the resulting phrase would be
|
|
longer than three words it should just read "OK".
|
|
|
|
]]
|
|
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
local ButtonTable = require("ui/widget/buttontable")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
local CheckButton = require("ui/widget/checkbutton")
|
|
local Device = require("device")
|
|
local FocusManager = require("ui/widget/focusmanager")
|
|
local Font = require("ui/font")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local InputText = require("ui/widget/inputtext")
|
|
local MovableContainer = require("ui/widget/container/movablecontainer")
|
|
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
|
|
local Notification = require("ui/widget/notification")
|
|
local Size = require("ui/size")
|
|
local TitleBar = require("ui/widget/titlebar")
|
|
local UIManager = require("ui/uimanager")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
local Screen = Device.screen
|
|
local T = require("ffi/util").template
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
|
|
local InputDialog = FocusManager:extend{
|
|
is_always_active = true,
|
|
title = "",
|
|
input = "",
|
|
input_hint = "",
|
|
description = nil,
|
|
buttons = nil,
|
|
input_type = nil,
|
|
deny_keyboard_hiding = false, -- don't hide keyboard on tap outside
|
|
enter_callback = nil,
|
|
strike_callback = nil, -- call this on every keystroke (used by Terminal plugin's TermInputText)
|
|
inputtext_class = InputText, -- (Terminal plugin provides TermInputText)
|
|
readonly = false, -- don't allow editing, will not show keyboard
|
|
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
|
|
use_available_height = false, -- adjust input box to fill available height on screen
|
|
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
|
|
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)
|
|
|
|
-- If save_callback provided, a Save and a Close buttons will be added to the first row
|
|
-- if reset_callback provided, a Reset button will be added (before Save) to the first row
|
|
save_callback = nil, -- Called with the input text content when Save (and true as 2nd arg
|
|
-- if closing, false if non-closing Save).
|
|
-- Should return nil or true on success, false on failure.
|
|
-- (This save_callback can do some syntax check before saving)
|
|
reset_callback = nil, -- Called with no arg, should return the original content on success,
|
|
-- nil on failure.
|
|
-- Both these callbacks can return a string as a 2nd return value.
|
|
-- This string is then shown:
|
|
-- - on success: as the notification text instead of the default one
|
|
-- - on failure: in an InfoMessage
|
|
close_callback = nil, -- Called when closing (if discarded or saved, after save_callback if saved)
|
|
edited_callback = nil, -- Called on each text modification
|
|
|
|
-- For use by TextEditor plugin:
|
|
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,
|
|
|
|
width = nil,
|
|
|
|
text_width = nil,
|
|
text_height = nil,
|
|
|
|
bottom_v_padding = 0,
|
|
input_face = Font:getFace("x_smallinfofont"),
|
|
input_padding = Size.padding.default,
|
|
input_margin = Size.margin.default,
|
|
button_padding = Size.padding.default,
|
|
border_size = Size.border.window,
|
|
|
|
-- 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
|
|
_keyboard_was_visible = nil, -- previous kb visibility state
|
|
_text_modified = false, -- previous known modified status
|
|
_top_line_num = nil,
|
|
_charpos = nil,
|
|
_buttons_edit_callback = nil,
|
|
_buttons_scroll_callback = nil,
|
|
_buttons_backup_done = false,
|
|
_buttons_backup = nil,
|
|
}
|
|
|
|
function InputDialog:init()
|
|
self.layout = {{}}
|
|
self.screen_width = Screen:getWidth()
|
|
self.screen_height = Screen:getHeight()
|
|
if self.fullscreen then
|
|
self.is_movable = false
|
|
self.border_size = 0
|
|
self.width = self.screen_width - 2*self.border_size
|
|
self.covers_fullscreen = true -- hint for UIManager:_repaint()
|
|
else
|
|
self.width = self.width or math.floor(math.min(self.screen_width, self.screen_height) * 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 math.floor(self.width * 0.9)
|
|
end
|
|
if self.readonly then -- hide keyboard if we can't edit
|
|
self.keyboard_visible = false
|
|
end
|
|
if self.fullscreen or self.add_nav_bar then
|
|
self.deny_keyboard_hiding = true
|
|
end
|
|
|
|
-- Title & description
|
|
self.title_bar = TitleBar:new{
|
|
width = self.width,
|
|
align = "left",
|
|
with_bottom_line = true,
|
|
title = self.title,
|
|
title_multilines = true,
|
|
bottom_v_padding = self.bottom_v_padding,
|
|
info_text = self.description,
|
|
show_parent = self,
|
|
}
|
|
|
|
-- Vertical spaces added before and after InputText
|
|
-- (these will be adjusted later to center the input text if needed)
|
|
-- (can be disabled by setting condensed=true)
|
|
local padding_width = self.condensed and 0 or Size.padding.default
|
|
local vspan_before_input_text = VerticalSpan:new{ width = padding_width }
|
|
local vspan_after_input_text = VerticalSpan:new{ width = padding_width }
|
|
|
|
-- Buttons
|
|
-- In case of re-init(), keep backup of original buttons and restore them
|
|
self:_backupRestoreButtons()
|
|
-- If requested, add predefined buttons alongside provided ones
|
|
if self.save_callback then
|
|
-- If save_callback provided, adds (Reset) / Save / Close buttons
|
|
self:_addSaveCloseButtons()
|
|
end
|
|
if self.add_nav_bar then -- Home / End / Up / Down buttons
|
|
self:_addScrollButtons(true)
|
|
elseif self.add_scroll_buttons then -- Up / Down buttons
|
|
self:_addScrollButtons(false)
|
|
end
|
|
-- Buttons Table
|
|
self.button_table = ButtonTable:new{
|
|
width = self.width - 2*self.button_padding,
|
|
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,
|
|
}
|
|
|
|
-- Remember provided text_height if any (to restore it on keyboard height change)
|
|
if self.orig_text_height == nil then
|
|
if self.text_height then
|
|
self.orig_text_height = self.text_height
|
|
else
|
|
self.orig_text_height = false
|
|
end
|
|
end
|
|
|
|
-- 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 = self.inputtext_class:new{
|
|
text = self.fullscreen and "-" or self.input,
|
|
input_type = self.input_type,
|
|
face = self.input_face,
|
|
width = self.text_width,
|
|
padding = self.input_padding,
|
|
margin = self.input_margin,
|
|
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...
|
|
}
|
|
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 = 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
|
|
- 2*self.border_size
|
|
- self.title_bar:getHeight()
|
|
- 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 self.use_available_height 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
|
|
if text_height > available_height then
|
|
self.cursor_at_end = false -- stay at start if overflowed
|
|
end
|
|
else
|
|
-- Don't leave unusable space in the text widget
|
|
self.text_height = text_height
|
|
end
|
|
end
|
|
if self.view_pos_callback then
|
|
-- 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,
|
|
hint = self.input_hint,
|
|
face = self.input_face,
|
|
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.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()
|
|
for _,btn_row in ipairs(self.buttons) do
|
|
for _,btn in ipairs(btn_row) do
|
|
if btn.is_enter_default then
|
|
btn.callback()
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
strike_callback = self.strike_callback,
|
|
edit_callback = self._buttons_edit_callback, -- nil if no Save/Close buttons
|
|
scroll_callback = self._buttons_scroll_callback, -- nil if no Nav or Scroll buttons
|
|
scroll = true,
|
|
scroll_by_pan = self.scroll_by_pan,
|
|
cursor_at_end = self.cursor_at_end,
|
|
readonly = self.readonly,
|
|
parent = self,
|
|
is_text_edited = self._text_modified,
|
|
top_line_num = self._top_line_num,
|
|
charpos = self._charpos,
|
|
}
|
|
table.insert(self.layout[1], self._input_widget)
|
|
if self.allow_newline then -- remove any enter_callback
|
|
self._input_widget.enter_callback = nil
|
|
end
|
|
self:mergeLayoutInVertical(self.button_table)
|
|
self:refocusWidget()
|
|
-- Complementary setup for some of our added buttons
|
|
if self.save_callback then
|
|
local save_button = self.button_table:getButtonById("save")
|
|
if self.readonly then
|
|
save_button:setText(_("Read only"), save_button.width)
|
|
elseif not self._input_widget:isTextEditable() then
|
|
save_button:setText(_("Not editable"), save_button.width)
|
|
end
|
|
end
|
|
|
|
-- Combine all
|
|
self.vgroup = VerticalGroup:new{
|
|
align = "left",
|
|
self.title_bar,
|
|
vspan_before_input_text,
|
|
CenterContainer:new{
|
|
dimen = Geom:new{
|
|
w = self.width,
|
|
h = self._input_widget:getSize().h,
|
|
},
|
|
self._input_widget,
|
|
},
|
|
-- added widgets may be inserted here
|
|
vspan_after_input_text,
|
|
buttons_container,
|
|
}
|
|
|
|
-- 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,
|
|
self.vgroup,
|
|
}
|
|
local frame = self.dialog_frame
|
|
if self.is_movable then
|
|
self.movable = MovableContainer:new{ -- (UIManager expects this as 'self.movable')
|
|
self.dialog_frame,
|
|
}
|
|
frame = self.movable
|
|
end
|
|
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,
|
|
h = self.screen_height - keyboard_height,
|
|
},
|
|
ignore_if_over = "height",
|
|
frame,
|
|
}
|
|
if Device:isTouchDevice() then -- is used to hide the keyboard with a tap outside of inputbox
|
|
self.ges_events.Tap = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = self[1].dimen, -- screen above the keyboard
|
|
},
|
|
}
|
|
end
|
|
if Device:hasKeys() then
|
|
self.key_events.CloseDialog = { { Device.input.group.Back } }
|
|
end
|
|
if self._added_widgets then
|
|
for _, widget in ipairs(self._added_widgets) do
|
|
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)
|
|
table.insert(self.layout, #self.layout, {widget})
|
|
if not re_init then -- backup widget for re-init
|
|
widget = CenterContainer:new{
|
|
dimen = Geom:new{
|
|
w = self.width,
|
|
h = widget:getSize().h,
|
|
},
|
|
widget,
|
|
}
|
|
if not self._added_widgets then
|
|
self._added_widgets = {}
|
|
end
|
|
table.insert(self._added_widgets, widget)
|
|
end
|
|
-- insert widget before the bottom buttons and their previous vspan
|
|
table.insert(self.vgroup, #self.vgroup-1, widget)
|
|
end
|
|
|
|
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
|
|
self:onCloseKeyboard()
|
|
end
|
|
|
|
function InputDialog:getInputText()
|
|
return self._input_widget:getText()
|
|
end
|
|
|
|
function InputDialog:getInputValue()
|
|
local text = self:getInputText()
|
|
if self.input_type == "number" then
|
|
return tonumber(text)
|
|
else
|
|
return text
|
|
end
|
|
end
|
|
|
|
function InputDialog:setInputText(text, edited_state, cursor_at_start_or_end)
|
|
self._input_widget:setText(text)
|
|
if edited_state ~= nil and self._buttons_edit_callback then
|
|
self._buttons_edit_callback(edited_state)
|
|
end
|
|
if cursor_at_start_or_end ~= nil then -- true=start, false=end
|
|
if cursor_at_start_or_end then
|
|
self._input_widget:scrollToTop()
|
|
else
|
|
self._input_widget:scrollToBottom()
|
|
end
|
|
end
|
|
end
|
|
|
|
function InputDialog:isTextEditable()
|
|
return self._input_widget:isTextEditable()
|
|
end
|
|
|
|
function InputDialog:isTextEdited()
|
|
return self._input_widget:isTextEdited()
|
|
end
|
|
|
|
function InputDialog:onShow()
|
|
UIManager:setDirty(self, function()
|
|
return "ui", self.dialog_frame.dimen
|
|
end)
|
|
end
|
|
|
|
function InputDialog:onCloseWidget()
|
|
self:onClose()
|
|
UIManager:setDirty(nil, self.fullscreen and "full" or function()
|
|
return "ui", self.dialog_frame.dimen
|
|
end)
|
|
end
|
|
|
|
function InputDialog:onShowKeyboard(ignore_first_hold_release)
|
|
-- 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: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()
|
|
|
|
-- 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
|
|
if self._added_widgets then
|
|
-- prevent these externally added widgets from being freed as :init() will re-add them
|
|
for i = 1, #self._added_widgets do
|
|
table.remove(self.vgroup, #self.vgroup-2)
|
|
end
|
|
end
|
|
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 self.keyboard_visible then
|
|
self:onShowKeyboard()
|
|
end
|
|
-- Our position on screen has probably changed, so have the full screen refreshed
|
|
UIManager:setDirty("all", "flashui")
|
|
end
|
|
|
|
function InputDialog:onCloseDialog()
|
|
local close_button = self.button_table:getButtonById("close")
|
|
if close_button and close_button.enabled then
|
|
close_button.callback()
|
|
return true
|
|
end
|
|
return false
|
|
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
|
|
-- 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:onCloseKeyboard()
|
|
end
|
|
|
|
function InputDialog:refreshButtons()
|
|
-- Using what ought to be enough:
|
|
-- return "ui", self.button_table.dimen
|
|
-- causes 2 non-intersecting refreshes (because if our buttons
|
|
-- change, the text widget did) that may sometimes cause
|
|
-- the button_table to become white.
|
|
-- Safer to refresh the whole widget so the refreshes can
|
|
-- be merged into one.
|
|
UIManager:setDirty(self, function()
|
|
return "ui", self.dialog_frame.dimen
|
|
end)
|
|
end
|
|
|
|
function InputDialog:_backupRestoreButtons()
|
|
-- In case of re-init(), keep backup of original buttons and restore them
|
|
if self._buttons_backup_done then
|
|
-- Move backup and override current, and re-create backup from original,
|
|
-- to avoid duplicating the copy code)
|
|
self.buttons = self._buttons_backup -- restore (we may restore 'nil')
|
|
end
|
|
if self.buttons then -- (re-)create backup
|
|
self._buttons_backup = {} -- deep copy, except for the buttons themselves
|
|
for i, row in ipairs(self.buttons) do
|
|
if row then
|
|
local row_copy = {}
|
|
self._buttons_backup[i] = row_copy
|
|
for j, b in ipairs(row) do
|
|
row_copy[j] = b
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self._buttons_backup_done = true
|
|
end
|
|
|
|
function InputDialog:_addSaveCloseButtons()
|
|
if not self.buttons then
|
|
self.buttons = {{}}
|
|
end
|
|
-- Add them to the end of first row
|
|
local row = self.buttons[1]
|
|
local button = function(id) -- shortcut for more readable code
|
|
return self.button_table:getButtonById(id)
|
|
end
|
|
-- Callback to enable/disable Reset/Save buttons, for feedback when text modified
|
|
self._buttons_edit_callback = function(edited)
|
|
if self._text_modified and not edited then
|
|
self._text_modified = false
|
|
button("save"):disable()
|
|
if button("reset") then button("reset"):disable() end
|
|
self:refreshButtons()
|
|
elseif edited and not self._text_modified then
|
|
self._text_modified = true
|
|
button("save"):enable()
|
|
if button("reset") then button("reset"):enable() end
|
|
self:refreshButtons()
|
|
end
|
|
if self.edited_callback then
|
|
self.edited_callback()
|
|
end
|
|
end
|
|
if self.reset_callback then
|
|
-- if reset_callback provided, add button to restore
|
|
-- test to some previous state
|
|
table.insert(row, {
|
|
text = self.reset_button_text or _("Reset"),
|
|
id = "reset",
|
|
enabled = self._text_modified,
|
|
callback = function()
|
|
-- Wrapped via Trapper, to allow reset_callback to use Trapper
|
|
-- to show progress or ask questions while getting original content
|
|
require("ui/trapper"):wrap(function()
|
|
local content, msg = self.reset_callback()
|
|
if content then
|
|
self:setInputText(content)
|
|
self._buttons_edit_callback(false)
|
|
UIManager:show(Notification:new{
|
|
text = msg or _("Text reset"),
|
|
})
|
|
else -- nil content, assume failure and show msg
|
|
if msg ~= false then -- false allows for no InfoMessage
|
|
UIManager:show(InfoMessage:new{
|
|
text = msg or _("Resetting failed."),
|
|
})
|
|
end
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
table.insert(row, {
|
|
text = self.save_button_text or _("Save"),
|
|
id = "save",
|
|
enabled = self._text_modified,
|
|
callback = function()
|
|
-- Wrapped via Trapper, to allow save_callback to use Trapper
|
|
-- to show progress or ask questions while saving
|
|
require("ui/trapper"):wrap(function()
|
|
if self._text_modified then
|
|
local success, msg = self.save_callback(self:getInputText())
|
|
if success == false then
|
|
if msg ~= false then -- false allows for no InfoMessage
|
|
UIManager:show(InfoMessage:new{
|
|
text = msg or _("Saving failed."),
|
|
})
|
|
end
|
|
else -- nil or true
|
|
self._buttons_edit_callback(false)
|
|
UIManager:show(Notification:new{
|
|
text = msg or _("Saved"),
|
|
})
|
|
end
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
table.insert(row, {
|
|
text = self.close_button_text or _("Close"),
|
|
id = "close",
|
|
callback = function()
|
|
if self._text_modified then
|
|
UIManager:show(MultiConfirmBox:new{
|
|
text = self.close_unsaved_confirm_text or _("You have unsaved changes."),
|
|
cancel_text = self.close_cancel_button_text or _("Cancel"),
|
|
choice1_text = self.close_discard_button_text or _("Discard"),
|
|
choice1_callback = function()
|
|
if self.close_callback then self.close_callback() end
|
|
UIManager:close(self)
|
|
UIManager:show(Notification:new{
|
|
text = self.close_discarded_notif_text or _("Changes discarded"),
|
|
})
|
|
end,
|
|
choice2_text = self.close_save_button_text or _("Save"),
|
|
choice2_callback = function()
|
|
-- Wrapped via Trapper, to allow save_callback to use Trapper
|
|
-- to show progress or ask questions while saving
|
|
require("ui/trapper"):wrap(function()
|
|
local success, msg = self.save_callback(self:getInputText(), true)
|
|
if success == false then
|
|
if msg ~= false then -- false allows for no InfoMessage
|
|
UIManager:show(InfoMessage:new{
|
|
text = msg or _("Saving failed."),
|
|
})
|
|
end
|
|
else -- nil or true
|
|
if self.close_callback then self.close_callback() end
|
|
UIManager:close(self)
|
|
UIManager:show(Notification:new{
|
|
text = msg or _("Saved"),
|
|
})
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
else
|
|
-- Not modified, exit without any message
|
|
if self.close_callback then self.close_callback() end
|
|
UIManager:close(self)
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
function InputDialog:_addScrollButtons(nav_bar)
|
|
local row
|
|
if nav_bar then -- Add Home / End / Up / Down buttons as a last row
|
|
if not self.buttons then
|
|
self.buttons = {}
|
|
end
|
|
row = {} -- Empty additional buttons row
|
|
table.insert(self.buttons, row)
|
|
else -- Add the Up / Down buttons to the first row
|
|
if not self.buttons then
|
|
self.buttons = {{}}
|
|
end
|
|
row = self.buttons[1]
|
|
end
|
|
if nav_bar then -- Add the Home & End buttons
|
|
-- Also add Keyboard hide/show button if we can
|
|
if self.fullscreen and not self.readonly then
|
|
table.insert(row, {
|
|
text = self.keyboard_visible and "↓⌨" or "↑⌨",
|
|
id = "keyboard",
|
|
callback = function()
|
|
self:toggleKeyboard()
|
|
end,
|
|
})
|
|
end
|
|
if self.fullscreen then
|
|
-- Add a button to search for a string in the edited text
|
|
table.insert(row, {
|
|
text = _("Find"),
|
|
callback = function()
|
|
self:toggleKeyboard(false) -- hide text editor keyboard
|
|
local input_dialog
|
|
input_dialog = InputDialog:new{
|
|
title = _("Enter text to search for"),
|
|
stop_events_propagation = true, -- avoid interactions with upper InputDialog
|
|
input = self.search_value,
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(input_dialog)
|
|
self:toggleKeyboard()
|
|
end,
|
|
},
|
|
{
|
|
text = _("Find first"),
|
|
callback = function()
|
|
self:findCallback(input_dialog, true)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Find next"),
|
|
is_enter_default = true,
|
|
callback = function()
|
|
self:findCallback(input_dialog)
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
self.check_button_case = CheckButton:new{
|
|
text = _("Case sensitive"),
|
|
checked = self.case_sensitive,
|
|
parent = input_dialog,
|
|
callback = function()
|
|
self.case_sensitive = self.check_button_case.checked
|
|
end,
|
|
}
|
|
input_dialog:addWidget(self.check_button_case)
|
|
|
|
UIManager:show(input_dialog)
|
|
input_dialog:onShowKeyboard()
|
|
end,
|
|
})
|
|
-- Add a button to go to the line by its number in the file
|
|
table.insert(row, {
|
|
text = _("Go"),
|
|
callback = function()
|
|
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{
|
|
title = _("Enter line number"),
|
|
-- @translators %1 is the current line number, %2 is the last line number
|
|
input_hint = T(_("%1 (1 - %2)"), cur_line_num, last_line_num),
|
|
input_type = "number",
|
|
stop_events_propagation = true, -- avoid interactions with upper InputDialog
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(input_dialog)
|
|
self:toggleKeyboard()
|
|
end,
|
|
},
|
|
{
|
|
text = _("Go to line"),
|
|
is_enter_default = true,
|
|
callback = function()
|
|
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:toggleKeyboard()
|
|
self._input_widget:moveCursorToCharPos(self._input_widget:getLineCharPos(new_line_num))
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(input_dialog)
|
|
input_dialog:onShowKeyboard()
|
|
end,
|
|
})
|
|
end
|
|
table.insert(row, {
|
|
text = "⇱",
|
|
id = "top",
|
|
vsync = true,
|
|
callback = function()
|
|
self._input_widget:scrollToTop()
|
|
end,
|
|
})
|
|
table.insert(row, {
|
|
text = "⇲",
|
|
id = "bottom",
|
|
vsync = true,
|
|
callback = function()
|
|
self._input_widget:scrollToBottom()
|
|
end,
|
|
})
|
|
end
|
|
-- Add the Up & Down buttons
|
|
table.insert(row, {
|
|
text = "△",
|
|
id = "up",
|
|
callback = function()
|
|
self._input_widget:scrollUp()
|
|
end,
|
|
})
|
|
table.insert(row, {
|
|
text = "▽",
|
|
id = "down",
|
|
callback = function()
|
|
self._input_widget:scrollDown()
|
|
end,
|
|
})
|
|
-- Callback to enable/disable buttons, for at-top/at-bottom feedback
|
|
local prev_at_top = false -- Buttons were created enabled
|
|
local prev_at_bottom = false
|
|
local button = function(id) -- shortcut for more readable code
|
|
return self.button_table:getButtonById(id)
|
|
end
|
|
self._buttons_scroll_callback = function(low, high)
|
|
local changed = false
|
|
if prev_at_top and low > 0 then
|
|
button("up"):enable()
|
|
if button("top") then button("top"):enable() end
|
|
prev_at_top = false
|
|
changed = true
|
|
elseif not prev_at_top and low <= 0 then
|
|
button("up"):disable()
|
|
if button("top") then button("top"):disable() end
|
|
prev_at_top = true
|
|
changed = true
|
|
end
|
|
if prev_at_bottom and high < 1 then
|
|
button("down"):enable()
|
|
if button("bottom") then button("bottom"):enable() end
|
|
prev_at_bottom = false
|
|
changed = true
|
|
elseif not prev_at_bottom and high >= 1 then
|
|
button("down"):disable()
|
|
if button("bottom") then button("bottom"):disable() end
|
|
prev_at_bottom = true
|
|
changed = true
|
|
end
|
|
if changed then
|
|
self:refreshButtons()
|
|
end
|
|
end
|
|
end
|
|
|
|
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: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)
|
|
local msg
|
|
if char_pos > 0 then
|
|
self._input_widget:moveCursorToCharPos(char_pos)
|
|
msg = T(_("Found in line %1."), self._input_widget:getLineNums())
|
|
else
|
|
msg = _("Not found.")
|
|
end
|
|
UIManager:show(Notification:new{
|
|
text = msg,
|
|
})
|
|
end
|
|
|
|
return InputDialog
|