mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
Text editor plugin, InputDialog enhancements (#4135)
This plugin mostly sets up a "Text editor>" submenu, that allows browsing files, creating a new file, and managing a history of previously opened file for easier re-opening. It restore previous scroll and cursor positions on re-opening. Additional "Check lua" syntax button is added when editing a .lua file, and prevent saving if errors. The text editing is mainly provided by the enhanced InputDialog. InputDialog: added a few more options, the main one being 'save_callback', which will add a Save and Close buttons and manage saving/discarding/exiting. If "fullscreen" and "add_nav_bar", will add a show/hide keyboard button to it. Moved the preset buttons setup code in their own InputDialog methods for clarity of the main init code. Buttons are now enabled/disabled depending on context for feedback (eg: Save is disabled as long as text has not been modified). Added util.checkLuaSyntax(lua_string), might be useful elsewhere.
This commit is contained in:
@@ -51,6 +51,7 @@ local order = {
|
||||
"read_timer",
|
||||
"news_downloader",
|
||||
"send2ebook",
|
||||
"text_editor",
|
||||
"----------------------------",
|
||||
"more_plugins",
|
||||
"----------------------------",
|
||||
|
||||
@@ -72,6 +72,7 @@ local order = {
|
||||
"zsync",
|
||||
"news_downloader",
|
||||
"send2ebook",
|
||||
"text_editor",
|
||||
"----------------------------",
|
||||
"more_plugins",
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ local ButtonTable = FocusManager:new{
|
||||
function ButtonTable:init()
|
||||
self.selected = { x = 1, y = 1 }
|
||||
self.buttons_layout = {}
|
||||
self.button_by_id = {}
|
||||
self.container = VerticalGroup:new{ width = self.width }
|
||||
table.insert(self, self.container)
|
||||
if self.zero_sep then
|
||||
@@ -61,6 +62,9 @@ function ButtonTable:init()
|
||||
text_font_size = self.button_font_size,
|
||||
show_parent = self.show_parent,
|
||||
}
|
||||
if btn_entry.id then
|
||||
self.button_by_id[btn_entry.id] = button
|
||||
end
|
||||
local button_dim = button:getSize()
|
||||
local vertical_sep = LineWidget:new{
|
||||
background = Blitbuffer.COLOR_GREY,
|
||||
@@ -121,4 +125,8 @@ function ButtonTable:onSelectByKeyPress()
|
||||
end
|
||||
end
|
||||
|
||||
function ButtonTable:getButtonById(id)
|
||||
return self.button_by_id[id] -- nil if not found
|
||||
end
|
||||
|
||||
return ButtonTable
|
||||
|
||||
@@ -46,6 +46,35 @@ To get a full screen text editor, use:
|
||||
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 closed if any )
|
||||
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.
|
||||
@@ -63,10 +92,13 @@ local Device = require("device")
|
||||
local Font = require("ui/font")
|
||||
local FrameContainer = require("ui/widget/container/framecontainer")
|
||||
local Geom = require("ui/geometry")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local InputText = require("ui/widget/inputtext")
|
||||
local LineWidget = require("ui/widget/linewidget")
|
||||
local MovableContainer = require("ui/widget/container/movablecontainer")
|
||||
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
|
||||
local Notification = require("ui/widget/notification")
|
||||
local RenderText = require("ui/rendertext")
|
||||
local Size = require("ui/size")
|
||||
local TextBoxWidget = require("ui/widget/textboxwidget")
|
||||
@@ -75,6 +107,7 @@ local UIManager = require("ui/uimanager")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local VerticalSpan = require("ui/widget/verticalspan")
|
||||
local Screen = Device.screen
|
||||
local _ = require("gettext")
|
||||
|
||||
local InputDialog = InputContainer:new{
|
||||
is_always_active = true,
|
||||
@@ -85,13 +118,33 @@ local InputDialog = InputContainer:new{
|
||||
buttons = nil,
|
||||
input_type = nil,
|
||||
enter_callback = nil,
|
||||
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
|
||||
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
|
||||
-- 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
|
||||
-- needs add_nav_bar to have a Show keyboard button to get it back
|
||||
|
||||
-- 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
|
||||
|
||||
-- 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.
|
||||
|
||||
-- movable = true, -- set to false if movable gestures conflicts with subwidgets gestures
|
||||
-- for now, too much conflicts between InputText and MovableContainer, and
|
||||
@@ -116,6 +169,15 @@ local InputDialog = InputContainer:new{
|
||||
input_margin = Size.margin.default,
|
||||
button_padding = Size.padding.default,
|
||||
border_size = Size.border.window,
|
||||
|
||||
-- for internal use
|
||||
_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()
|
||||
@@ -123,6 +185,7 @@ function InputDialog:init()
|
||||
self.movable = false
|
||||
self.border_size = 0
|
||||
self.width = Screen:getWidth() - 2*self.border_size
|
||||
self.covers_fullscreen = true -- hint for UIManager:_repaint()
|
||||
else
|
||||
self.width = self.width or Screen:getWidth() * 0.8
|
||||
end
|
||||
@@ -131,6 +194,9 @@ function InputDialog:init()
|
||||
else
|
||||
self.text_width = self.text_width or self.width * 0.9
|
||||
end
|
||||
if self.readonly then -- hide keyboard if we can't edit
|
||||
self.keyboard_hidden = true
|
||||
end
|
||||
|
||||
-- Title & description
|
||||
local title_width = RenderText:sizeUtf8Text(0, self.width,
|
||||
@@ -142,7 +208,7 @@ function InputDialog:init()
|
||||
self.title = RenderText:getSubTextByWidth(self.title, self.title_face,
|
||||
self.width - indicator_w, true) .. indicator
|
||||
end
|
||||
self.title = FrameContainer:new{
|
||||
self.title_widget = FrameContainer:new{
|
||||
padding = self.title_padding,
|
||||
margin = self.title_margin,
|
||||
bordersize = 0,
|
||||
@@ -191,54 +257,19 @@ function InputDialog:init()
|
||||
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,
|
||||
})
|
||||
-- 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,
|
||||
button_font_face = "cfont",
|
||||
@@ -269,12 +300,15 @@ 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 = input_widget:getKeyboardDimen().h
|
||||
local keyboard_height = 0
|
||||
if not self.keyboard_hidden then
|
||||
keyboard_height = input_widget:getKeyboardDimen().h
|
||||
end
|
||||
input_widget:free()
|
||||
-- Find out available height
|
||||
local available_height = Screen:getHeight()
|
||||
- 2*self.border_size
|
||||
- self.title:getSize().h
|
||||
- self.title_widget:getSize().h
|
||||
- self.title_bar:getSize().h
|
||||
- self.description_widget:getSize().h
|
||||
- vspan_before_input_text:getSize().h
|
||||
@@ -297,6 +331,11 @@ function InputDialog:init()
|
||||
self.text_height = text_height
|
||||
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()
|
||||
end
|
||||
self._input_widget = InputText:new{
|
||||
text = self.input,
|
||||
hint = self.input_hint,
|
||||
@@ -317,9 +356,15 @@ function InputDialog:init()
|
||||
end
|
||||
end
|
||||
end,
|
||||
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,
|
||||
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,
|
||||
}
|
||||
if self.allow_newline then -- remove any enter_callback
|
||||
self._input_widget.enter_callback = nil
|
||||
@@ -328,6 +373,15 @@ function InputDialog:init()
|
||||
--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
|
||||
-- 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
|
||||
|
||||
-- Final widget
|
||||
self.dialog_frame = FrameContainer:new{
|
||||
@@ -338,7 +392,7 @@ function InputDialog:init()
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_widget,
|
||||
self.title_bar,
|
||||
self.description_widget,
|
||||
vspan_before_input_text,
|
||||
@@ -359,10 +413,12 @@ function InputDialog:init()
|
||||
self.dialog_frame,
|
||||
}
|
||||
end
|
||||
local keyboard_height = self.keyboard_hidden and 0
|
||||
or self._input_widget:getKeyboardDimen().h
|
||||
self[1] = CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = Screen:getWidth(),
|
||||
h = Screen:getHeight() - self._input_widget:getKeyboardDimen().h,
|
||||
h = Screen:getHeight() - keyboard_height,
|
||||
},
|
||||
frame
|
||||
}
|
||||
@@ -407,11 +463,279 @@ function InputDialog:onCloseWidget()
|
||||
end
|
||||
|
||||
function InputDialog:onShowKeyboard()
|
||||
self._input_widget:onShowKeyboard()
|
||||
if not self.readonly and not self.keyboard_hidden then
|
||||
self._input_widget:onShowKeyboard()
|
||||
end
|
||||
end
|
||||
|
||||
function InputDialog:onClose()
|
||||
-- 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
|
||||
self.view_pos_callback(self._top_line_num, self._charpos)
|
||||
end
|
||||
self._input_widget: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
|
||||
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"),
|
||||
timeout = 2
|
||||
})
|
||||
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"),
|
||||
timeout = 2
|
||||
})
|
||||
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()
|
||||
UIManager:close(self)
|
||||
UIManager:show(Notification:new{
|
||||
text = self.close_discarded_notif_text or _("Changes discarded"),
|
||||
timeout = 2
|
||||
})
|
||||
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
|
||||
UIManager:close(self)
|
||||
UIManager:show(Notification:new{
|
||||
text = msg or _("Saved"),
|
||||
timeout = 2
|
||||
})
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
else
|
||||
-- Not modified, exit without any message
|
||||
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_hidden and "↑⌨" or "↓⌨",
|
||||
id = "keyboard",
|
||||
callback = function()
|
||||
self.keyboard_hidden = not self.keyboard_hidden
|
||||
self.input = self:getInputText() -- re-init with up-to-date text
|
||||
self:onClose() -- will close keyboard and save view position
|
||||
self:free()
|
||||
self:init()
|
||||
if not self.keyboard_hidden then
|
||||
self:onShowKeyboard()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
table.insert(row, {
|
||||
text = "⇱",
|
||||
id = "top",
|
||||
callback = function()
|
||||
self._input_widget:scrollToTop()
|
||||
end,
|
||||
})
|
||||
table.insert(row, {
|
||||
text = "⇲",
|
||||
id = "bottom",
|
||||
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
|
||||
|
||||
return InputDialog
|
||||
|
||||
@@ -28,6 +28,8 @@ local InputText = InputContainer:new{
|
||||
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)
|
||||
|
||||
width = nil,
|
||||
height = nil, -- when nil, will be set to original text height (possibly
|
||||
@@ -209,9 +211,14 @@ function InputText:init()
|
||||
-- text_type changes from "password" to "text" when we toggle password
|
||||
self.is_password_type = true
|
||||
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" and type(self.text) == "number" then
|
||||
-- checkTextEditability() fails if self.text stays not a string
|
||||
self.text = tostring(self.text)
|
||||
end
|
||||
self:initTextBox(self.text)
|
||||
self:checkTextEditability()
|
||||
self.is_text_edited = false
|
||||
if self.readonly ~= true then
|
||||
self:initKeyboard()
|
||||
self:initEventListener()
|
||||
@@ -314,6 +321,7 @@ function InputText:initTextBox(text, char_added)
|
||||
width = self.width,
|
||||
height = self.height,
|
||||
dialog = self.parent,
|
||||
scroll_callback = self.scroll_callback,
|
||||
}
|
||||
else
|
||||
self.text_widget = TextBoxWidget:new{
|
||||
@@ -356,6 +364,9 @@ function InputText:initTextBox(text, char_added)
|
||||
UIManager:setDirty(self.parent, function()
|
||||
return "ui", self.dimen
|
||||
end)
|
||||
if self.edit_callback then
|
||||
self.edit_callback(self.is_text_edited)
|
||||
end
|
||||
end
|
||||
|
||||
function InputText:initKeyboard()
|
||||
@@ -408,11 +419,16 @@ function InputText:getKeyboardDimen()
|
||||
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 not self:isTextEditable(true) then
|
||||
if self.readonly or not self:isTextEditable(true) then
|
||||
return
|
||||
end
|
||||
self.is_text_edited = true
|
||||
@@ -422,7 +438,7 @@ function InputText:addChars(chars)
|
||||
end
|
||||
|
||||
function InputText:delChar()
|
||||
if not self:isTextEditable(true) then
|
||||
if self.readonly or not self:isTextEditable(true) then
|
||||
return
|
||||
end
|
||||
if self.charpos == 1 then return end
|
||||
@@ -433,7 +449,7 @@ function InputText:delChar()
|
||||
end
|
||||
|
||||
function InputText:delToStartOfLine()
|
||||
if not self:isTextEditable(true) then
|
||||
if self.readonly or not self:isTextEditable(true) then
|
||||
return
|
||||
end
|
||||
if self.charpos == 1 then return end
|
||||
|
||||
@@ -52,7 +52,7 @@ function LoginDialog:init()
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_widget,
|
||||
self.title_bar,
|
||||
-- username input
|
||||
CenterContainer:new{
|
||||
|
||||
@@ -29,7 +29,7 @@ function MultiInputDialog:init()
|
||||
InputDialog.init(self)
|
||||
local VerticalGroupData = VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_widget,
|
||||
self.title_bar,
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ function OpenWithDialog:init()
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_widget,
|
||||
self.title_bar,
|
||||
VerticalSpan:new{
|
||||
width = Size.span.vertical_large*2,
|
||||
|
||||
@@ -121,6 +121,9 @@ function ScrollTextWidget:updateScrollBar(is_partial)
|
||||
UIManager:setDirty(self.dialog, function()
|
||||
return refreshfunc, self.dimen
|
||||
end)
|
||||
if self.scroll_callback then
|
||||
self.scroll_callback(low, high)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -471,7 +471,7 @@ end
|
||||
|
||||
--- Replaces invalid UTF-8 characters with a replacement string.
|
||||
--
|
||||
-- Based on <a href="http://notebook.kulchenko.com/programming/fixing-malformed-utf8-in-lua">http://notebook.kulchenko.com/programming/fixing-malformed-utf8-in-lua</a>
|
||||
-- Based on http://notebook.kulchenko.com/programming/fixing-malformed-utf8-in-lua
|
||||
---- @string str the string to be checked for invalid characters
|
||||
---- @string replacement the string to replace invalid characters with
|
||||
---- @treturn string valid UTF-8
|
||||
@@ -677,4 +677,18 @@ function util.urlDecode(url)
|
||||
return url
|
||||
end
|
||||
|
||||
--- Check lua syntax of string
|
||||
--- @string text lua code text
|
||||
--- @treturn string with parsing error, nil if syntax ok
|
||||
function util.checkLuaSyntax(lua_text)
|
||||
local lua_code_ok, err = loadstring(lua_text)
|
||||
if lua_code_ok then
|
||||
return nil
|
||||
end
|
||||
-- Replace: [string "blah blah..."]:3: '=' expected near '123'
|
||||
-- with: Line 3: '=' expected near '123'
|
||||
err = err:gsub("%[string \".-%\"]:", "Line ")
|
||||
return err
|
||||
end
|
||||
|
||||
return util
|
||||
|
||||
498
plugins/texteditor.koplugin/main.lua
Normal file
498
plugins/texteditor.koplugin/main.lua
Normal file
@@ -0,0 +1,498 @@
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local DataStorage = require("datastorage")
|
||||
local Font = require("ui/font")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local LuaSettings = require("luasettings")
|
||||
local Notification = require("ui/widget/notification")
|
||||
local PathChooser = require("ui/widget/pathchooser")
|
||||
local Trapper = require("ui/trapper")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local ffiutil = require("ffi/util")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
local Screen = require("device").screen
|
||||
local T = ffiutil.template
|
||||
|
||||
local TextEditor = WidgetContainer:new{
|
||||
name = "text_editor",
|
||||
settings_file = DataStorage:getSettingsDir() .. "/text_editor.lua",
|
||||
settings = nil, -- loaded only when needed
|
||||
-- how many to display in menu (10x3 pages minus our 3 default menu items):
|
||||
history_menu_size = 27,
|
||||
history_keep_size = 60, -- hom many to keep in settings
|
||||
normal_font = "x_smallinfofont",
|
||||
monospace_font = "infont",
|
||||
min_file_size_warn = 200000, -- warn/ask when opening files bigger than this
|
||||
}
|
||||
|
||||
function TextEditor:init()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
end
|
||||
|
||||
function TextEditor:loadSettings()
|
||||
if self.settings then
|
||||
return
|
||||
end
|
||||
self.settings = LuaSettings:open(self.settings_file)
|
||||
self.history = self.settings:readSetting("history") or {}
|
||||
self.last_view_pos = self.settings:readSetting("last_view_pos") or {}
|
||||
self.last_path = self.settings:readSetting("last_path") or ffiutil.realpath(DataStorage:getDataDir())
|
||||
self.font_face = self.settings:readSetting("font_face") or self.normal_font
|
||||
self.font_size = self.settings:readSetting("font_size") or 20 -- x_smallinfofont default size
|
||||
-- The font settings could be saved in G_reader_setting if we want them
|
||||
-- to be re-used by default by InputDialog (on certain conditaions,
|
||||
-- when fullscreen or condensed or add_nav_bar...)
|
||||
--
|
||||
-- Allow users to set their prefered font manually in text_editor.lua
|
||||
-- (sadly, not via TextEditor itself, as they would be overriden on close)
|
||||
if self.settings:readSetting("normal_font") then
|
||||
self.normal_font = self.settings:readSetting("normal_font")
|
||||
end
|
||||
if self.settings:readSetting("monospace_font") then
|
||||
self.monospace_font = self.settings:readSetting("monospace_font")
|
||||
end
|
||||
end
|
||||
|
||||
function TextEditor:onFlushSettings()
|
||||
if self.settings then
|
||||
self.settings:saveSetting("history", self.history)
|
||||
self.settings:saveSetting("last_view_pos", self.last_view_pos)
|
||||
self.settings:saveSetting("last_path", self.last_path)
|
||||
self.settings:saveSetting("font_face", self.font_face)
|
||||
self.settings:saveSetting("font_size", self.font_size)
|
||||
self.settings:flush()
|
||||
end
|
||||
end
|
||||
|
||||
function TextEditor:addToMainMenu(menu_items)
|
||||
menu_items.text_editor = {
|
||||
text = _("Text editor"),
|
||||
sub_item_table_func = function()
|
||||
return self:getSubMenuItems()
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function TextEditor:getSubMenuItems()
|
||||
self:loadSettings()
|
||||
local sub_item_table = {
|
||||
{
|
||||
text = _("Text editor settings"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("Set text font size"),
|
||||
callback = function()
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local font_size = self.font_size
|
||||
UIManager:show(SpinWidget:new{
|
||||
width = Screen:getWidth() * 0.6,
|
||||
value = font_size,
|
||||
value_min = 8,
|
||||
value_max = 26,
|
||||
ok_text = _("Set font size"),
|
||||
title_text = _("Select font size"),
|
||||
callback = function(spin)
|
||||
self.font_size = spin.value
|
||||
end,
|
||||
})
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Use monospace font"),
|
||||
checked_func = function()
|
||||
return self.font_face == self.monospace_font
|
||||
end,
|
||||
callback = function()
|
||||
if self.font_face == self.monospace_font then
|
||||
self.font_face = self.normal_font
|
||||
else
|
||||
self.font_face = self.monospace_font
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("Select a file to open"),
|
||||
callback = function()
|
||||
self:chooseFile()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Edit a new empty file"),
|
||||
callback = function()
|
||||
self:newFile()
|
||||
end,
|
||||
separator = true,
|
||||
},
|
||||
}
|
||||
for i=1, math.min(#self.history, self.history_menu_size) do
|
||||
local file_path = self.history[i]
|
||||
local directory, filename = util.splitFilePathName(file_path) -- luacheck: no unused
|
||||
table.insert(sub_item_table, {
|
||||
text = T("%1. %2", i, filename),
|
||||
callback = function()
|
||||
self:checkEditFile(file_path, true)
|
||||
end,
|
||||
hold_callback = function()
|
||||
-- Show full path and some info, and propose to remove from history
|
||||
local text
|
||||
local attr = lfs.attributes(file_path)
|
||||
if attr then
|
||||
local filesize = util.getFormattedSize(attr.size)
|
||||
local lastmod = os.date("%Y-%m-%d %H:%M", attr.modification)
|
||||
text = T(_("File path:\n%1\n\nFile size: %2 bytes\nLast modified: %3\n\nRemove this file from text editor history?"),
|
||||
file_path, filesize, lastmod)
|
||||
else
|
||||
text = T(_("File path:\n%1\n\nThis file does not exist anymore.\n\nRemove it from text editor history?"),
|
||||
file_path)
|
||||
end
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = text,
|
||||
ok_text = _("Yes"),
|
||||
cancel_text = _("No"),
|
||||
ok_callback = function()
|
||||
self:removeFromHistory(file_path)
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
end
|
||||
return sub_item_table
|
||||
end
|
||||
|
||||
function TextEditor:removeFromHistory(file_path)
|
||||
for i=#self.history, 1, -1 do
|
||||
if self.history[i] == file_path then
|
||||
table.remove(self.history, i)
|
||||
end
|
||||
end
|
||||
self.last_view_pos[file_path] = nil
|
||||
end
|
||||
|
||||
function TextEditor:addToHistory(file_path)
|
||||
local new_history = {}
|
||||
table.insert(new_history, file_path)
|
||||
-- Trim history and cleanup duplicates
|
||||
local seen = {}
|
||||
seen[file_path] = true
|
||||
while #self.history > 0 and #new_history < self.history_keep_size do
|
||||
local item = table.remove(self.history, 1)
|
||||
if not seen[item] then
|
||||
table.insert(new_history, item)
|
||||
seen[item] = true
|
||||
end
|
||||
end
|
||||
self.history = new_history
|
||||
end
|
||||
|
||||
function TextEditor:newFile()
|
||||
self:loadSettings()
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = _([[To start editing a new file, you will have to:
|
||||
|
||||
- First select a directory
|
||||
- Then type the new file filename
|
||||
- And start editing it
|
||||
|
||||
Do you want to proceeed?]]),
|
||||
ok_text = _("Yes"),
|
||||
cancel_text = _("No"),
|
||||
ok_callback = function()
|
||||
local path_chooser = PathChooser:new{
|
||||
select_directory = true,
|
||||
select_file = false,
|
||||
height = Screen:getHeight(),
|
||||
path = self.last_path,
|
||||
onConfirm = function(dir_path)
|
||||
local file_input
|
||||
file_input = InputDialog:new{
|
||||
title = _("Enter new file filename"),
|
||||
input = dir_path == "/" and "/" or dir_path .. "/",
|
||||
buttons = {{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
callback = function()
|
||||
UIManager:close(file_input)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Edit"),
|
||||
callback = function()
|
||||
local file_path = file_input:getInputText()
|
||||
UIManager:close(file_input)
|
||||
-- Remember last_path
|
||||
self.last_path = file_path:match("(.*)/")
|
||||
if self.last_path == "" then self.last_path = "/" end
|
||||
self:checkEditFile(file_path, false, true)
|
||||
end,
|
||||
},
|
||||
}},
|
||||
}
|
||||
UIManager:show(file_input)
|
||||
file_input:onShowKeyboard()
|
||||
end,
|
||||
}
|
||||
UIManager:show(path_chooser)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function TextEditor:chooseFile()
|
||||
self:loadSettings()
|
||||
local path_chooser = PathChooser:new{
|
||||
select_file = true,
|
||||
select_directory = false,
|
||||
detailed_file_info = true,
|
||||
height = Screen:getHeight(),
|
||||
path = self.last_path,
|
||||
onConfirm = function(file_path)
|
||||
-- Remember last_path only when we select a file from it
|
||||
self.last_path = file_path:match("(.*)/")
|
||||
if self.last_path == "" then self.last_path = "/" end
|
||||
self:checkEditFile(file_path)
|
||||
end
|
||||
}
|
||||
UIManager:show(path_chooser)
|
||||
end
|
||||
|
||||
function TextEditor:checkEditFile(file_path, from_history, possibly_new_file)
|
||||
local attr = lfs.attributes(file_path)
|
||||
if not possibly_new_file and not attr then
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = T(_("This file does not exist anymore:\n\n%1\n\nDo you want to create it and start editing it?"), file_path),
|
||||
ok_text = _("Yes"),
|
||||
cancel_text = _("No"),
|
||||
ok_callback = function()
|
||||
-- go again thru there with possibly_new_file=true
|
||||
self:checkEditFile(file_path, from_history, true)
|
||||
end,
|
||||
})
|
||||
return
|
||||
end
|
||||
if attr then
|
||||
-- File exists: get its real path with symlink and ../ resolved
|
||||
file_path = ffiutil.realpath(file_path)
|
||||
attr = lfs.attributes(file_path)
|
||||
end
|
||||
if attr then -- File exists
|
||||
if attr.mode ~= "file" then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("This file is not a regular file:\n\n%1"), file_path)
|
||||
})
|
||||
return
|
||||
end
|
||||
-- Check if file is writable ("r+b" checks that, and does not
|
||||
-- update the last mod timestamp, unlike "wb")
|
||||
-- No need to warn if readonly, the user will know it when we open
|
||||
-- without keyboard and the Save button says "Read only".
|
||||
local readonly = true
|
||||
local file = io.open(file_path, 'r+b')
|
||||
if file then
|
||||
file:close()
|
||||
readonly = false
|
||||
end
|
||||
-- Don't check size if coming from history: user had already confirmed it
|
||||
if not from_history and attr.size > self.min_file_size_warn then
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = T(_("This file is %2:\n\n%1\n\nAre you sure you want to open it?\n\nOpening big files may take some time."),
|
||||
file_path, util.getFriendlySize(attr.size)),
|
||||
ok_text = _("Yes"),
|
||||
cancel_text = _("No"),
|
||||
ok_callback = function()
|
||||
self:editFile(file_path, readonly)
|
||||
end,
|
||||
})
|
||||
else
|
||||
self:editFile(file_path, readonly)
|
||||
end
|
||||
else -- File does not exist
|
||||
-- Try to create it just to check if writting to it later is possible
|
||||
local file, err = io.open(file_path, "wb")
|
||||
if file then
|
||||
-- Clean it, we'll create it again on Save, and allow closing
|
||||
-- without saving in case the user has changed his mind.
|
||||
file:close()
|
||||
os.remove(file_path)
|
||||
self:editFile(file_path)
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("This file can not be created:\n\n%1\n\nReason: %2"), file_path, err)
|
||||
})
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TextEditor:readFileContent(file_path)
|
||||
local file = io.open(file_path, "rb")
|
||||
if not file then
|
||||
-- We checked file existence before, so assume it's
|
||||
-- because it's a new file
|
||||
return ""
|
||||
end
|
||||
local file_content = file:read("*all")
|
||||
file:close()
|
||||
return file_content
|
||||
end
|
||||
|
||||
function TextEditor:saveFileContent(file_path, content)
|
||||
local file, err = io.open(file_path, "wb")
|
||||
if file then
|
||||
file:write(content)
|
||||
file:close()
|
||||
logger.info("TextEditor: saved file", file_path)
|
||||
return true
|
||||
end
|
||||
logger.info("TextEditor: failed saving file", file_path, ":", err)
|
||||
return false, err
|
||||
end
|
||||
|
||||
function TextEditor:deleteFile(file_path)
|
||||
local ok, err = os.remove(file_path)
|
||||
if ok then
|
||||
logger.info("TextEditor: deleted file", file_path)
|
||||
return true
|
||||
end
|
||||
logger.info("TextEditor: failed deleting file", file_path, ":", err)
|
||||
return false, err
|
||||
end
|
||||
|
||||
function TextEditor:editFile(file_path, readonly)
|
||||
self:addToHistory(file_path)
|
||||
local directory, filename = util.splitFilePathName(file_path) -- luacheck: no unused
|
||||
local filename_without_suffix, filetype = util.splitFileNameSuffix(filename) -- luacheck: no unused
|
||||
local is_lua = filetype:lower() == "lua"
|
||||
local input
|
||||
input = InputDialog:new{
|
||||
title = filename,
|
||||
input = self:readFileContent(file_path),
|
||||
input_face = Font:getFace(self.font_face, self.font_size),
|
||||
fullscreen = true,
|
||||
condensed = true,
|
||||
allow_newline = true,
|
||||
cursor_at_end = false,
|
||||
readonly = readonly,
|
||||
add_nav_bar = true,
|
||||
buttons = is_lua and {{
|
||||
-- First button on first row, that will be filled with Reset|Save|Close
|
||||
{
|
||||
text = _("Check lua"),
|
||||
callback = function()
|
||||
local parse_error = util.checkLuaSyntax(input:getInputText())
|
||||
if parse_error then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("lua syntax check failed:\n\n%1"), parse_error)
|
||||
})
|
||||
else
|
||||
UIManager:show(Notification:new{
|
||||
text = T(_("lua syntax OK")),
|
||||
timeout = 2,
|
||||
})
|
||||
end
|
||||
end,
|
||||
},
|
||||
}},
|
||||
-- Set/save 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.
|
||||
if top_line_num and charpos then
|
||||
self.last_view_pos[file_path] = {top_line_num, charpos}
|
||||
else
|
||||
local prev_pos = self.last_view_pos[file_path]
|
||||
if type(prev_pos) == "table" and prev_pos[1] and prev_pos[2] then
|
||||
return prev_pos[1], prev_pos[2]
|
||||
end
|
||||
return nil, nil -- no previous position known
|
||||
end
|
||||
end,
|
||||
-- File restoring callback
|
||||
reset_callback = function(content) -- Will add a Reset button
|
||||
return self:readFileContent(file_path), _("Text reset to last saved content")
|
||||
end,
|
||||
-- File saving callback
|
||||
save_callback = function(content, closing) -- Will add Save/Close buttons
|
||||
if self.readonly then
|
||||
-- We shouldn't be called if read-only, but just in case
|
||||
return false, _("File is read only")
|
||||
end
|
||||
if content and #content > 0 then
|
||||
if not is_lua then
|
||||
local ok, err = self:saveFileContent(file_path, content)
|
||||
if ok then
|
||||
return true, _("File saved")
|
||||
else
|
||||
return false, T(_("Failed saving file: %1"), err)
|
||||
end
|
||||
end
|
||||
local parse_error = util.checkLuaSyntax(content)
|
||||
if not parse_error then
|
||||
local ok, err = self:saveFileContent(file_path, content)
|
||||
if ok then
|
||||
return true, _("lua syntax OK, file saved")
|
||||
else
|
||||
return false, T(_("Failed saving file: %1"), err)
|
||||
end
|
||||
end
|
||||
local save_anyway = Trapper:confirm(T(_(
|
||||
[[lua syntax check failed:
|
||||
|
||||
%1
|
||||
|
||||
KOReader may crash if this is saved.
|
||||
Do you really want to save to this file?
|
||||
|
||||
%2]]), parse_error, file_path), _("Do not save"), _("Save anyway"))
|
||||
-- we'll get the safer "Do not save" on tap outside
|
||||
if save_anyway then
|
||||
local ok, err = self:saveFileContent(file_path, content)
|
||||
if ok then
|
||||
return true, _("File saved")
|
||||
else
|
||||
return false, T(_("Failed saving file: %1"), err)
|
||||
end
|
||||
else
|
||||
return false, false -- no need for more InfoMessage
|
||||
end
|
||||
else -- If content is empty, propose to delete the file
|
||||
local delete_file = Trapper:confirm(T(_(
|
||||
[[Text content is empty.
|
||||
Do you want to keep this file as empty, or do you prefer to delete it?
|
||||
|
||||
%1]]), file_path), _("Keep empty file"), _("Delete file"))
|
||||
-- we'll get the safer "Keep empty file" on tap outside
|
||||
if delete_file then
|
||||
local ok, err = self:deleteFile(file_path)
|
||||
if ok then
|
||||
return true, _("File deleted")
|
||||
else
|
||||
return false, T(_("Failed deleting file: %1"), err)
|
||||
end
|
||||
else
|
||||
local ok, err = self:saveFileContent(file_path, content)
|
||||
if ok then
|
||||
return true, _("File saved")
|
||||
else
|
||||
return false, T(_("Failed saving file: %1"), err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
}
|
||||
UIManager:show(input)
|
||||
input:onShowKeyboard()
|
||||
-- Note about self.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
|
||||
-- own readonly prevention (and on devices where we run as root, we
|
||||
-- will hardly ever be readonly).
|
||||
end
|
||||
|
||||
return TextEditor
|
||||
Reference in New Issue
Block a user