2021-06-01 10:06:55 +00:00
|
|
|
local DataStorage = require("datastorage")
|
|
|
|
local Event = require("ui/event")
|
|
|
|
local FFIUtil = require("ffi/util")
|
|
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
|
|
local UIManager = require("ui/uimanager")
|
2021-07-15 10:53:28 +00:00
|
|
|
local Utf8Proc = require("ffi/utf8proc")
|
2021-06-01 10:06:55 +00:00
|
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
|
|
local logger = require("logger")
|
|
|
|
local _ = require("gettext")
|
|
|
|
local T = require("ffi/util").template
|
|
|
|
|
2022-03-16 20:30:58 +00:00
|
|
|
-- if sometime in the future crengine is updated to use normalized utf8 for hypenation
|
|
|
|
-- this variable can be set to `true`. (see discussion in : https://github.com/koreader/crengine/pull/466),
|
|
|
|
-- and some `if NORM then` branches can be simplified.
|
|
|
|
local NORM = false
|
|
|
|
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
|
|
|
local ReaderUserHyph = WidgetContainer:extend{
|
2021-06-01 10:06:55 +00:00
|
|
|
-- return values from setUserHyphenationDict (crengine's UserHyphDict::init())
|
|
|
|
USER_DICT_RELOAD = 0,
|
|
|
|
USER_DICT_NOCHANGE = 1,
|
|
|
|
USER_DICT_MALFORMED = 2,
|
|
|
|
USER_DICT_ERROR_NOT_SORTED = 3,
|
|
|
|
}
|
|
|
|
|
|
|
|
-- returns path to the user dictionary
|
|
|
|
function ReaderUserHyph:getDictionaryPath()
|
|
|
|
return FFIUtil.joinPath(DataStorage:getSettingsDir(),
|
|
|
|
"user-" .. tostring(self.ui.document:getTextMainLangDefaultHyphDictionary():gsub(".pattern$", "")) .. ".hyph")
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Load the user dictionary suitable for the actual language
|
|
|
|
-- if reload==true, force a reload
|
|
|
|
-- Unload is done automatically when a new dictionary is loaded.
|
2022-03-16 20:30:58 +00:00
|
|
|
function ReaderUserHyph:loadDictionary(name, reload, no_scrubbing)
|
2022-09-27 23:10:50 +00:00
|
|
|
local cre = require("document/credocument"):engineInit()
|
2021-06-01 10:06:55 +00:00
|
|
|
if G_reader_settings:isTrue("hyph_user_dict") and lfs.attributes(name, "mode") == "file" then
|
2022-03-16 20:30:58 +00:00
|
|
|
logger.dbg("set user hyphenation dict", name, reload, no_scrubbing)
|
2021-07-15 10:53:28 +00:00
|
|
|
local ret = cre.setUserHyphenationDict(name, reload)
|
2021-06-01 10:06:55 +00:00
|
|
|
-- this should only happen, if a user edits a dictionary by hand or the user messed
|
|
|
|
-- with the dictionary file by hand. -> Warning and disable.
|
|
|
|
if ret == self.USER_DICT_ERROR_NOT_SORTED then
|
2022-03-16 20:30:58 +00:00
|
|
|
if no_scrubbing then
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
|
|
text = T(_("The user dictionary\n%1\nis not alphabetically sorted.\n\nIt will be disabled now."), name),
|
|
|
|
})
|
|
|
|
logger.warn("UserHyph: Dictionary " .. name .. " is not sorted alphabetically.")
|
|
|
|
G_reader_settings:makeFalse("hyph_user_dict")
|
|
|
|
else
|
|
|
|
self:scrubDictionary()
|
|
|
|
self:loadDictionary(name, reload, true)
|
|
|
|
end
|
2021-06-01 10:06:55 +00:00
|
|
|
elseif ret == self.USER_DICT_MALFORMED then
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
|
|
text = T(_("The user dictionary\n%1\nhas corrupted entries.\n\nOnly valid entries will be used."), name),
|
|
|
|
})
|
|
|
|
logger.warn("UserHyph: Dictionary " .. name .. " has corrupted entries.")
|
|
|
|
end
|
|
|
|
else
|
2022-03-16 20:30:58 +00:00
|
|
|
logger.dbg("UserHyph: reset user hyphenation dict")
|
2021-07-15 10:53:28 +00:00
|
|
|
cre.setUserHyphenationDict("", true) -- clear crengine user hyph dict
|
2021-06-01 10:06:55 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Reload on change of the hyphenation language
|
|
|
|
function ReaderUserHyph:onTypographyLanguageChanged()
|
|
|
|
self:loadUserDictionary()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Reload on "ChangedUserDictionary" event,
|
|
|
|
-- doesn't load dictionary if filesize and filename haven't changed
|
|
|
|
-- if reload==true reload
|
|
|
|
function ReaderUserHyph:loadUserDictionary(reload)
|
|
|
|
self:loadDictionary(self:isAvailable() and self:getDictionaryPath() or "", reload and true or false)
|
|
|
|
self.ui:handleEvent(Event:new("UpdatePos"))
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Functions to use with the UI
|
|
|
|
|
|
|
|
function ReaderUserHyph:isAvailable()
|
|
|
|
return G_reader_settings:isTrue("hyph_user_dict") and self:_enabled()
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderUserHyph:_enabled()
|
|
|
|
return self.ui.typography.hyphenation
|
|
|
|
end
|
|
|
|
|
|
|
|
-- add Menu entry
|
|
|
|
function ReaderUserHyph:getMenuEntry()
|
|
|
|
return {
|
|
|
|
text = _("Custom hyphenation rules"),
|
|
|
|
help_text = _("The hyphenation of a word can be changed from its default by long pressing for 3 seconds and selecting 'Hyphenate'."),
|
|
|
|
callback = function()
|
|
|
|
local hyph_user_dict = not G_reader_settings:isTrue("hyph_user_dict")
|
|
|
|
G_reader_settings:saveSetting("hyph_user_dict", hyph_user_dict)
|
|
|
|
self:loadUserDictionary() -- not needed to force a reload here
|
|
|
|
end,
|
|
|
|
checked_func = function()
|
|
|
|
return self:isAvailable()
|
|
|
|
end,
|
|
|
|
enabled_func = function()
|
|
|
|
return self:_enabled()
|
|
|
|
end,
|
|
|
|
separator = true,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Helper functions for dictionary entries-------------------------------------------
|
|
|
|
|
|
|
|
-- checks if suggestion is well formated
|
|
|
|
function ReaderUserHyph:checkHyphenation(suggestion, word)
|
|
|
|
if suggestion:find("%-%-") then
|
|
|
|
return false -- two or more consecutive '-'
|
|
|
|
end
|
|
|
|
|
|
|
|
suggestion = suggestion:gsub("-","")
|
2022-03-16 20:30:58 +00:00
|
|
|
if Utf8Proc.lowercase(suggestion, NORM) == Utf8Proc.lowercase(word, NORM) then
|
2021-06-01 10:06:55 +00:00
|
|
|
return true -- characters match (case insensitive)
|
|
|
|
end
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderUserHyph:updateDictionary(word, hyphenation)
|
2022-03-16 20:30:58 +00:00
|
|
|
if not word then
|
|
|
|
logger.err("UserHyph: called without arguments")
|
|
|
|
end
|
2021-06-01 10:06:55 +00:00
|
|
|
local dict_file = self:getDictionaryPath()
|
|
|
|
local new_dict_file = dict_file .. ".new"
|
|
|
|
|
|
|
|
local new_dict = io.open(new_dict_file, "w")
|
|
|
|
if not new_dict then
|
|
|
|
logger.err("UserHyph: could not open " .. new_dict_file)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2022-03-16 20:30:58 +00:00
|
|
|
if NORM then
|
|
|
|
word = Utf8Proc.normalize_NFC(word)
|
|
|
|
end
|
|
|
|
|
|
|
|
local word_lower = Utf8Proc.lowercase(word, NORM)
|
2021-06-01 10:06:55 +00:00
|
|
|
local line
|
|
|
|
|
|
|
|
local dict = io.open(dict_file, "r")
|
|
|
|
if dict then
|
|
|
|
line = dict:read()
|
2022-03-16 20:30:58 +00:00
|
|
|
if NORM then
|
|
|
|
line = line and Utf8Proc.normalize_NFC(line)
|
|
|
|
end
|
2021-06-01 10:06:55 +00:00
|
|
|
--search entry
|
2022-03-16 20:30:58 +00:00
|
|
|
while line and Utf8Proc.lowercase(line:sub(1, line:find(";") - 1), NORM) < word_lower do
|
2021-06-01 10:06:55 +00:00
|
|
|
new_dict:write(line .. "\n")
|
|
|
|
line = dict:read()
|
2022-03-16 20:30:58 +00:00
|
|
|
if NORM then
|
|
|
|
line = line and Utf8Proc.normalize_NFC(line)
|
|
|
|
end
|
2021-06-01 10:06:55 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- last word = nil if EOF, else last_word=word if found in file, else last_word is word after the new entry
|
|
|
|
if line then
|
2022-03-16 20:30:58 +00:00
|
|
|
local last_word = Utf8Proc.lowercase(line:sub(1, line:find(";") - 1), NORM)
|
|
|
|
if last_word == word_lower then
|
2021-06-01 10:06:55 +00:00
|
|
|
line = nil -- word found
|
|
|
|
end
|
|
|
|
else
|
|
|
|
line = nil -- EOF
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- write new entry
|
|
|
|
if hyphenation and hyphenation ~= "" then
|
|
|
|
new_dict:write(string.format("%s;%s\n", word, hyphenation))
|
|
|
|
end
|
|
|
|
|
|
|
|
-- write old entry if there was one
|
|
|
|
if line then
|
|
|
|
new_dict:write(line .. "\n")
|
|
|
|
end
|
|
|
|
|
|
|
|
if dict then
|
|
|
|
repeat
|
|
|
|
line = dict:read()
|
2022-03-16 20:30:58 +00:00
|
|
|
if NORM then
|
|
|
|
line = line and Utf8Proc.normalize_NFC(line)
|
|
|
|
end
|
2021-06-01 10:06:55 +00:00
|
|
|
if line then
|
|
|
|
new_dict:write(line .. "\n")
|
|
|
|
end
|
|
|
|
until (not line)
|
|
|
|
dict:close()
|
|
|
|
os.remove(dict_file)
|
|
|
|
end
|
|
|
|
|
|
|
|
new_dict:close()
|
|
|
|
os.rename(new_dict_file, dict_file)
|
|
|
|
|
|
|
|
self:loadUserDictionary(true) -- dictionary has changed, force a reload here
|
|
|
|
end
|
|
|
|
|
2022-03-16 20:30:58 +00:00
|
|
|
-- This is called when the file is badly sorted or has double entries (which should only happen
|
|
|
|
-- if a user has edited the hyphenation file by hand).
|
|
|
|
function ReaderUserHyph:scrubDictionary()
|
|
|
|
logger.dbg("UserHyph: scrubbing and sorting user hyphenation dict")
|
|
|
|
|
|
|
|
local dict_file = self:getDictionaryPath()
|
|
|
|
local dict = io.open(dict_file, "r")
|
|
|
|
if not dict then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local dict_entries = {}
|
|
|
|
|
|
|
|
local line = dict:read()
|
|
|
|
if NORM then
|
|
|
|
line = line and Utf8Proc.normalize_NFC(line)
|
|
|
|
end
|
|
|
|
while line do
|
|
|
|
table.insert(dict_entries, line)
|
|
|
|
line = dict:read()
|
|
|
|
if NORM then
|
|
|
|
line = line and Utf8Proc.normalize_NFC(line)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
dict:close()
|
|
|
|
|
|
|
|
if #dict_entries == 1 then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
table.sort(dict_entries, function(a,b) return Utf8Proc.lowercase(a, NORM) < Utf8Proc.lowercase(b, NORM) end)
|
|
|
|
|
|
|
|
-- remove double entries
|
|
|
|
local later_key = Utf8Proc.lowercase(dict_entries[#dict_entries]:gsub(";.*$",""), NORM)
|
|
|
|
for i = #dict_entries-1, 1, -1 do
|
|
|
|
local former_key = Utf8Proc.lowercase(dict_entries[i]:gsub(";.*$",""), NORM)
|
|
|
|
if later_key == former_key then
|
|
|
|
logger.dbg("UserHyph: remove double entry", dict_entries[i])
|
|
|
|
table.remove(dict_entries, i)
|
|
|
|
end
|
|
|
|
later_key = former_key
|
|
|
|
end
|
|
|
|
|
|
|
|
local new_dict_file = dict_file .. ".new"
|
|
|
|
|
|
|
|
local new_dict = io.open(new_dict_file, "w")
|
|
|
|
if not new_dict then
|
|
|
|
logger.err("UserHyph: could not open " .. new_dict_file)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
for i = 1, #dict_entries do
|
|
|
|
new_dict:write(dict_entries[i], "\n")
|
|
|
|
end
|
|
|
|
new_dict:close()
|
|
|
|
|
|
|
|
os.remove(dict_file)
|
|
|
|
os.rename(new_dict_file, dict_file)
|
|
|
|
end
|
|
|
|
|
2021-06-01 10:06:55 +00:00
|
|
|
function ReaderUserHyph:modifyUserEntry(word)
|
|
|
|
if word:find("[ ,;-%.]") then return end -- no button if more than one word
|
|
|
|
|
|
|
|
if not self.ui.document then return end
|
|
|
|
|
2022-03-16 20:30:58 +00:00
|
|
|
if NORM then
|
|
|
|
word = Utf8Proc.normalize_NFC(word)
|
|
|
|
end
|
|
|
|
|
2022-09-27 23:10:50 +00:00
|
|
|
local cre = require("document/credocument"):engineInit()
|
2021-07-15 10:53:28 +00:00
|
|
|
local suggested_hyphenation = cre.getHyphenationForWord(word)
|
2021-06-01 10:06:55 +00:00
|
|
|
|
2022-03-16 20:30:58 +00:00
|
|
|
-- word may have some strange punctuation marks (as the upper dot),
|
|
|
|
-- so we use crengine to trimm that.
|
|
|
|
word = suggested_hyphenation:gsub("-","")
|
|
|
|
|
2021-06-01 10:06:55 +00:00
|
|
|
local input_dialog
|
|
|
|
input_dialog = InputDialog:new{
|
|
|
|
title = T(_("Hyphenate: %1"), word),
|
|
|
|
description = _("Add hyphenation positions with hyphens ('-') or spaces (' ')."),
|
|
|
|
input = suggested_hyphenation,
|
2022-03-16 20:30:58 +00:00
|
|
|
old_hyph_lowercase = Utf8Proc.lowercase(suggested_hyphenation, NORM),
|
2021-06-01 10:06:55 +00:00
|
|
|
input_type = "string",
|
|
|
|
buttons = {
|
|
|
|
{
|
|
|
|
{
|
|
|
|
text = _("Cancel"),
|
2022-03-04 20:20:00 +00:00
|
|
|
id = "close",
|
2021-06-01 10:06:55 +00:00
|
|
|
callback = function()
|
|
|
|
UIManager:close(input_dialog)
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Remove"),
|
|
|
|
callback = function()
|
|
|
|
UIManager:close(input_dialog)
|
|
|
|
self:updateDictionary(word)
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Save"),
|
|
|
|
is_enter_default = true,
|
|
|
|
callback = function()
|
|
|
|
local new_suggestion = input_dialog:getInputText()
|
|
|
|
new_suggestion = new_suggestion:gsub(" ","-") -- replace spaces with hyphens
|
|
|
|
new_suggestion = new_suggestion:gsub("^-","") -- remove leading hypenations
|
|
|
|
new_suggestion = new_suggestion:gsub("-$","") -- remove trailing hypenations
|
|
|
|
|
|
|
|
if self:checkHyphenation(new_suggestion, word) then
|
|
|
|
-- don't save if no changes
|
2022-03-16 20:30:58 +00:00
|
|
|
if Utf8Proc.lowercase(new_suggestion, NORM) ~= input_dialog.old_hyph_lowercase then
|
2021-06-01 10:06:55 +00:00
|
|
|
self:updateDictionary(word, new_suggestion)
|
|
|
|
end
|
|
|
|
UIManager:close(input_dialog)
|
|
|
|
else
|
|
|
|
UIManager:show(InfoMessage:new{
|
2022-03-16 20:30:58 +00:00
|
|
|
text = _("Invalid hyphenation!"),
|
2021-06-01 10:06:55 +00:00
|
|
|
})
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
UIManager:show(input_dialog)
|
|
|
|
input_dialog:onShowKeyboard()
|
|
|
|
end
|
|
|
|
|
|
|
|
return ReaderUserHyph
|