2
0
mirror of https://github.com/koreader/koreader synced 2024-11-11 19:11:14 +00:00
koreader/plugins/profiles.koplugin/main.lua

471 lines
18 KiB
Lua
Raw Normal View History

local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local Dispatcher = require("dispatcher")
2020-09-15 18:39:32 +00:00
local FFIUtil = require("ffi/util")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local LuaSettings = require("luasettings")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local _ = require("gettext")
2020-09-15 18:39:32 +00:00
local T = FFIUtil.template
2022-09-17 22:11:03 +00:00
local util = require("util")
local autostart_done = 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 Profiles = WidgetContainer:extend{
name = "profiles",
prefix = "profile_exec_",
profiles_file = DataStorage:getSettingsDir() .. "/profiles.lua",
profiles = nil,
data = nil,
updated = false,
}
function Profiles:init()
Dispatcher:init()
self.ui.menu:registerToMainMenu(self)
2022-09-17 22:11:03 +00:00
self:onDispatcherRegisterActions()
self:executeAutostart()
end
function Profiles:loadProfiles()
if self.profiles then
return
end
self.profiles = LuaSettings:open(self.profiles_file)
self.data = self.profiles.data
-- ensure profile name
for k, v in pairs(self.data) do
if not v.settings then
self.data[k].settings = {}
end
if not self.data[k].settings.name then
self.data[k].settings.name = k
self.updated = true
end
end
self:onFlushSettings()
end
function Profiles:onFlushSettings()
if self.profiles and self.updated then
self.profiles:flush()
self.updated = false
end
end
2022-09-17 22:11:03 +00:00
local function dispatcherRegisterProfile(name)
Dispatcher:registerAction(Profiles.prefix..name,
{category="none", event="ProfileExecute", arg=name, title=T(_("Profile %1"), name), general=true})
2022-09-17 22:11:03 +00:00
end
local function dispatcherUnregisterProfile(name)
Dispatcher:removeAction(Profiles.prefix..name)
2022-09-17 22:11:03 +00:00
end
function Profiles:onDispatcherRegisterActions()
self:loadProfiles()
for k, v in pairs(self.data) do
if v.settings.registered then
dispatcherRegisterProfile(k)
end
2022-09-17 22:11:03 +00:00
end
end
function Profiles:addToMainMenu(menu_items)
menu_items.profiles = {
text = _("Profiles"),
sub_item_table_func = function()
return self:getSubMenuItems()
end,
}
end
function Profiles:getSubMenuItems()
self:loadProfiles()
local sub_item_table = {
{
text = _("New"),
keep_menu_open = true,
callback = function(touchmenu_instance)
2022-09-17 22:11:03 +00:00
local function editCallback(new_name)
self.data[new_name] = {["settings"] = {["name"] = new_name}}
2022-09-17 22:11:03 +00:00
self.updated = true
touchmenu_instance.item_table = self:getSubMenuItems()
touchmenu_instance.page = 1
touchmenu_instance:updateItems()
end
self:editProfileName(editCallback)
end,
},
{
text = _("New with current document settings"),
enabled = self.ui.file_chooser == nil,
keep_menu_open = true,
callback = function(touchmenu_instance)
local function editCallback(new_name)
self.data[new_name] = self:getProfileFromCurrentDocument(new_name)
self.updated = true
touchmenu_instance.item_table = self:getSubMenuItems()
touchmenu_instance.page = 1
touchmenu_instance:updateItems()
end
self:editProfileName(editCallback)
end,
separator = true,
2022-09-17 22:11:03 +00:00
},
}
2022-09-17 22:11:03 +00:00
for k, v in FFIUtil.orderedPairs(self.data) do
local edit_actions_sub_items = {}
Dispatcher:addSubMenu(self, edit_actions_sub_items, self.data, k)
local sub_items = {
{
2022-09-17 22:11:03 +00:00
text = _("Execute"),
callback = function(touchmenu_instance)
touchmenu_instance:onClose()
self:onProfileExecute(k, { qm_show = false })
end,
},
{
text = _("Show as QuickMenu"),
callback = function(touchmenu_instance)
touchmenu_instance:onClose()
self:onProfileExecute(k, { qm_show = true })
2022-09-17 22:11:03 +00:00
end,
},
{
text = _("Autostart"),
help_text = _("Execute this profile when KOReader is started with 'file browser' or 'last file'."),
checked_func = function()
2022-09-17 22:11:03 +00:00
return G_reader_settings:getSettingForExt("autostart_profiles", k)
end,
callback = function()
2022-09-17 22:11:03 +00:00
local new_value = not G_reader_settings:getSettingForExt("autostart_profiles", k) or nil
G_reader_settings:saveSettingForExt("autostart_profiles", new_value, k)
end,
separator = true,
},
{
text = _("Show in action list"),
checked_func = function()
return v.settings.registered
end,
callback = function(touchmenu_instance)
if v.settings.registered then
dispatcherUnregisterProfile(k)
self:updateGestures(self.prefix..k)
self:updateProfiles(self.prefix..k)
self.data[k].settings.registered = nil
else
dispatcherRegisterProfile(k)
self.data[k].settings.registered = true
end
self.updated = true
local actions_sub_menu = {}
Dispatcher:addSubMenu(self, actions_sub_menu, self.data, k)
touchmenu_instance.item_table[5].sub_item_table = actions_sub_menu -- "Edit actions" submenu (item #5)
touchmenu_instance.item_table_stack[#touchmenu_instance.item_table_stack] = self:getSubMenuItems()
end,
},
2022-09-17 22:11:03 +00:00
{
text_func = function() return T(_("Edit actions: (%1)"), Dispatcher:menuTextFunc(v)) end,
2022-09-17 22:11:03 +00:00
sub_item_table = edit_actions_sub_items,
separator = true,
2022-09-17 22:11:03 +00:00
},
{
text = T(_("Rename: %1"), k),
keep_menu_open = true,
callback = function(touchmenu_instance)
local function editCallback(new_name)
self.data[new_name] = util.tableDeepCopy(v)
self.data[new_name].settings.name = new_name
self:updateAutostart(k, new_name)
if v.settings.registered then
dispatcherUnregisterProfile(k)
dispatcherRegisterProfile(new_name)
self:updateGestures(self.prefix..k, self.prefix..new_name)
self:updateProfiles(self.prefix..k, self.prefix..new_name)
end
2022-09-17 22:11:03 +00:00
self.data[k] = nil
self.updated = true
touchmenu_instance.item_table = self:getSubMenuItems()
touchmenu_instance:updateItems()
table.remove(touchmenu_instance.item_table_stack)
2022-09-17 22:11:03 +00:00
end
self:editProfileName(editCallback, k)
end,
},
{
text = _("Duplicate"),
2022-09-17 22:11:03 +00:00
keep_menu_open = true,
callback = function(touchmenu_instance)
local function editCallback(new_name)
self.data[new_name] = util.tableDeepCopy(v)
self.data[new_name].settings.name = new_name
if v.settings.registered then
dispatcherRegisterProfile(new_name)
end
2022-09-17 22:11:03 +00:00
self.updated = true
touchmenu_instance.item_table = self:getSubMenuItems()
touchmenu_instance:updateItems()
table.remove(touchmenu_instance.item_table_stack)
2022-09-17 22:11:03 +00:00
end
self:editProfileName(editCallback, k)
end,
},
{
text = _("Delete"),
keep_menu_open = true,
separator = true,
callback = function(touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = _("Do you want to delete this profile?"),
ok_text = _("Delete"),
ok_callback = function()
self:updateAutostart(k)
if v.settings.registered then
dispatcherUnregisterProfile(k)
self:updateGestures(self.prefix..k)
self:updateProfiles(self.prefix..k)
end
2022-09-17 22:11:03 +00:00
self.data[k] = nil
self.updated = true
touchmenu_instance.item_table = self:getSubMenuItems()
touchmenu_instance:updateItems()
table.remove(touchmenu_instance.item_table_stack)
2022-09-17 22:11:03 +00:00
end,
})
end,
},
}
table.insert(sub_item_table, {
text_func = function()
return (v.settings.show_as_quickmenu and "\u{F0CA} " or "\u{F144} ") .. k
end,
hold_keep_menu_open = false,
sub_item_table = sub_items,
hold_callback = function()
self:onProfileExecute(k)
end,
})
end
return sub_item_table
end
function Profiles:onProfileExecute(name, exec_props)
Dispatcher:execute(self.data[name], exec_props)
end
2022-09-17 22:11:03 +00:00
function Profiles:editProfileName(editCallback, old_name)
local name_input
name_input = InputDialog:new{
title = _("Enter profile name"),
input = old_name,
buttons = {{
{
text = _("Cancel"),
id = "close",
2022-09-17 22:11:03 +00:00
callback = function()
UIManager:close(name_input)
end,
},
{
text = _("Save"),
callback = function()
local new_name = name_input:getInputText()
if new_name == "" or new_name == old_name then return end
UIManager:close(name_input)
if self.data[new_name] then
UIManager:show(InfoMessage:new{
text = T(_("Profile already exists: %1"), new_name),
})
else
editCallback(new_name)
end
end,
},
}},
}
UIManager:show(name_input)
name_input:onShowKeyboard()
end
function Profiles:getProfileFromCurrentDocument(new_name)
local document_settings
if self.ui.rolling then
document_settings = {
"rotation_mode",
"set_font",
"font_size",
"font_gamma",
"font_base_weight",
"font_hinting",
"font_kerning",
"word_spacing",
"word_expansion",
"visible_pages",
"h_page_margins",
"sync_t_b_page_margins",
"t_page_margin",
"b_page_margin",
"view_mode",
"block_rendering_mode",
"render_dpi",
"line_spacing",
"embedded_css",
"embedded_fonts",
"smooth_scaling",
"nightmode_images",
"status_line",
}
else
document_settings = {
"rotation_mode",
"kopt_text_wrap",
"kopt_trim_page",
"kopt_page_margin",
"kopt_zoom_overlap_h",
"kopt_zoom_overlap_v",
"kopt_max_columns",
"kopt_zoom_mode_genus",
"kopt_zoom_mode_type",
"kopt_zoom_factor",
"kopt_zoom_direction",
"kopt_page_scroll",
"kopt_line_spacing",
"kopt_font_size",
"kopt_contrast",
"kopt_quality",
}
end
local setting_needs_arg = {
["sync_t_b_page_margins"] = true,
["view_mode"] = true,
["embedded_css"] = true,
["embedded_fonts"] = true,
["smooth_scaling"] = true,
["nightmode_images"] = true,
["kopt_trim_page"] = true,
["kopt_zoom_mode_genus"] = true,
["kopt_zoom_mode_type"] = true,
["kopt_page_scroll"] = true,
}
local profile = { settings = { name = new_name, order = document_settings } }
for _, v in ipairs(document_settings) do
-- document configurable settings do not have prefixes
local value = self.document.configurable[v:gsub("^kopt_", "")]
if setting_needs_arg[v] then
value = Dispatcher:getArgFromValue(v, value)
end
profile[v] = value
end
if self.ui.rolling then
profile["set_font"] = self.ui.font.font_face -- not in configurable settings
end
return profile
end
function Profiles:updateGestures(action_old_name, action_new_name)
local gestures_path = FFIUtil.joinPath(DataStorage:getSettingsDir(), "gestures.lua")
local all_gestures = LuaSettings:open(gestures_path) -- in file
if not all_gestures then return end
local updated = false
for section, gestures in pairs(all_gestures.data) do -- custom_multiswipes, fm, reader sections
for gesture_name, gesture in pairs(gestures) do
if gesture[action_old_name] then
local gesture_loaded = self.ui.gestures.gestures[gesture_name] -- in memory
if gesture.settings and gesture.settings.order then
for i, action in ipairs(gesture.settings.order) do
if action == action_old_name then
if action_new_name then
gesture.settings.order[i] = action_new_name
gesture_loaded.settings.order[i] = action_new_name
else
table.remove(gesture.settings.order, i)
table.remove(gesture_loaded.settings.order, i)
if #gesture.settings.order == 0 then
gesture.settings.order = nil
if next(gesture.settings) == nil then
gesture.settings = nil
end
end
end
break
end
end
end
gesture[action_old_name] = nil
gesture_loaded[action_old_name] = nil
if action_new_name then
gesture[action_new_name] = true
gesture_loaded[action_new_name] = true
else
if next(gesture) == nil then
all_gestures.data[section][gesture_name] = nil
end
end
updated = true
end
end
end
if updated then
all_gestures:flush()
end
end
function Profiles:updateProfiles(action_old_name, action_new_name)
for _, profile in pairs(self.data) do
if profile[action_old_name] then
if profile.settings and profile.settings.order then
for i, action in ipairs(profile.settings.order) do
if action == action_old_name then
if action_new_name then
profile.settings.order[i] = action_new_name
else
table.remove(profile.settings.order, i)
if #profile.settings.order == 0 then
profile.settings.order = nil
end
end
break
end
end
end
profile[action_old_name] = nil
if action_new_name then
profile[action_new_name] = true
end
self.updated = true
end
end
end
function Profiles:updateAutostart(old_name, new_name)
2022-09-17 22:11:03 +00:00
if G_reader_settings:getSettingForExt("autostart_profiles", old_name) then
G_reader_settings:saveSettingForExt("autostart_profiles", nil, old_name)
if new_name then
G_reader_settings:saveSettingForExt("autostart_profiles", true, new_name)
end
end
end
function Profiles:executeAutostart()
if autostart_done then return end
self:loadProfiles()
local autostart_table = G_reader_settings:readSetting("autostart_profiles") or {}
for autostart_profile_name, profile_enabled in pairs(autostart_table) do
if self.data[autostart_profile_name] and profile_enabled then
UIManager:nextTick(function()
Dispatcher:execute(self.data[autostart_profile_name])
end)
else
self:updateAutostart(autostart_profile_name) -- remove deleted profile from autostart_profile
2022-09-17 22:11:03 +00:00
end
end
autostart_done = true
end
return Profiles