2
0
mirror of https://github.com/koreader/koreader synced 2024-10-31 21:20:20 +00:00
koreader/plugins/texteditor.koplugin/main.lua
poire-z 850be52177
Keep some menus open when Tap or Hold (#4189)
TouchMenu: added options to menu items with the following defaults:
    keep_menu_open = false
    hold_keep_menu_open = true
So, default for Tap callback is to close menu, and for Hold callback
to keep menu open.
In both cases, provide the TouchMenu instance as the 1st argument to
the callback functions (instead of a refresh_menu_func I added in #3941)
so the callback can do more things, like closing, refreshing,
changing menu items text and re-ordering...

ReaderZooming: show symbol for default (like it was done for
ReaderFont, ReaderHyphenation...)
TextEditor plugin: update the previously opened files list in real
time, so the menu can be kept open and used as the TextEditor main
interface.
SSH plugin: keep menu open and update the Start/Stop state in real time
ReadTimer plugin: tried to do what feels right (but I don't use it)

Also remove forgotten cp in the move/paste file code
2018-09-04 23:55:58 +02:00

541 lines
21 KiB
Lua

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 = "texteditor",
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()
self.whenDoneFunc = nil -- discard reference to previous TouchMenu instance
local sub_item_table = {
{
text = _("Text editor settings"),
sub_item_table = {
{
text = _("Set text font size"),
keep_menu_open = true,
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"),
keep_menu_open = true,
callback = function(touchmenu_instance)
self:setupWhenDoneFunc(touchmenu_instance)
self:chooseFile()
end,
},
{
text = _("Edit a new empty file"),
keep_menu_open = true,
callback = function(touchmenu_instance)
self:setupWhenDoneFunc(touchmenu_instance)
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),
keep_menu_open = true,
callback = function(touchmenu_instance)
self:setupWhenDoneFunc(touchmenu_instance)
self:checkEditFile(file_path, true)
end,
_texteditor_id = file_path, -- for removal from menu itself
hold_callback = function(touchmenu_instance)
-- 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)
-- Also remove from menu itself
for j=1, #sub_item_table do
if sub_item_table[j]._texteditor_id == file_path then
table.remove(sub_item_table, j)
break
end
end
touchmenu_instance:updateItems()
end,
})
end,
})
end
return sub_item_table
end
function TextEditor:setupWhenDoneFunc(touchmenu_instance)
-- This will keep a reference to the TouchMenu instance, that may not
-- get released if file opening is aborted while in the file selection
-- widgets and dialogs (quite complicated to call a resetWhenDoneFunc()
-- in every abort case). But :getSubMenuItems() will release it when
-- the TextEditor menu is opened again.
self.whenDoneFunc = function()
touchmenu_instance.item_table = self:getSubMenuItems()
touchmenu_instance.page = 1
touchmenu_instance:updateItems()
end
end
function TextEditor:execWhenDoneFunc()
if self.whenDoneFunc then
self.whenDoneFunc()
self.whenDoneFunc = nil
end
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 enter a name for the new file
- And start editing it
Do you want to proceed?]]),
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 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,
scroll_by_pan = 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,
-- Close callback
close_callback = function()
self:execWhenDoneFunc()
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