2020-01-04 00:18:51 +00:00
local BD = require ( " ui/bidi " )
2018-05-12 21:24:43 +00:00
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 " )
2022-11-22 12:46:44 +00:00
local Dispatcher = require ( " dispatcher " )
2018-05-12 21:24:43 +00:00
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 " )
2020-06-08 18:47:31 +00:00
local Notification = require ( " ui/widget/notification " )
2018-05-12 21:24:43 +00:00
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 " )
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 WidgetContainer = require ( " ui/widget/container/widgetcontainer " )
2018-05-12 21:24:43 +00:00
local lfs = require ( " libs/libkoreader-lfs " )
local logger = require ( " logger " )
local util = require ( " util " )
local _ = require ( " gettext " )
local Screen = Device.screen
local T = require ( " ffi/util " ) . template
-- Simple widget for showing tweak info
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 TweakInfoWidget = InputContainer : extend {
2018-05-12 21:24:43 +00:00
tweak = nil ,
is_global_default = nil ,
toggle_global_default_callback = function ( ) end ,
modal = true ,
2020-06-14 00:21:41 +00:00
width = math.floor ( Screen : getWidth ( ) * 0.75 ) ,
2018-05-12 21:24:43 +00:00
}
function TweakInfoWidget : init ( )
local tweak = self.tweak
if Device : isTouchDevice ( ) then
2020-06-08 18:47:31 +00:00
self.ges_events . Tap = {
2018-05-12 21:24:43 +00:00
GestureRange : new {
ges = " tap " ,
range = Geom : new {
x = 0 , y = 0 ,
w = Screen : getWidth ( ) ,
h = Screen : getHeight ( ) ,
}
}
}
end
2022-11-01 22:22:07 +00:00
self : registerKeyEvents ( )
2018-05-12 21:24:43 +00:00
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
2020-09-30 17:56:56 +00:00
self.css_text = util.trim ( css )
2020-06-08 18:47:31 +00:00
self.css_frame = FrameContainer : new {
2018-05-12 21:24:43 +00:00
bordersize = Size.border . thin ,
padding = Size.padding . large ,
TextBoxWidget : new {
2020-06-08 18:47:31 +00:00
text = self.css_text ,
2018-05-12 21:24:43 +00:00
face = Font : getFace ( " infont " , 16 ) ,
width = self.width - 2 * Size.padding . large ,
2019-12-06 21:55:35 +00:00
para_direction_rtl = false , -- LTR
2018-05-12 21:24:43 +00:00
}
2020-06-08 18:47:31 +00:00
}
table.insert ( content , self.css_frame )
2018-05-12 21:24:43 +00:00
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 = {
{
2022-11-22 12:46:44 +00:00
{
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 ,
} ,
2018-05-12 21:24:43 +00:00
} ,
{
2022-11-22 12:46:44 +00:00
{
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 ,
} ,
2018-05-12 21:24:43 +00:00
} ,
}
local button_table = ButtonTable : new {
width = content : getSize ( ) . w ,
button_font_face = " cfont " ,
button_font_size = 20 ,
2022-11-22 12:46:44 +00:00
buttons = buttons ,
2018-05-12 21:24:43 +00:00
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
2022-11-01 22:22:07 +00:00
function TweakInfoWidget : registerKeyEvents ( )
2022-10-31 23:17:25 +00:00
if Device : hasKeys ( ) then
self.key_events . Close = { { Device.input . group.Back } }
end
end
TweakInfoWidget.onPhysicalKeyboardConnected = TweakInfoWidget.registerKeyEvents
2018-05-12 21:24:43 +00:00
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
2020-06-08 18:47:31 +00:00
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
2018-05-12 21:24:43 +00:00
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
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 ReaderStyleTweak = WidgetContainer : extend {
2018-05-12 21:24:43 +00:00
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
2023-03-26 18:10:39 +00:00
dispatcher_prefix = " style_tweak_ " ,
2018-05-12 21:24:43 +00:00
}
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
2022-07-06 21:09:18 +00:00
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,
2022-07-08 15:22:35 +00:00
-- it is expected the caller will re-enable it
2022-07-06 21:09:18 +00:00
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
2022-07-08 15:22:35 +00:00
-- in doc_tweaks to have them disabled (but we keep them in global_tweaks)
2022-07-06 21:09:18 +00:00
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,
2022-07-08 15:22:35 +00:00
-- it is expected the caller will re-enable it
2022-07-06 21:09:18 +00:00
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
2018-05-12 21:24:43 +00:00
-- 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
2018-05-20 23:19:36 +00:00
-- 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
2018-05-12 21:24:43 +00:00
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
2020-09-30 17:56:56 +00:00
css = util.trim ( css )
2018-05-12 21:24:43 +00:00
table.insert ( css_snippets , css )
end
2020-06-08 18:47:31 +00:00
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
2018-05-12 21:24:43 +00:00
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 )
2021-03-06 21:44:18 +00:00
self.enabled = config : nilOrTrue ( " style_tweaks_enabled " )
2018-05-12 21:24:43 +00:00
self.doc_tweaks = config : readSetting ( " style_tweaks " ) or { }
2020-02-28 22:06:58 +00:00
-- 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
2020-06-08 18:47:31 +00:00
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 " )
2018-05-12 21:24:43 +00:00
self : updateCssText ( )
end
function ReaderStyleTweak : onSaveSettings ( )
if self.enabled then
self.ui . doc_settings : delSetting ( " style_tweaks_enabled " )
else
2021-03-06 21:44:18 +00:00
self.ui . doc_settings : makeFalse ( " style_tweaks_enabled " )
2018-05-12 21:24:43 +00:00
end
self.ui . doc_settings : saveSetting ( " style_tweaks " , util.tableSize ( self.doc_tweaks ) > 0 and self.doc_tweaks or nil )
2020-02-28 22:06:58 +00:00
G_reader_settings : saveSetting ( " style_tweaks " , self.global_tweaks )
2022-11-22 12:46:44 +00:00
G_reader_settings : saveSetting ( " style_tweaks_in_dispatcher " , self.tweaks_in_dispatcher )
2020-06-08 18:47:31 +00:00
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 )
2018-05-12 21:24:43 +00:00
end
2022-11-22 12:46:44 +00:00
local function dispatcherRegisterStyleTweak ( tweak_id , tweak_title )
2023-03-26 18:10:39 +00:00
Dispatcher : registerAction ( ReaderStyleTweak.dispatcher_prefix .. tweak_id ,
2022-11-22 12:46:44 +00:00
{ category = " none " , event = " ToggleStyleTweak " , arg = tweak_id , title = T ( _ ( " Toggle style tweak: %1 " ) , tweak_title ) , rolling = true } )
end
local function dispatcherUnregisterStyleTweak ( tweak_id )
2023-03-26 18:10:39 +00:00
Dispatcher : removeAction ( ReaderStyleTweak.dispatcher_prefix .. tweak_id )
2022-11-22 12:46:44 +00:00
end
2018-05-12 21:24:43 +00:00
function ReaderStyleTweak : init ( )
2022-11-22 12:46:44 +00:00
self.tweaks_in_dispatcher = G_reader_settings : readSetting ( " style_tweaks_in_dispatcher " ) or { }
2018-05-12 21:24:43 +00:00
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 , {
2021-09-06 15:08:48 +00:00
text = _ ( " Enable style tweaks (long-press for help) " ) ,
2018-05-12 21:24:43 +00:00
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 {
2019-08-22 15:11:47 +00:00
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 .
2018-05-12 21:24:43 +00:00
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
2020-06-08 18:47:31 +00:00
addTweakMenuItem = function ( menu , item , max_per_page )
2018-05-12 21:24:43 +00:00
if type ( item ) == " table " and # item > 0 then -- sub-menu
local sub_item_table = { }
2020-06-08 18:47:31 +00:00
sub_item_table.max_per_page = max_per_page
2018-05-12 21:24:43 +00:00
for _ , it in ipairs ( item ) do
2020-06-08 18:47:31 +00:00
addTweakMenuItem ( sub_item_table , it , max_per_page ) -- recurse
2018-05-12 21:24:43 +00:00
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 ,
2018-05-24 22:22:08 +00:00
separator = item.separator ,
2018-05-12 21:24:43 +00:00
} )
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
2022-11-22 12:46:44 +00:00
if self.tweaks_in_dispatcher [ item.id ] then
title = title .. " \u{F144} "
end
2018-05-12 21:24:43 +00:00
return title
end ,
2018-09-04 21:55:58 +00:00
hold_callback = function ( touchmenu_instance )
2018-05-12 21:24:43 +00:00
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
2022-07-06 21:09:18 +00:00
if self.doc_tweaks [ item.id ] == false then
self.doc_tweaks [ item.id ] = nil
end
2018-05-12 21:24:43 +00:00
else
2022-07-06 21:09:18 +00:00
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
2022-07-08 15:22:35 +00:00
-- otherwise, use it instead of item.conflicts_with
if item.global_conflicts_with ~= true and item.global_conflicts_with ~= nil then
2022-07-06 21:09:18 +00:00
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
2018-05-12 21:24:43 +00:00
self.global_tweaks [ item.id ] = true
end
2018-09-04 21:55:58 +00:00
touchmenu_instance : updateItems ( )
2018-05-12 21:24:43 +00:00
self : updateCssText ( true ) -- apply it immediately
2022-11-22 12:46:44 +00:00
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 )
2023-03-26 18:10:39 +00:00
local Profiles = self.ui . profiles
if Profiles then
Profiles : updateGestures ( self.dispatcher_prefix .. item.id )
Profiles : updateProfiles ( self.dispatcher_prefix .. item.id )
end
2022-11-22 12:46:44 +00:00
else
self.tweaks_in_dispatcher [ item.id ] = item.title
dispatcherRegisterStyleTweak ( item.id , item.title )
end
touchmenu_instance : updateItems ( )
end ,
2018-05-12 21:24:43 +00:00
} )
end ,
callback = function ( )
-- enable/disable only for this book
2022-11-22 12:46:44 +00:00
self : onToggleStyleTweak ( item.id , item )
2018-05-12 21:24:43 +00:00
end ,
separator = item.separator ,
} )
2019-01-01 17:23:10 +00:00
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 ,
} )
2018-05-12 21:24:43 +00:00
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
2020-01-04 00:18:51 +00:00
description = T ( _ ( " User style tweak at %1 " ) , BD.filepath ( filepath ) ) ,
2018-05-12 21:24:43 +00:00
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
2020-06-08 18:47:31 +00:00
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
2021-09-06 15:08:48 +00:00
return _ ( " Book-specific tweak (long-press to edit) " )
2020-06-08 18:47:31 +00:00
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 )
2018-05-12 21:24:43 +00:00
self.ui . menu : registerToMainMenu ( self )
2022-11-22 12:46:44 +00:00
self : onDispatcherRegisterActions ( )
2018-05-12 21:24:43 +00:00
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
2022-11-22 12:46:44 +00:00
function ReaderStyleTweak : onToggleStyleTweak ( tweak_id , item )
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
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
end
self : updateCssText ( true ) -- apply it immediately
end
function ReaderStyleTweak : onDispatcherRegisterActions ( )
for tweak_id , tweak_title in pairs ( self.tweaks_in_dispatcher ) do
dispatcherRegisterStyleTweak ( tweak_id , tweak_title )
end
end
2020-06-08 18:47:31 +00:00
local BOOK_TWEAK_SAMPLE_CSS = [ [
2023-02-16 22:30:46 +00:00
/* Remove indent from some P used as titles */
p.someTitleClassName {
text - indent : 0 ;
}
/* Get in - page footnotes when no tweak works */
2020-06-08 18:47:31 +00:00
. footnoteContainerClassName {
- cr - hint : footnote - inpage ;
2023-02-16 22:30:46 +00:00
}
/* Help getting some alternative ToC when no headings */
. someSeparatorClassName {
- cr - hint : toc - level1 ;
break - before : always ;
}
/* Hide annoying content */
DIV.someAdvertisement {
display : none ! important ;
2020-06-08 18:47:31 +00:00
}
] ]
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 )
2023-03-23 17:37:46 +00:00
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. " ) } ,
}
2020-06-08 18:47:31 +00:00
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 = { {
2023-03-23 17:37:46 +00:00
-- First buttons on first row (row will be completed with Reset|Save|Close)
2020-06-08 18:47:31 +00:00
{
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 ,
} ,
2023-03-23 17:37:46 +00:00
{
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 ,
} ,
2020-06-08 18:47:31 +00:00
} } ,
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 ,
-- 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.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 ,
2022-09-08 19:52:06 +00:00
close_discarded_notif_text = NOT_MODIFIED_MSG ,
2020-06-08 18:47:31 +00:00
}
UIManager : show ( editor )
editor : onShowKeyboard ( true )
-- ignore first hold release, as we may be invoked from hold
end
2018-05-12 21:24:43 +00:00
return ReaderStyleTweak