mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
1014 lines
41 KiB
Lua
1014 lines
41 KiB
Lua
local BD = require("ui/bidi")
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
local ButtonTable = require("ui/widget/buttontable")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
local CssTweaks = require("ui/data/css_tweaks")
|
|
local DataStorage = require("datastorage")
|
|
local Device = require("device")
|
|
local Dispatcher = require("dispatcher")
|
|
local Event = require("ui/event")
|
|
local Font = require("ui/font")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local MovableContainer = require("ui/widget/container/movablecontainer")
|
|
local Notification = require("ui/widget/notification")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
local UIManager = require("ui/uimanager")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
local logger = require("logger")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
local C_ = _.pgettext
|
|
local Screen = Device.screen
|
|
local T = require("ffi/util").template
|
|
|
|
-- Simple widget for showing tweak info
|
|
local TweakInfoWidget = InputContainer:extend{
|
|
tweak = nil,
|
|
is_global_default = nil,
|
|
toggle_global_default_callback = function() end,
|
|
modal = true,
|
|
width = math.floor(Screen:getWidth() * 0.75),
|
|
}
|
|
|
|
function TweakInfoWidget:init()
|
|
local tweak = self.tweak
|
|
if Device:isTouchDevice() then
|
|
self.ges_events.Tap = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = Geom:new{
|
|
x = 0, y = 0,
|
|
w = Screen:getWidth(),
|
|
h = Screen:getHeight(),
|
|
}
|
|
}
|
|
}
|
|
end
|
|
self:registerKeyEvents()
|
|
|
|
local content = VerticalGroup:new{
|
|
TextBoxWidget:new{
|
|
text = tweak.title,
|
|
bold = true,
|
|
face = Font:getFace("infofont"),
|
|
width = self.width,
|
|
},
|
|
VerticalSpan:new{
|
|
width = Size.padding.large,
|
|
},
|
|
}
|
|
if tweak.description then
|
|
table.insert(content,
|
|
TextBoxWidget:new{
|
|
text = tweak.description,
|
|
face = Font:getFace("smallinfofont"),
|
|
width = self.width,
|
|
}
|
|
)
|
|
table.insert(content, VerticalSpan:new{
|
|
width = Size.padding.large,
|
|
})
|
|
end
|
|
|
|
-- This css TextBoxWidget may make the widget overflow screen with
|
|
-- large css text. For now, we don't bother with the complicated
|
|
-- setup of a scrollable ScrollTextWidget.
|
|
local css = tweak.css
|
|
if not css and tweak.css_path then
|
|
css = ""
|
|
local f = io.open(tweak.css_path, "r")
|
|
if f then
|
|
css = f:read("*all")
|
|
f:close()
|
|
end
|
|
end
|
|
self.css_text = util.trim(css)
|
|
self.css_frame = FrameContainer:new{
|
|
bordersize = Size.border.thin,
|
|
padding = Size.padding.large,
|
|
TextBoxWidget:new{
|
|
text = self.css_text,
|
|
face = Font:getFace("infont", 16),
|
|
width = self.width - 2*Size.padding.large,
|
|
para_direction_rtl = false, -- LTR
|
|
}
|
|
}
|
|
table.insert(content, self.css_frame)
|
|
|
|
if self.is_global_default then
|
|
table.insert(content, VerticalSpan:new{
|
|
width = Size.padding.large,
|
|
})
|
|
table.insert(content,
|
|
TextBoxWidget:new{
|
|
text = _("This tweak is applied on all books."),
|
|
face = Font:getFace("smallinfofont"),
|
|
width = self.width,
|
|
}
|
|
)
|
|
end
|
|
|
|
content = FrameContainer:new{
|
|
bordersize = 0,
|
|
padding = Size.padding.large,
|
|
padding_top = Size.padding.default,
|
|
content,
|
|
}
|
|
|
|
local buttons = {
|
|
{
|
|
{
|
|
text = self.is_tweak_in_dispatcher and _("Don't show in action list") or _("Show in action list"),
|
|
callback = function()
|
|
self.toggle_tweak_in_dispatcher_callback()
|
|
UIManager:close(self)
|
|
end,
|
|
},
|
|
},
|
|
{
|
|
{
|
|
text = _("Close"),
|
|
callback = function()
|
|
UIManager:close(self)
|
|
end,
|
|
},
|
|
{
|
|
text = self.is_global_default and _("Don't use on all books") or _("Use on all books"),
|
|
callback = function()
|
|
self.toggle_global_default_callback()
|
|
UIManager:close(self)
|
|
end,
|
|
},
|
|
},
|
|
}
|
|
|
|
local button_table = ButtonTable:new{
|
|
width = content:getSize().w,
|
|
buttons = buttons,
|
|
zero_sep = true,
|
|
show_parent = self,
|
|
}
|
|
|
|
self.movable = MovableContainer:new{
|
|
FrameContainer:new{
|
|
background = Blitbuffer.COLOR_WHITE,
|
|
radius = Size.radius.window,
|
|
margin = Size.margin.default,
|
|
padding = Size.padding.default,
|
|
padding_bottom = 0, -- no padding below buttontable
|
|
VerticalGroup:new{
|
|
align = "left",
|
|
content,
|
|
button_table,
|
|
}
|
|
}
|
|
}
|
|
self[1] = CenterContainer:new{
|
|
dimen = Screen:getSize(),
|
|
self.movable
|
|
}
|
|
end
|
|
|
|
function TweakInfoWidget:registerKeyEvents()
|
|
if Device:hasKeys() then
|
|
self.key_events.Close = { { Device.input.group.Back } }
|
|
end
|
|
end
|
|
|
|
TweakInfoWidget.onPhysicalKeyboardConnected = TweakInfoWidget.registerKeyEvents
|
|
|
|
function TweakInfoWidget:onShow()
|
|
UIManager:setDirty(self, function()
|
|
return "ui", self.movable.dimen
|
|
end)
|
|
end
|
|
|
|
function TweakInfoWidget:onCloseWidget()
|
|
UIManager:setDirty(nil, function()
|
|
return "ui", self.movable.dimen
|
|
end)
|
|
end
|
|
|
|
function TweakInfoWidget:onClose()
|
|
UIManager:close(self)
|
|
return true
|
|
end
|
|
|
|
function TweakInfoWidget:onTap(arg, ges)
|
|
if ges.pos:intersectWith(self.css_frame.dimen) and Device:hasClipboard() then
|
|
-- Tap inside CSS text copies it into clipboard (so it
|
|
-- can be pasted into the book-specific tweak editor)
|
|
-- (Add \n on both sides for easier pasting)
|
|
Device.input.setClipboardText("\n"..self.css_text.."\n")
|
|
UIManager:show(Notification:new{
|
|
text = _("CSS text copied to clipboard"),
|
|
})
|
|
return true
|
|
elseif ges.pos:notIntersectWith(self.movable.dimen) then
|
|
-- Tap outside closes widget
|
|
self:onClose()
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function TweakInfoWidget:onSelect()
|
|
if self.selected.x == 1 then
|
|
self:toggle_global_default_callback()
|
|
end
|
|
UIManager:close(self)
|
|
return true
|
|
end
|
|
|
|
-- Ordering function for tweaks when appened to css_test.
|
|
-- The order needs to be consistent for crengine's stylesheet change
|
|
-- detection code to not invalidate cache across loadings.
|
|
local function tweakOrdering(l, r)
|
|
if l.priority ~= r.priority then
|
|
-- lower priority first in the CSS text
|
|
return l.priority < r.priority
|
|
end
|
|
-- same priority: order by ids
|
|
return l.id < r.id
|
|
end
|
|
|
|
-- Reader component for managing tweaks. The aggregated css_text
|
|
-- is actually requested from us and applied by ReaderTypeset
|
|
local ReaderStyleTweak = WidgetContainer:extend{
|
|
tweaks_by_id = nil,
|
|
tweaks_table = nil, -- sub-menu items
|
|
nb_enabled_tweaks = 0, -- for use by main menu item
|
|
css_text = nil, -- aggregated css text from tweaks individual css snippets
|
|
enabled = true, -- allows for toggling between selected tweaks / none
|
|
dispatcher_prefix = "style_tweak_",
|
|
}
|
|
|
|
function ReaderStyleTweak:isTweakEnabled(tweak_id)
|
|
local g_enabled = false
|
|
local enabled = false
|
|
if self.global_tweaks[tweak_id] then
|
|
enabled = true
|
|
g_enabled = true
|
|
end
|
|
if self.doc_tweaks[tweak_id] == true then
|
|
enabled = true
|
|
elseif self.doc_tweaks[tweak_id] == false then
|
|
enabled = false
|
|
end
|
|
return enabled, g_enabled
|
|
end
|
|
|
|
function ReaderStyleTweak:nbTweaksEnabled(sub_item_table)
|
|
local nb_enabled = 0
|
|
local nb_found = 0
|
|
for _, item in ipairs(sub_item_table) do
|
|
if item.sub_item_table then
|
|
local sub_nb_enabled, sub_nb_found = self:nbTweaksEnabled(item.sub_item_table)
|
|
nb_enabled = nb_enabled + sub_nb_enabled
|
|
nb_found = nb_found + sub_nb_found
|
|
elseif item.tweak_id then
|
|
if self:isTweakEnabled(item.tweak_id) then
|
|
nb_enabled = nb_enabled + 1
|
|
end
|
|
nb_found = nb_found + 1
|
|
end
|
|
end
|
|
return nb_enabled, nb_found
|
|
end
|
|
|
|
function ReaderStyleTweak:resolveConflictsBeforeEnabling(id, conflicts_with)
|
|
-- conflicts_with may be a string, an array or hash table of ids, or a function:
|
|
-- make it a function for us here
|
|
local conflicts_with_type = type(conflicts_with)
|
|
local conflicts_with_func
|
|
if conflicts_with_type == "function" then
|
|
conflicts_with_func = conflicts_with
|
|
elseif conflicts_with_type == "string" then
|
|
conflicts_with_func = function(otid) return otid == conflicts_with end
|
|
elseif conflicts_with_type == "table" then
|
|
conflicts_with_func = function(otid) return conflicts_with[otid] ~= nil or util.arrayContains(conflicts_with, otid) end
|
|
else
|
|
conflicts_with_func = function(otid) return false end
|
|
end
|
|
local to_remove = {}
|
|
for other_id, other_enabled in pairs(self.doc_tweaks) do
|
|
-- We also reset the provided "id" for a complete cleanup,
|
|
-- it is expected the caller will re-enable it
|
|
if other_enabled and (other_id == id or conflicts_with_func(other_id)) then
|
|
table.insert(to_remove, other_id)
|
|
end
|
|
end
|
|
for _, other_id in ipairs(to_remove) do
|
|
self.doc_tweaks[other_id] = nil
|
|
end
|
|
-- global_tweaks may also contain some conflicting ids: we need to make them false
|
|
-- in doc_tweaks to have them disabled (but we keep them in global_tweaks)
|
|
local to_make_false = {}
|
|
for other_id, other_enabled in pairs(self.global_tweaks) do
|
|
-- (We shouldn't be called if the provided "id" is already enabled
|
|
-- in global_tweaks. So we don't check for that here.)
|
|
if other_enabled and conflicts_with_func(other_id) then
|
|
table.insert(to_make_false, other_id)
|
|
end
|
|
end
|
|
for _, other_id in ipairs(to_make_false) do
|
|
self.doc_tweaks[other_id] = false
|
|
end
|
|
end
|
|
|
|
function ReaderStyleTweak:resolveConflictsBeforeMakingDefault(id, conflicts_with)
|
|
local conflicts_with_type = type(conflicts_with)
|
|
local conflicts_with_func
|
|
if conflicts_with_type == "function" then
|
|
conflicts_with_func = conflicts_with
|
|
elseif conflicts_with_type == "string" then
|
|
conflicts_with_func = function(otid) return otid == conflicts_with end
|
|
elseif conflicts_with_type == "table" then
|
|
conflicts_with_func = function(otid) return conflicts_with[otid] ~= nil or util.arrayContains(conflicts_with, otid) end
|
|
else
|
|
conflicts_with_func = function(otid) return false end
|
|
end
|
|
local to_remove = {}
|
|
for other_id, other_enabled in pairs(self.global_tweaks) do
|
|
-- We also reset the provided "id" for a complete cleanup,
|
|
-- it is expected the caller will re-enable it
|
|
if other_id == id or conflicts_with_func(other_id) then
|
|
table.insert(to_remove, other_id)
|
|
end
|
|
end
|
|
for _, other_id in ipairs(to_remove) do
|
|
self.global_tweaks[other_id] = nil
|
|
end
|
|
-- Also remove the provided "id" and any conflicting one from doc_tweaks (where
|
|
-- they may be false and prevent this new default to apply to current book)
|
|
to_remove = {}
|
|
for other_id, other_enabled in pairs(self.doc_tweaks) do
|
|
if other_id == id or conflicts_with_func(other_id) then
|
|
table.insert(to_remove, other_id)
|
|
end
|
|
end
|
|
for _, other_id in ipairs(to_remove) do
|
|
self.doc_tweaks[other_id] = nil
|
|
end
|
|
end
|
|
|
|
-- Called by ReaderTypeset, returns the already built string
|
|
function ReaderStyleTweak:getCssText()
|
|
return self.css_text
|
|
end
|
|
|
|
-- Build css_text, and request ReaderTypeset to apply it if wanted
|
|
function ReaderStyleTweak:updateCssText(apply)
|
|
if self.enabled then
|
|
local tweaks = {}
|
|
for id, enabled in pairs(self.global_tweaks) do
|
|
-- there are only enabled tweaks in global_tweaks, but we don't
|
|
-- add them here if they appear in doc_tweaks (if enabled in
|
|
-- doc_tweaks, they'll be added below; if disabled, they should
|
|
-- not be added)
|
|
if self.doc_tweaks[id] == nil then
|
|
table.insert(tweaks, self.tweaks_by_id[id])
|
|
end
|
|
end
|
|
for id, enabled in pairs(self.doc_tweaks) do
|
|
-- there are enabled (true) and disabled (false) tweaks in doc_tweaks
|
|
if self.doc_tweaks[id] == true then
|
|
table.insert(tweaks, self.tweaks_by_id[id])
|
|
end
|
|
end
|
|
table.sort(tweaks, tweakOrdering)
|
|
self.nb_enabled_tweaks = 0
|
|
local css_snippets = {}
|
|
for _, tweak in ipairs(tweaks) do
|
|
self.nb_enabled_tweaks = self.nb_enabled_tweaks + 1
|
|
local css = tweak.css
|
|
if not css and tweak.css_path then
|
|
css = ""
|
|
local f = io.open(tweak.css_path, "r")
|
|
if f then
|
|
css = f:read("*all")
|
|
f:close()
|
|
end
|
|
-- We could store what's been read into tweak.css to avoid
|
|
-- re-reading it, but this will allow a user to experiment
|
|
-- wihout having to restart KOReader
|
|
end
|
|
css = util.trim(css)
|
|
table.insert(css_snippets, css)
|
|
end
|
|
if self.book_style_tweak and self.book_style_tweak_enabled then
|
|
self.nb_enabled_tweaks = self.nb_enabled_tweaks + 1
|
|
table.insert(css_snippets, self.book_style_tweak)
|
|
end
|
|
self.css_text = table.concat(css_snippets, "\n")
|
|
logger.dbg("made tweak css:\n".. self.css_text .. "[END]")
|
|
else
|
|
self.css_text = nil
|
|
logger.dbg("made no tweak css (Style tweaks disabled)")
|
|
end
|
|
if apply then
|
|
self.ui:handleEvent(Event:new("ApplyStyleSheet"))
|
|
end
|
|
end
|
|
|
|
function ReaderStyleTweak:onReadSettings(config)
|
|
self.enabled = config:nilOrTrue("style_tweaks_enabled")
|
|
self.doc_tweaks = config:readSetting("style_tweaks") or {}
|
|
-- Default globally enabled style tweaks (for new installations)
|
|
-- are defined in css_tweaks.lua
|
|
self.global_tweaks = G_reader_settings:readSetting("style_tweaks") or CssTweaks.DEFAULT_GLOBAL_STYLE_TWEAKS
|
|
self.book_style_tweak = config:readSetting("book_style_tweak") -- string or nil
|
|
self.book_style_tweak_enabled = config:readSetting("book_style_tweak_enabled")
|
|
self.book_style_tweak_last_edit_pos = config:readSetting("book_style_tweak_last_edit_pos")
|
|
self:updateCssText()
|
|
end
|
|
|
|
function ReaderStyleTweak:onSaveSettings()
|
|
if self.enabled then
|
|
self.ui.doc_settings:delSetting("style_tweaks_enabled")
|
|
else
|
|
self.ui.doc_settings:makeFalse("style_tweaks_enabled")
|
|
end
|
|
self.ui.doc_settings:saveSetting("style_tweaks", util.tableSize(self.doc_tweaks) > 0 and self.doc_tweaks or nil)
|
|
G_reader_settings:saveSetting("style_tweaks", self.global_tweaks)
|
|
G_reader_settings:saveSetting("style_tweaks_in_dispatcher", self.tweaks_in_dispatcher)
|
|
self.ui.doc_settings:saveSetting("book_style_tweak", self.book_style_tweak)
|
|
self.ui.doc_settings:saveSetting("book_style_tweak_enabled", self.book_style_tweak_enabled)
|
|
self.ui.doc_settings:saveSetting("book_style_tweak_last_edit_pos", self.book_style_tweak_last_edit_pos)
|
|
end
|
|
|
|
local function dispatcherRegisterStyleTweak(tweak_id, tweak_title)
|
|
Dispatcher:registerAction(ReaderStyleTweak.dispatcher_prefix..tweak_id,
|
|
{category="none", event="ToggleStyleTweak", arg=tweak_id, title=T(_("Toggle style tweak: %1"), tweak_title), rolling=true})
|
|
end
|
|
|
|
local function dispatcherUnregisterStyleTweak(tweak_id)
|
|
Dispatcher:removeAction(ReaderStyleTweak.dispatcher_prefix..tweak_id)
|
|
end
|
|
|
|
function ReaderStyleTweak:init()
|
|
self.tweaks_in_dispatcher = G_reader_settings:readSetting("style_tweaks_in_dispatcher") or {}
|
|
self.tweaks_by_id = {}
|
|
self.tweaks_table = {}
|
|
|
|
-- Add first item of sub-menu, that allows toggling between
|
|
-- enabled tweaks / none (without the need to disable each of
|
|
-- them)
|
|
table.insert(self.tweaks_table, {
|
|
text = _("Enable style tweaks (long-press for help)"),
|
|
checked_func = function() return self.enabled end,
|
|
callback = function()
|
|
self.enabled = not self.enabled
|
|
self:updateCssText(true) -- apply it immediately
|
|
end,
|
|
hold_callback = function()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _([[
|
|
Style tweaks allow changing small parts of book styles (including the publisher/embedded styles) to make visual adjustments or disable unwanted publisher layout choices.
|
|
|
|
Some tweaks may be useful with some books, while resulting in undesirable effects with others.
|
|
|
|
You can enable individual tweaks on this book with a tap, or view more details about a tweak and enable it on all books with hold.]])
|
|
})
|
|
end,
|
|
separator = true,
|
|
})
|
|
|
|
-- Single function for use as enabled_func
|
|
local is_enabled = function() return self.enabled end
|
|
|
|
-- Generic function to recursively build a sub_item_table (as expected
|
|
-- by TouchMenu) from a table of tweak definitions (like CssTweaks from
|
|
-- css_tweaks.lua, or like the one we build from user styletweaks
|
|
-- directory files and sub-directories)
|
|
local addTweakMenuItem
|
|
addTweakMenuItem = function(menu, item, max_per_page)
|
|
if type(item) == "table" and #item > 0 then -- sub-menu
|
|
local sub_item_table = {}
|
|
sub_item_table.max_per_page = max_per_page
|
|
for _, it in ipairs(item) do
|
|
addTweakMenuItem(sub_item_table, it, max_per_page) -- recurse
|
|
end
|
|
table.insert(menu, {
|
|
text_func = function()
|
|
local text = item.title or "### undefined submenu title ###"
|
|
local nb_enabled, nb_found = self:nbTweaksEnabled(sub_item_table) -- luacheck: no unused
|
|
-- We could add nb_enabled/nb_found, but that makes for
|
|
-- a busy/ugly menu
|
|
-- text = string.format("%s (%d/%d)", text, nb_enabled, nb_found)
|
|
if nb_enabled > 0 then
|
|
text = string.format("%s (%d)", text, nb_enabled)
|
|
end
|
|
return text
|
|
end,
|
|
enabled_func = is_enabled,
|
|
sub_item_table = sub_item_table,
|
|
separator = item.separator,
|
|
})
|
|
elseif item.id then -- tweak menu item
|
|
-- Set a default priority of 0 if item doesn't have one
|
|
if not item.priority then item.priority = 0 end
|
|
self.tweaks_by_id[item.id] = item
|
|
table.insert(menu, {
|
|
tweak_id = item.id,
|
|
enabled_func = is_enabled,
|
|
checked_func = function() return self:isTweakEnabled(item.id) end,
|
|
text_func = function()
|
|
local title = item.title or "### undefined tweak title ###"
|
|
if self.global_tweaks[item.id] then
|
|
title = title .. " ★"
|
|
end
|
|
if self.tweaks_in_dispatcher[item.id] then
|
|
title = title .. " \u{F144}"
|
|
end
|
|
return title
|
|
end,
|
|
hold_callback = function(touchmenu_instance)
|
|
UIManager:show(TweakInfoWidget:new{
|
|
tweak = item,
|
|
is_global_default = self.global_tweaks[item.id],
|
|
toggle_global_default_callback = function()
|
|
if self.global_tweaks[item.id] then
|
|
self.global_tweaks[item.id] = nil
|
|
if self.doc_tweaks[item.id] == false then
|
|
self.doc_tweaks[item.id] = nil
|
|
end
|
|
else
|
|
if item.conflicts_with and item.global_conflicts_with ~= false then
|
|
-- For hold/makeDefault/global_tweaks, the tweak may provide 'global_conflicts_with':
|
|
-- if 'false': no conflict checks
|
|
-- if nil or 'true', use item.conflicts_with
|
|
-- otherwise, use it instead of item.conflicts_with
|
|
if item.global_conflicts_with ~= true and item.global_conflicts_with ~= nil then
|
|
self:resolveConflictsBeforeMakingDefault(item.id, item.global_conflicts_with)
|
|
else
|
|
self:resolveConflictsBeforeMakingDefault(item.id, item.conflicts_with)
|
|
end
|
|
-- Remove all references in doc_tweak
|
|
self:resolveConflictsBeforeEnabling(item.id, item.conflicts_with)
|
|
self.doc_tweaks[item.id] = nil
|
|
end
|
|
self.global_tweaks[item.id] = true
|
|
end
|
|
touchmenu_instance:updateItems()
|
|
self:updateCssText(true) -- apply it immediately
|
|
end,
|
|
is_tweak_in_dispatcher = self.tweaks_in_dispatcher[item.id],
|
|
toggle_tweak_in_dispatcher_callback = function()
|
|
if self.tweaks_in_dispatcher[item.id] then
|
|
self.tweaks_in_dispatcher[item.id] = nil
|
|
dispatcherUnregisterStyleTweak(item.id)
|
|
if self.ui.profiles then
|
|
self.ui.profiles:updateProfiles(self.dispatcher_prefix..item.id)
|
|
end
|
|
else
|
|
self.tweaks_in_dispatcher[item.id] = item.title
|
|
dispatcherRegisterStyleTweak(item.id, item.title)
|
|
end
|
|
touchmenu_instance:updateItems()
|
|
end,
|
|
})
|
|
end,
|
|
callback = function()
|
|
-- enable/disable only for this book
|
|
self:onToggleStyleTweak(item.id, item, true) -- no notification
|
|
end,
|
|
separator = item.separator,
|
|
})
|
|
elseif item.info_text then -- informative menu item
|
|
table.insert(menu, {
|
|
text = item.title or "### undefined menu title ###",
|
|
-- No check box.
|
|
-- Show the info text when either tap or hold
|
|
keep_menu_open = true,
|
|
callback = function()
|
|
UIManager:show(InfoMessage:new{
|
|
text = item.info_text,
|
|
})
|
|
end,
|
|
hold_callback = function()
|
|
UIManager:show(InfoMessage:new{
|
|
text = item.info_text,
|
|
})
|
|
end,
|
|
separator = item.separator,
|
|
})
|
|
else
|
|
table.insert(menu, {
|
|
text = item.if_empty_menu_title or _("This section is empty"),
|
|
enabled = false,
|
|
})
|
|
end
|
|
end
|
|
|
|
-- Add each of CssTweaks' top-level items as a sub-menu
|
|
for _, item in ipairs(CssTweaks) do
|
|
addTweakMenuItem(self.tweaks_table, item)
|
|
end
|
|
|
|
-- Users can put their own style tweaks as individual .css files into
|
|
-- koreader/styletweaks/ directory. These can be organized into
|
|
-- sub-directories that will show up as sub-menus.
|
|
local user_styletweaks_dir = DataStorage:getDataDir() .. "/styletweaks"
|
|
local user_tweaks_table = { title = _("User style tweaks") }
|
|
|
|
-- Build a tweak definition table from the content of a directory
|
|
local process_tweaks_dir
|
|
process_tweaks_dir = function(dir, item_table, if_empty_menu_title)
|
|
local file_list = {}
|
|
local dir_list = {}
|
|
if lfs.attributes(dir, "mode") == "directory" then
|
|
for f in lfs.dir(dir) do
|
|
if f ~= "." and f ~= ".." then
|
|
local mode = lfs.attributes(dir.."/"..f, "mode")
|
|
if mode == "directory" then
|
|
table.insert(dir_list, f)
|
|
elseif mode == "file" and string.match(f, "%.css$") then
|
|
table.insert(file_list, f)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
table.sort(dir_list)
|
|
table.sort(file_list)
|
|
for __, subdir in ipairs(dir_list) do
|
|
local sub_item_table = { title = subdir:gsub("_", " ") }
|
|
process_tweaks_dir(dir.."/"..subdir, sub_item_table)
|
|
table.insert(item_table, sub_item_table)
|
|
end
|
|
for __, file in ipairs(file_list) do
|
|
local title = file:gsub("%.css$", ""):gsub("_", " ")
|
|
local filepath = dir.."/"..file
|
|
table.insert(item_table, {
|
|
title = title,
|
|
id = file, -- keep ".css" in id, to distinguish between koreader/user tweaks
|
|
description = T(_("User style tweak at %1"), BD.filepath(filepath)),
|
|
priority = 10, -- give user tweaks a higher priority
|
|
css_path = filepath,
|
|
})
|
|
end
|
|
if #item_table == 0 then
|
|
table.insert(item_table, {
|
|
if_empty_menu_title = if_empty_menu_title or _("No CSS tweak found in this directory"),
|
|
})
|
|
end
|
|
end
|
|
local if_empty_menu_title = _("Add your own tweaks in koreader/styletweaks/")
|
|
process_tweaks_dir(user_styletweaks_dir, user_tweaks_table, if_empty_menu_title)
|
|
self.tweaks_table[#self.tweaks_table].separator = true
|
|
addTweakMenuItem(self.tweaks_table, user_tweaks_table, 6)
|
|
-- limit to 6 user tweaks per page
|
|
|
|
-- Book-specific editable tweak
|
|
self.tweaks_table[#self.tweaks_table].separator = true
|
|
local book_tweak_item = {
|
|
text_func = function()
|
|
if self.book_style_tweak then
|
|
return _("Book-specific tweak (long-press to edit)")
|
|
else
|
|
return _("Book-specific tweak")
|
|
end
|
|
end,
|
|
enabled_func = function() return self.enabled end,
|
|
checked_func = function() return self.book_style_tweak_enabled end,
|
|
callback = function(touchmenu_instance)
|
|
if self.book_style_tweak then
|
|
-- There is a tweak: toggle it on tap, like other tweaks
|
|
self.book_style_tweak_enabled = not self.book_style_tweak_enabled
|
|
self:updateCssText(true) -- apply it immediately
|
|
else
|
|
-- No tweak defined: launch editor
|
|
self:editBookTweak(touchmenu_instance)
|
|
end
|
|
end,
|
|
hold_callback = function(touchmenu_instance)
|
|
self:editBookTweak(touchmenu_instance)
|
|
end,
|
|
}
|
|
table.insert(self.tweaks_table, book_tweak_item)
|
|
|
|
self.ui.menu:registerToMainMenu(self)
|
|
self:onDispatcherRegisterActions()
|
|
end
|
|
|
|
function ReaderStyleTweak:addToMainMenu(menu_items)
|
|
-- insert table to main reader menu
|
|
menu_items.style_tweaks = {
|
|
text_func = function()
|
|
if self.enabled and self.nb_enabled_tweaks > 0 then
|
|
return T(_("Style tweaks (%1)"), self.nb_enabled_tweaks)
|
|
else
|
|
return _("Style tweaks")
|
|
end
|
|
end,
|
|
sub_item_table = self.tweaks_table,
|
|
}
|
|
end
|
|
|
|
function ReaderStyleTweak:onToggleStyleTweak(tweak_id, item, no_notification)
|
|
local text
|
|
local enabled, g_enabled = self:isTweakEnabled(tweak_id)
|
|
if enabled then
|
|
if g_enabled then
|
|
-- if globaly enabled, mark it as disabled
|
|
-- for this document only
|
|
self.doc_tweaks[tweak_id] = false
|
|
else
|
|
self.doc_tweaks[tweak_id] = nil
|
|
end
|
|
text = T(C_("Style tweak", "Off: %1"), self.tweaks_in_dispatcher[tweak_id])
|
|
else
|
|
local conflicts_with
|
|
if item then
|
|
conflicts_with = item.conflicts_with
|
|
else -- called from Dispatcher
|
|
for _, v in ipairs(CssTweaks) do
|
|
if v.id == tweak_id then
|
|
conflicts_with = v.conflicts_with
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if conflicts_with then
|
|
self:resolveConflictsBeforeEnabling(tweak_id, conflicts_with)
|
|
end
|
|
self.doc_tweaks[tweak_id] = true
|
|
text = T(C_("Style tweak", "On: %1"), self.tweaks_in_dispatcher[tweak_id])
|
|
end
|
|
self:updateCssText(true) -- apply it immediately
|
|
if not no_notification then
|
|
UIManager:show(Notification:new{
|
|
text = text,
|
|
})
|
|
end
|
|
end
|
|
|
|
function ReaderStyleTweak:onDispatcherRegisterActions()
|
|
for tweak_id, tweak_title in pairs(self.tweaks_in_dispatcher) do
|
|
dispatcherRegisterStyleTweak(tweak_id, tweak_title)
|
|
end
|
|
end
|
|
|
|
local BOOK_TWEAK_SAMPLE_CSS = [[
|
|
/* Remove indent from some P used as titles */
|
|
p.someTitleClassName {
|
|
text-indent: 0;
|
|
}
|
|
/* Get in-page footnotes when no tweak works */
|
|
.footnoteContainerClassName {
|
|
-cr-hint: footnote-inpage;
|
|
}
|
|
/* Help getting some alternative ToC when no headings */
|
|
.someSeparatorClassName {
|
|
-cr-hint: toc-level1;
|
|
break-before: always;
|
|
}
|
|
/* Hide annoying content */
|
|
DIV.someAdvertisement {
|
|
display: none !important;
|
|
}
|
|
]]
|
|
|
|
local BOOK_TWEAK_INPUT_HINT = T([[
|
|
/* %1 */
|
|
|
|
%2]], _("You can add CSS snippets which will be applied only to this book."), BOOK_TWEAK_SAMPLE_CSS)
|
|
|
|
local CSS_SUGGESTIONS = {
|
|
{ "-cr-hint: footnote-inpage;", _("When set on a block element containing the target id of a href, this block element will be shown as an in-page footnote.")},
|
|
{ "-cr-hint: non-linear-combining;", _("Can be set on some specific DocFragments (ie. DocFragment[id*=16]) to ignore them in the linear pages flow.")},
|
|
{ "-cr-hint: toc-level1;", _("When set on an element, its text can be used to build the alternative table of contents.")},
|
|
{ "display: run-in !important;", _("When set on a block element, this element content will be inlined with the next block element.")},
|
|
{ "font-size: 1rem !important;", _("1rem will enforce your main font size.")},
|
|
{ "hyphens: none !important;", _("Disables hyphenation inside the targeted elements.")},
|
|
{ "text-indent: 1.2em !important;", _("1.2em is our default text indentation.")},
|
|
}
|
|
|
|
function ReaderStyleTweak:editBookTweak(touchmenu_instance)
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local editor -- our InputDialog instance
|
|
local tweak_button_id = "editBookTweakButton"
|
|
-- We add a button on the left, which can have 3 states/labels:
|
|
local BUTTON_USE_SAMPLE = _("Use sample")
|
|
local BUTTON_PRETTIFY = _("Prettify")
|
|
local BUTTON_CONDENSE = _("Condense")
|
|
-- Initial button state differs whether we already have some CSS content
|
|
local tweak_button_state = self.book_style_tweak and BUTTON_PRETTIFY or BUTTON_USE_SAMPLE
|
|
local toggle_tweak_button = function(state)
|
|
if state then -- use provided state
|
|
tweak_button_state = state
|
|
else -- natural toggling
|
|
if tweak_button_state == BUTTON_USE_SAMPLE then
|
|
tweak_button_state = BUTTON_PRETTIFY
|
|
elseif tweak_button_state == BUTTON_PRETTIFY then
|
|
tweak_button_state = BUTTON_CONDENSE
|
|
elseif tweak_button_state == BUTTON_CONDENSE then
|
|
tweak_button_state = BUTTON_PRETTIFY
|
|
end
|
|
end
|
|
local tweak_button = editor.button_table:getButtonById(tweak_button_id)
|
|
tweak_button:init()
|
|
editor:refreshButtons()
|
|
end
|
|
-- The Save and Close buttons default behaviour, how they trigger
|
|
-- the callbacks and how they show or not a notification, is not
|
|
-- the most convenient here. We try to tweak that a bit.
|
|
local SAVE_BUTTON_LABEL
|
|
if self.book_style_tweak_enabled or not self.book_style_tweak then
|
|
SAVE_BUTTON_LABEL = _("Apply")
|
|
else
|
|
SAVE_BUTTON_LABEL = _("Save")
|
|
end
|
|
-- This message might be shown by multiple notifications at the
|
|
-- same time: having it similar will make that unnoticed.
|
|
local NOT_MODIFIED_MSG = _("Book tweak not modified.")
|
|
editor = InputDialog:new{
|
|
title = _("Edit book-specific style tweak"),
|
|
input = self.book_style_tweak or "",
|
|
input_hint = BOOK_TWEAK_INPUT_HINT,
|
|
input_face = Font:getFace("infont", 16), -- same as in TweakInfoWidget
|
|
para_direction_rtl = false,
|
|
lang = "en",
|
|
fullscreen = true,
|
|
condensed = true,
|
|
allow_newline = true,
|
|
cursor_at_end = false,
|
|
add_nav_bar = true,
|
|
scroll_by_pan = true,
|
|
buttons = {{
|
|
-- First buttons on first row (row will be completed with Reset|Save|Close)
|
|
{
|
|
id = tweak_button_id,
|
|
text_func = function()
|
|
return tweak_button_state -- usable as a label
|
|
end,
|
|
callback = function()
|
|
if tweak_button_state == BUTTON_USE_SAMPLE then
|
|
editor:setInputText(BOOK_TWEAK_SAMPLE_CSS, true)
|
|
-- will have edited_callback() called, which will do toggle_tweak_button()
|
|
else
|
|
local css_text = editor:getInputText()
|
|
css_text = util.prettifyCSS(css_text, tweak_button_state == BUTTON_CONDENSE)
|
|
editor:setInputText(css_text, true)
|
|
toggle_tweak_button()
|
|
end
|
|
end,
|
|
},
|
|
{
|
|
id = "css_suggestions_button_id",
|
|
text = "CSS \u{2261}",
|
|
callback = function()
|
|
local suggestions_popup_widget
|
|
local buttons = {}
|
|
for _, suggestion in ipairs(CSS_SUGGESTIONS) do
|
|
table.insert(buttons, {{
|
|
text = suggestion[1],
|
|
align = "left",
|
|
callback = function()
|
|
UIManager:close(suggestions_popup_widget)
|
|
editor._input_widget:addChars(suggestion[1])
|
|
end,
|
|
hold_callback = suggestion[2] and function()
|
|
UIManager:show(InfoMessage:new{ text = suggestion[2] })
|
|
end or nil
|
|
}})
|
|
end
|
|
local ButtonDialog = require("ui/widget/buttondialog")
|
|
suggestions_popup_widget = ButtonDialog:new{
|
|
modal = true, -- needed when keyboard is shown
|
|
width = math.floor(Screen:getWidth() * 0.9), -- max width, will get smaller
|
|
shrink_unneeded_width = true,
|
|
buttons = buttons,
|
|
anchor = function()
|
|
-- we return prefers_pop_down=true so it pops over the keyboard
|
|
-- instead of the text if it can
|
|
return editor.button_table:getButtonById("css_suggestions_button_id").dimen, true
|
|
end,
|
|
}
|
|
UIManager:show(suggestions_popup_widget)
|
|
end,
|
|
},
|
|
}},
|
|
edited_callback = function()
|
|
if not editor then
|
|
-- We might be called while the InputDialog is being
|
|
-- initialized (so not yet assigned to 'editor')
|
|
return
|
|
end
|
|
if #editor:getInputText() == 0 then
|
|
-- No content: show "Use sample"
|
|
if tweak_button_state ~= BUTTON_USE_SAMPLE then
|
|
toggle_tweak_button(BUTTON_USE_SAMPLE)
|
|
end
|
|
else
|
|
-- Some content: get rid of "Use sample" to not risk
|
|
-- overriding content
|
|
if tweak_button_state == BUTTON_USE_SAMPLE then
|
|
toggle_tweak_button()
|
|
end
|
|
end
|
|
end,
|
|
-- Store/retrieve view and cursor position callback
|
|
view_pos_callback = function(top_line_num, charpos)
|
|
-- This same callback is called with no arguments on init to retrieve the stored initial position,
|
|
-- and with arguments to store the final position on close.
|
|
if top_line_num and charpos then
|
|
self.book_style_tweak_last_edit_pos = {top_line_num, charpos}
|
|
else
|
|
local prev_pos = self.book_style_tweak_last_edit_pos
|
|
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,
|
|
reset_button_text = _("Restore"),
|
|
reset_callback = function(content) -- Will add a Reset button
|
|
return self.book_style_tweak or "", _("Book tweak restored")
|
|
end,
|
|
save_button_text = SAVE_BUTTON_LABEL,
|
|
close_save_button_text = SAVE_BUTTON_LABEL,
|
|
save_callback = function(content, closing) -- Will add Save/Close buttons
|
|
if content and content == "" then
|
|
content = nil -- we store nil when empty
|
|
end
|
|
local was_empty = self.book_style_tweak == nil
|
|
local is_empty = content == nil
|
|
local tweak_updated = content ~= self.book_style_tweak
|
|
local should_apply = false
|
|
local msg -- returned and shown as a notification by InputDialog
|
|
if was_empty and not is_empty then
|
|
-- Tweak was empty, and so just created: enable book tweak
|
|
-- so it's immediately applied, and checked in the menu
|
|
self.book_style_tweak_enabled = true
|
|
should_apply = true
|
|
msg = _("Book tweak created, applying…")
|
|
elseif is_empty then
|
|
if not was_empty and self.book_style_tweak_enabled then
|
|
-- Tweak was enabled, but has been emptied: make it
|
|
-- disabled in the menu, but apply CSS without it
|
|
should_apply = true
|
|
msg = _("Book tweak removed, rendering…")
|
|
else
|
|
msg = _("Book tweak emptied and removed.")
|
|
end
|
|
self.book_style_tweak_enabled = false
|
|
elseif tweak_updated then
|
|
if self.book_style_tweak_enabled then
|
|
should_apply = true
|
|
msg = _("Book tweak updated, applying…")
|
|
else
|
|
msg = _("Book tweak saved (not enabled).")
|
|
end
|
|
else
|
|
msg = NOT_MODIFIED_MSG
|
|
end
|
|
self.book_style_tweak = content
|
|
-- We always close the editor when this callback is called.
|
|
-- If closing=true, InputDialog will call close_callback().
|
|
-- If not, let's do it ourselves.
|
|
if not closing then
|
|
UIManager:close(editor)
|
|
end
|
|
if should_apply then
|
|
-- Let menu be closed and previous page be refreshed,
|
|
-- so one can see how the text is changed by the tweak.
|
|
touchmenu_instance:closeMenu()
|
|
UIManager:scheduleIn(0.2, function()
|
|
self:updateCssText(true) -- have it applied
|
|
end)
|
|
else
|
|
touchmenu_instance:updateItems()
|
|
end
|
|
editor.save_callback_called = true
|
|
return true, msg
|
|
end,
|
|
close_callback = function()
|
|
-- save_callback() will always have shown some notification,
|
|
-- so don't add another one
|
|
if not editor.save_callback_called then
|
|
UIManager:show(Notification:new{
|
|
text = NOT_MODIFIED_MSG,
|
|
})
|
|
-- This has to be the same message above and below: when
|
|
-- discarding, we can't prevent these 2 notifications from
|
|
-- being shown: having them identical will hide that.
|
|
end
|
|
end,
|
|
close_discarded_notif_text = NOT_MODIFIED_MSG,
|
|
}
|
|
UIManager:show(editor)
|
|
editor:onShowKeyboard(true)
|
|
-- ignore first hold release, as we may be invoked from hold
|
|
end
|
|
|
|
return ReaderStyleTweak
|