From 886b3063e759a3fd336e4d8838096f153b7a295b Mon Sep 17 00:00:00 2001 From: poire-z Date: Mon, 8 Jun 2020 20:47:31 +0200 Subject: [PATCH] Style tweaks: adds "Book-specific tweak" menu item (#6244) Allows editing a CSS snippet to be applied to this book only, without the need to create and edit a User style tweak. Allows copying any other tweak CSS by just taping on it (and pasting into this with Hold). Limit User style tweaks nb of items per menu page to 6 (like we try to do for other tweaks menus). --- .../apps/reader/modules/readerhighlight.lua | 31 +- .../apps/reader/modules/readerstyletweak.lua | 277 +++++++++++++++++- frontend/ui/widget/inputdialog.lua | 9 +- frontend/util.lua | 41 +++ 4 files changed, 320 insertions(+), 38 deletions(-) diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index 8dc63bdd2..cbad29e60 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -10,6 +10,7 @@ local TimeVal = require("ui/timeval") local Translator = require("ui/translator") local UIManager = require("ui/uimanager") local logger = require("logger") +local util = require("util") local _ = require("gettext") local C_ = _.pgettext local T = require("ffi/util").template @@ -797,30 +798,6 @@ function ReaderHighlight:lookup(selected_word, selected_link) end end -local function prettifyCss(css_text) - -- This is not perfect, but enough to make some ugly CSS readable. - -- Get rid of \t so we can use it as a replacement/hiding char - css_text = css_text:gsub("\t", " ") - -- Wrap and indent declarations - css_text = css_text:gsub("%s*{%s*", " {\n ") - css_text = css_text:gsub(";%s*}%s*", ";\n}\n") - css_text = css_text:gsub(";%s*([^}])", ";\n %1") - css_text = css_text:gsub("%s*}%s*", "\n}\n") - -- Cleanup declarations - css_text = css_text:gsub("{[^}]*}", function(s) - s = s:gsub("%s*:%s*", ": ") - -- Temporarily hide/replace ',' in declaration so they - -- are not matched and made multi-lines by followup gsub - s = s:gsub("%s*,%s*", "\t") - return s - end) - -- Have each selector (separated by ',') on a new line - css_text = css_text:gsub("%s*,%s*", " ,\n") - -- Restore hidden ',' in declarations - css_text = css_text:gsub("\t", ", ") - return css_text -end - function ReaderHighlight:viewSelectionHTML(debug_view) if self.ui.document.info.has_pages then return @@ -858,7 +835,7 @@ function ReaderHighlight:viewSelectionHTML(debug_view) -- the height of this section, we don't want to have to scroll -- many pages to get to the HTML content on the initial view.) html = html:gsub("(]*>)%s*(.-)%s*()", function(pre, css_text, post) - return pre .. "\n" .. prettifyCss(css_text) .. post + return pre .. "\n" .. util.prettifyCSS(css_text) .. post end) end local TextViewer = require("ui/widget/textviewer") @@ -887,7 +864,7 @@ function ReaderHighlight:viewSelectionHTML(debug_view) UIManager:close(cssviewer) UIManager:show(TextViewer:new{ title = css_files[i], - text = prettifyCss(css_text), + text = util.prettifyCSS(css_text), text_face = Font:getFace("smallinfont"), justified = false, para_direction_rtl = false, @@ -1248,7 +1225,7 @@ function ReaderHighlight:onHighlightSearch() logger.dbg("search highlight") self:highlightFromHoldPos() if self.selected_text then - local text = require("util").stripPunctuation(self.selected_text.text) + local text = util.stripPunctuation(self.selected_text.text) self.ui:handleEvent(Event:new("ShowSearchDialog", text)) end end diff --git a/frontend/apps/reader/modules/readerstyletweak.lua b/frontend/apps/reader/modules/readerstyletweak.lua index e3b2a18a3..28dee9e49 100644 --- a/frontend/apps/reader/modules/readerstyletweak.lua +++ b/frontend/apps/reader/modules/readerstyletweak.lua @@ -13,6 +13,7 @@ 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") @@ -37,7 +38,7 @@ local TweakInfoWidget = InputContainer:new{ function TweakInfoWidget:init() local tweak = self.tweak if Device:isTouchDevice() then - self.ges_events.TapClose = { + self.ges_events.Tap = { GestureRange:new{ ges = "tap", range = Geom:new{ @@ -90,17 +91,19 @@ function TweakInfoWidget:init() f:close() end end - css = css:gsub("^%s+", ""):gsub("%s+$", "") - table.insert(content, FrameContainer:new{ + self.css_text = css:gsub("^%s+", ""):gsub("%s+$", "") + self.css_frame = FrameContainer:new{ bordersize = Size.border.thin, padding = Size.padding.large, TextBoxWidget:new{ - text = css, + 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, @@ -183,8 +186,19 @@ function TweakInfoWidget:onClose() return true end -function TweakInfoWidget:onTapClose(arg, ges) - if ges.pos:notIntersectWith(self.movable.dimen) then +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"), + timeout = 2 + }) + return true + elseif ges.pos:notIntersectWith(self.movable.dimen) then + -- Tap outside closes widget self:onClose() return true end @@ -298,6 +312,10 @@ function ReaderStyleTweak:updateCssText(apply) css = css:gsub("^%s+", ""):gsub("%s+$", "") 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 @@ -315,6 +333,9 @@ function ReaderStyleTweak:onReadSettings(config) -- 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 @@ -326,6 +347,9 @@ function ReaderStyleTweak:onSaveSettings() 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) + 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 function ReaderStyleTweak:init() @@ -363,11 +387,12 @@ You can enable individual tweaks on this book with a tap, or view more details a -- css_tweaks.lua, or like the one we build from user styletweaks -- directory files and sub-directories) local addTweakMenuItem - addTweakMenuItem = function(menu, item) + 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) -- recurse + addTweakMenuItem(sub_item_table, it, max_per_page) -- recurse end table.insert(menu, { text_func = function() @@ -515,7 +540,36 @@ You can enable individual tweaks on this book with a tap, or view more details a 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) + 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 (hold 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) end @@ -534,4 +588,207 @@ function ReaderStyleTweak:addToMainMenu(menu_items) } end +local BOOK_TWEAK_SAMPLE_CSS = [[ +p.someTitleClassName { text-indent: 0; } + +DIV.advertisement { display: none !important; } + +.footnoteContainerClassName { + font-size: 80%; + margin: 0 !important; + -cr-hint: footnote-inpage; +} +]] + +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) + +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 button 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, + }, + }}, + 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, + timeout = 2, + }) + -- 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 diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 4f7dccf85..c21ca9329 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -152,6 +152,7 @@ local InputDialog = InputContainer:new{ -- - on success: as the notification text instead of the default one -- - on failure: in an InfoMessage close_callback = nil, -- Called when closing (if discarded or saved, after save_callback if saved) + edited_callback = nil, -- Called on each text modification -- For use by TextEditor plugin: view_pos_callback = nil, -- Called with no arg to get initial top_line_num/charpos, @@ -466,8 +467,11 @@ function InputDialog:getInputValue() end end -function InputDialog:setInputText(text) +function InputDialog:setInputText(text, edited_state) self._input_widget:setText(text) + if edited_state ~= nil and self._buttons_edit_callback then + self._buttons_edit_callback(edited_state) + end end function InputDialog:isTextEditable() @@ -580,6 +584,9 @@ function InputDialog:_addSaveCloseButtons() if button("reset") then button("reset"):enable() end self:refreshButtons() end + if self.edited_callback then + self.edited_callback() + end end if self.reset_callback then -- if reset_callback provided, add button to restore diff --git a/frontend/util.lua b/frontend/util.lua index b90fa7972..ff544ac35 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -908,6 +908,47 @@ function util.htmlEscape(text) }) end +--- Prettify a CSS stylesheet +-- Not perfect, but enough to make some ugly CSS readable. +-- By default, each selector and each property is put on its own line. +-- With condensed=true, condense each full declaration on a single line. +-- +--- @string CSS string +--- @boolean condensed[opt=false] true to condense each declaration on a line +--- @treturn string the CSS prettified +function util.prettifyCSS(css_text, condensed) + if not condensed then + -- Get rid of \t so we can use it as a replacement/hiding char + css_text = css_text:gsub("\t", " ") + -- Wrap and indent declarations + css_text = css_text:gsub("%s*{%s*", " {\n ") + css_text = css_text:gsub(";%s*}%s*", ";\n}\n") + css_text = css_text:gsub(";%s*([^}])", ";\n %1") + css_text = css_text:gsub("%s*}%s*", "\n}\n") + -- Cleanup declarations + css_text = css_text:gsub("{[^}]*}", function(s) + s = s:gsub("%s*:%s*", ": ") + -- Temporarily hide/replace ',' in declaration so they + -- are not matched and made multi-lines by followup gsub + s = s:gsub("%s*,%s*", "\t") + return s + end) + -- Have each selector (separated by ',') on a new line + css_text = css_text:gsub("%s*,%s*", " ,\n") + -- Restore hidden ',' in declarations + css_text = css_text:gsub("\t", ", ") + else + -- Go thru previous method to have something standard to work on + css_text = util.prettifyCSS(css_text) + -- And condense that + css_text = css_text:gsub(" {\n ", " { ") + css_text = css_text:gsub(";\n ", "; ") + css_text = css_text:gsub("\n}", " }") + css_text = css_text:gsub(" ,\n", ", ") + end + return css_text +end + --- Escape list for shell usage --- @table args the list of arguments to escape --- @treturn string the escaped and concatenated arguments