diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index faddf6844..a812102e1 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -1366,6 +1366,7 @@ function ReaderHighlight:viewSelectionHTML(debug_view, no_css_files_buttons) justified = false, para_direction_rtl = false, auto_para_direction = false, + add_default_buttons = true, buttons_table = { {{ text = _("Prettify"), @@ -1382,19 +1383,13 @@ function ReaderHighlight:viewSelectionHTML(debug_view, no_css_files_buttons) }) end, }}, - {{ - text = _("Close"), - callback = function() - UIManager:close(cssviewer) - end, - }}, } } UIManager:show(cssviewer) end, hold_callback = buttons_hold_callback, } - -- One button per row, too make room for the possibly long css filename + -- One button per row, to make room for the possibly long css filename table.insert(buttons_table, {button}) end end @@ -1418,13 +1413,6 @@ function ReaderHighlight:viewSelectionHTML(debug_view, no_css_files_buttons) end, hold_callback = buttons_hold_callback, }}) - table.insert(buttons_table, {{ - text = _("Close"), - callback = function() - UIManager:close(textviewer) - end, - hold_callback = buttons_hold_callback, - }}) textviewer = TextViewer:new{ title = _("Selection HTML"), text = html, @@ -1432,6 +1420,8 @@ function ReaderHighlight:viewSelectionHTML(debug_view, no_css_files_buttons) justified = false, para_direction_rtl = false, auto_para_direction = false, + add_default_buttons = true, + default_hold_callback = buttons_hold_callback, buttons_table = buttons_table, } UIManager:show(textviewer) diff --git a/frontend/ui/translator.lua b/frontend/ui/translator.lua index d3c0c5abb..9b63ab676 100644 --- a/frontend/ui/translator.lua +++ b/frontend/ui/translator.lua @@ -578,38 +578,10 @@ function Translator:_showTranslation(text, target_lang, source_lang, from_highli -- table.insert(output, require("dump")(result)) -- for debugging local text_all = table.concat(output, "\n") local textviewer - local buttons_table = { - { - { - text = _("Close"), - is_enter_default = true, - callback = function() - textviewer:onClose() - end, - }, - }, - } - if Device:hasClipboard() then - table.insert(buttons_table, 1, - { - { - text = _("Copy main translation"), - callback = function() - Device.input.setClipboardText(text_main) - end, - }, - { - text = _("Copy all"), - callback = function() - Device.input.setClipboardText(text_all) - end, - }, - } - ) - end + local buttons_table = {} if from_highlight then local ui = require("apps/reader/readerui").instance - table.insert(buttons_table, 1, + table.insert(buttons_table, { { text = _("Save main translation to note"), @@ -640,6 +612,24 @@ function Translator:_showTranslation(text, target_lang, source_lang, from_highli } ) end + if Device:hasClipboard() then + table.insert(buttons_table, + { + { + text = _("Copy main translation"), + callback = function() + Device.input.setClipboardText(text_main) + end, + }, + { + text = _("Copy all"), + callback = function() + Device.input.setClipboardText(text_all) + end, + }, + } + ) + end textviewer = TextViewer:new{ title = T(_("Translation from %1"), self:getLanguageName(source_lang, "?")), title_multilines = true, @@ -648,6 +638,7 @@ function Translator:_showTranslation(text, target_lang, source_lang, from_highli text = text_all, height = math.floor(Screen:getHeight() * 0.8), justified = G_reader_settings:nilOrTrue("dict_justify"), + add_default_buttons = true, buttons_table = buttons_table, close_callback = function() if from_highlight then diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index ec6b8f019..9b7b945d9 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -117,6 +117,7 @@ local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local Screen = Device.screen local T = require("ffi/util").template +local util = require("util") local _ = require("gettext") local InputDialog = FocusManager:new{ @@ -793,46 +794,14 @@ function InputDialog:_addScrollButtons(nav_bar) { text = _("Find first"), callback = function() - self.search_value = input_dialog:getInputText() - if self.search_value ~= "" then - UIManager:close(input_dialog) - self.keyboard_hidden = keyboard_hidden_state - self:toggleKeyboard() - local msg - local char_pos = self._input_widget:searchString(self.search_value, self.case_sensitive, 1) - if char_pos > 0 then - self._input_widget:moveCursorToCharPos(char_pos) - msg = T(_("Found in line %1."), self._input_widget:getLineNums()) - else - msg = _("Not found.") - end - UIManager:show(Notification:new{ - text = msg, - }) - end + self:findCallback(keyboard_hidden_state, input_dialog, true) end, }, { text = _("Find next"), is_enter_default = true, callback = function() - self.search_value = input_dialog:getInputText() - if self.search_value ~= "" then - UIManager:close(input_dialog) - self.keyboard_hidden = keyboard_hidden_state - self:toggleKeyboard() - local msg - local char_pos = self._input_widget:searchString(self.search_value, self.case_sensitive) - if char_pos > 0 then - self._input_widget:moveCursorToCharPos(char_pos) - msg = T(_("Found in line %1."), self._input_widget:getLineNums()) - else - msg = _("Not found.") - end - UIManager:show(Notification:new{ - text = msg, - }) - end + self:findCallback(keyboard_hidden_state, input_dialog) end, }, }, @@ -967,4 +936,24 @@ function InputDialog:_addScrollButtons(nav_bar) end end +function InputDialog:findCallback(keyboard_hidden_state, input_dialog, find_first) + self.search_value = input_dialog:getInputText() + if self.search_value == "" then return end + UIManager:close(input_dialog) + self.keyboard_hidden = keyboard_hidden_state + self:toggleKeyboard() + local start_pos = find_first and 1 or self._charpos + 1 + local char_pos = util.stringSearch(self.input, self.search_value, self.case_sensitive, start_pos) + local msg + if char_pos > 0 then + self._input_widget:moveCursorToCharPos(char_pos) + msg = T(_("Found in line %1."), self._input_widget:getLineNums()) + else + msg = _("Not found.") + end + UIManager:show(Notification:new{ + text = msg, + }) +end + return InputDialog diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index 0093f494d..79cdee573 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -12,7 +12,6 @@ local ScrollTextWidget = require("ui/widget/scrolltextwidget") local Size = require("ui/size") local TextBoxWidget = require("ui/widget/textboxwidget") local UIManager = require("ui/uimanager") -local Utf8Proc = require("ffi/utf8proc") local VerticalGroup = require("ui/widget/verticalgroup") local dbg = require("dbg") local util = require("util") @@ -189,6 +188,7 @@ if Device:isTouchDevice() or Device:hasDPad() then width = math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * 0.8), height = math.floor(math.max(Screen:getWidth(), Screen:getHeight()) * 0.4), justified = false, + modal = true, stop_events_propagation = true, buttons_table = { { @@ -759,36 +759,6 @@ function InputText:getStringPos(left_delimiter, right_delimiter, char_pos) return start_pos, end_pos end ---- Search for a string. --- if start_pos not set, starts a search from the next to cursor position --- returns first found position or 0 if not found -function InputText:searchString(str, case_sensitive, start_pos) - local str_charlist = util.splitToChars(str) - local str_len = #str_charlist - local char_pos, found = 0, 0 - start_pos = start_pos and (start_pos - 1) or self.charpos - for i = start_pos, #self.charlist - str_len do - for j = 1, str_len do - local char_txt = self.charlist[i + j] - local char_str = str_charlist[j] - if not case_sensitive then - char_txt = Utf8Proc.lowercase(util.fixUtf8(char_txt, "?")) - char_str = Utf8Proc.lowercase(util.fixUtf8(char_str, "?")) - end - if char_txt ~= char_str then - found = 0 - break - end - found = found + 1 - end - if found == str_len then - char_pos = i + 1 - break - end - end - return char_pos -end - --- Return the character at the given offset. If is_absolute is truthy then the -- offset is the absolute position, otherwise the offset is added to the current -- cursor position (negative offsets are allowed). diff --git a/frontend/ui/widget/scrolltextwidget.lua b/frontend/ui/widget/scrolltextwidget.lua index d5932dc01..578d9f39a 100644 --- a/frontend/ui/widget/scrolltextwidget.lua +++ b/frontend/ui/widget/scrolltextwidget.lua @@ -144,6 +144,15 @@ function ScrollTextWidget:getCharPos() return self.text_widget:getCharPos() end +function ScrollTextWidget:getCharPosAtXY(x, y) + return self.text_widget:getCharPosAtXY(x, y) +end + +function ScrollTextWidget:getCharPosLineNum(charpos) + local _, _, line_num = self.text_widget:_getXYForCharPos(charpos) + return line_num -- screen line number +end + function ScrollTextWidget:updateScrollBar(is_partial) local low, high = self.text_widget:getVisibleHeightRatios() if low ~= self.prev_low or high ~= self.prev_high then @@ -187,8 +196,12 @@ function ScrollTextWidget:resetScroll() self.v_scroll_bar.enable = visible_line_count < total_line_count end -function ScrollTextWidget:moveCursorToCharPos(charpos) - self.text_widget:moveCursorToCharPos(charpos) +function ScrollTextWidget:moveCursorToCharPos(charpos, centered_lines_count) + if centered_lines_count then + self.text_widget:moveCursorToCharPosKeepingViewCentered(charpos, centered_lines_count) + else + self.text_widget:moveCursorToCharPos(charpos) + end self:updateScrollBar() end diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index a1322605f..9a2f45d50 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -1363,6 +1363,7 @@ end -- Return the coordinates (relative to current view, so negative y is possible) -- of the left of char at charpos (use self.charpos if none provided) +-- and the number of the line with charpos on the screen function TextBoxWidget:_getXYForCharPos(charpos) if not charpos then charpos = self.charpos @@ -1389,6 +1390,7 @@ function TextBoxWidget:_getXYForCharPos(charpos) end end local y = (ln - self.virtual_line_num) * self.line_height_px + local screen_line_num = ln - self.virtual_line_num + 1 -- Find the x offset in the current line. @@ -1441,7 +1443,7 @@ function TextBoxWidget:_getXYForCharPos(charpos) end end -- logger.dbg("_getXYForCharPos(", charpos, "):", x, y) - return x, y + return x, y, screen_line_num end -- Only when not self.use_xtext: @@ -1458,7 +1460,7 @@ function TextBoxWidget:_getXYForCharPos(charpos) -- Cursor can be drawn at x, it will be on the left of the char pointed by charpos -- (x=0 for first char of line - for end of line, it will be before the \n, the \n -- itself being not displayed) - return x, y + return x, y, screen_line_num end -- Return the charpos at provided coordinates (relative to current view, @@ -1565,11 +1567,6 @@ local CURSOR_USE_REFRESH_FUNCS = G_reader_settings:nilOrTrue("ui_cursor_use_refr -- Update charpos to the one provided; if out of current view, update -- virtual_line_num to move it to view, and draw the cursor function TextBoxWidget:moveCursorToCharPos(charpos) - if not self.editable then - -- we shouldn't have been called if not editable - logger.warn("TextBoxWidget:moveCursorToCharPos called, but not editable") - return - end self.charpos = charpos self.prev_virtual_line_num = self.virtual_line_num local x, y = self:_getXYForCharPos() -- we can get y outside current view @@ -1711,6 +1708,30 @@ function TextBoxWidget:moveCursorToCharPos(charpos) end end +-- Update view to show the line with charpos not far than lines away +-- from the center of the screen, and draw the cursor. +function TextBoxWidget:moveCursorToCharPosKeepingViewCentered(charpos, centered_lines_count) + local old_virtual_line_num = self.virtual_line_num + self.for_measurement_only = true + self:moveCursorToCharPos(charpos) + self.for_measurement_only = false + local _, _, screen_line_num = self:_getXYForCharPos(charpos) + local new_virtual_line_num = self.virtual_line_num + screen_line_num - self.lines_per_page / 2 + local max_virtual_line_num = #self.vertical_string_list - self.lines_per_page + 1 + if new_virtual_line_num < 1 then + new_virtual_line_num = 1 + elseif new_virtual_line_num > max_virtual_line_num then + new_virtual_line_num = max_virtual_line_num + end + if math.abs(new_virtual_line_num - old_virtual_line_num) > centered_lines_count then + self.virtual_line_num = new_virtual_line_num + else + self.virtual_line_num = old_virtual_line_num + end + self:_updateLayout() + self:moveCursorToCharPos(charpos) +end + function TextBoxWidget:moveCursorToXY(x, y, restrict_to_view) if restrict_to_view then -- Wrap y to current view (when getting coordinates from gesture) diff --git a/frontend/ui/widget/textviewer.lua b/frontend/ui/widget/textviewer.lua index 4fce28bcb..3a56a7d94 100644 --- a/frontend/ui/widget/textviewer.lua +++ b/frontend/ui/widget/textviewer.lua @@ -12,24 +12,28 @@ local BD = require("ui/bidi") local Blitbuffer = require("ffi/blitbuffer") local ButtonTable = require("ui/widget/buttontable") local CenterContainer = require("ui/widget/container/centercontainer") +local CheckButton = require("ui/widget/checkbutton") local Device = require("device") local Geom = require("ui/geometry") local Font = require("ui/font") local FrameContainer = require("ui/widget/container/framecontainer") local GestureRange = require("ui/gesturerange") local InputContainer = require("ui/widget/container/inputcontainer") +local InputDialog = require("ui/widget/inputdialog") local MovableContainer = require("ui/widget/container/movablecontainer") +local Notification = require("ui/widget/notification") local ScrollTextWidget = require("ui/widget/scrolltextwidget") local Size = require("ui/size") local TitleBar = require("ui/widget/titlebar") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local WidgetContainer = require("ui/widget/container/widgetcontainer") +local T = require("ffi/util").template +local util = require("util") local _ = require("gettext") local Screen = Device.screen local TextViewer = InputContainer:new{ - modal = true, title = nil, text = nil, width = nil, @@ -56,6 +60,10 @@ local TextViewer = InputContainer:new{ text_padding = Size.padding.large, text_margin = Size.margin.small, button_padding = Size.padding.default, + -- Bottom row with Close, Find buttons. Also added when no caller's buttons defined. + add_default_buttons = nil, + default_hold_callback = nil, -- on each default button + find_centered_lines_count = 5, -- line with find results to be not far from the center } function TextViewer:init() @@ -69,6 +77,10 @@ function TextViewer:init() self.width = self.width or Screen:getWidth() - Screen:scaleBySize(30) self.height = self.height or Screen:getHeight() - Screen:scaleBySize(30) + self._find_next = false + self._find_next_button = false + self._old_virtual_line_num = 1 + if Device:hasKeys() then self.key_events = { Close = { {Device.input.group.Back}, doc = "close text viewer" } @@ -112,25 +124,48 @@ function TextViewer:init() show_parent = self, } - local buttons = self.buttons_table or + local default_buttons = { { - { - text = _("Close"), - callback = function() - self:onClose() - end, - }, + text = _("Close"), + callback = function() + self:onClose() + end, + hold_callback = self.default_hold_callback, + }, + { + text = _("Find"), + id = "find", + callback = function() + if self._find_next then + self:findCallback() + else + self:findDialog() + end + end, + hold_callback = function() + if self._find_next then + self:findDialog() + else + if self.default_hold_callback then + self.default_hold_callback() + end + end + end, }, } - local button_table = ButtonTable:new{ + local buttons = self.buttons_table or {} + if self.add_default_buttons or not self.buttons_table then + table.insert(buttons, default_buttons) + end + self.button_table = ButtonTable:new{ width = self.width - 2*self.button_padding, buttons = buttons, zero_sep = true, show_parent = self, } - local textw_height = self.height - titlebar:getHeight() - button_table:getSize().h + local textw_height = self.height - titlebar:getHeight() - self.button_table:getSize().h self.scroll_text_w = ScrollTextWidget:new{ text = self.text, @@ -170,9 +205,9 @@ function TextViewer:init() CenterContainer:new{ dimen = Geom:new{ w = self.width, - h = button_table:getSize().h, + h = self.button_table:getSize().h, }, - button_table, + self.button_table, } } } @@ -244,4 +279,90 @@ function TextViewer:onSwipe(arg, ges) return self.movable:onMovableSwipe(arg, ges) end +function TextViewer:findDialog() + local input_dialog + input_dialog = InputDialog:new{ + title = _("Enter text to search for"), + input = self.search_value, + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(input_dialog) + end, + }, + { + text = _("Find first"), + callback = function() + self._find_next = false + self:findCallback(input_dialog) + end, + }, + { + text = _("Find next"), + is_enter_default = true, + callback = function() + self._find_next = true + self:findCallback(input_dialog) + end, + }, + }, + }, + } + self.check_button_case = CheckButton:new{ + text = _("Case sensitive"), + checked = self.case_sensitive, + parent = input_dialog, + callback = function() + self.case_sensitive = self.check_button_case.checked + end, + } + input_dialog:addWidget(self.check_button_case) + + UIManager:show(input_dialog) + input_dialog:onShowKeyboard() +end + +function TextViewer:findCallback(input_dialog) + if input_dialog then + self.search_value = input_dialog:getInputText() + if self.search_value == "" then return end + UIManager:close(input_dialog) + end + local start_pos = 1 + if self._find_next then + local charpos, new_virtual_line_num = self.scroll_text_w:getCharPos() + if math.abs(new_virtual_line_num - self._old_virtual_line_num) > self.find_centered_lines_count then + start_pos = self.scroll_text_w:getCharPosAtXY(0, 0) -- first char of the top line + else + start_pos = (charpos or 0) + 1 -- previous search result + end + end + local char_pos = util.stringSearch(self.text, self.search_value, self.case_sensitive, start_pos) + local msg + if char_pos > 0 then + self.scroll_text_w:moveCursorToCharPos(char_pos, self.find_centered_lines_count) + msg = T(_("Found, screen line %1."), self.scroll_text_w:getCharPosLineNum()) + self._find_next = true + self._old_virtual_line_num = select(2, self.scroll_text_w:getCharPos()) + else + msg = _("Not found.") + self._find_next = false + self._old_virtual_line_num = 1 + end + UIManager:show(Notification:new{ + text = msg, + }) + if self._find_next_button ~= self._find_next then + self._find_next_button = self._find_next + local button_text = self._find_next and _("Find next") or _("Find") + local find_button = self.button_table:getButtonById("find") + find_button:setText(button_text, find_button.width) + UIManager:setDirty(self, function() + return "ui", find_button.dimen + end) + end +end + return TextViewer diff --git a/frontend/util.lua b/frontend/util.lua index 7eb3364c4..08bcf186c 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -3,6 +3,7 @@ This module contains miscellaneous helper functions for the KOReader frontend. ]] local BaseUtil = require("ffi/util") +local Utf8Proc = require("ffi/utf8proc") local _ = require("gettext") local C_ = _.pgettext local T = BaseUtil.template @@ -1334,6 +1335,41 @@ function util.stringEndsWith(str, ending) return ending == "" or str:sub(-#ending) == ending end +--- Search a string in a text. +-- @string or table txt Text (char list) to search in +-- @string str String to search for +-- @boolean case_sensitive +-- @number start_pos Position number in text to start search from +-- @treturn number Position number or 0 if not found +function util.stringSearch(txt, str, case_sensitive, start_pos) + if not case_sensitive then + str = Utf8Proc.lowercase(util.fixUtf8(str, "?")) + end + local txt_charlist = type(txt) == "table" and txt or util.splitToChars(txt) + local str_charlist = util.splitToChars(str) + local str_len = #str_charlist + local char_pos, found = 0, 0 + for i = start_pos - 1, #txt_charlist - str_len do + for j = 1, str_len do + local char_txt = txt_charlist[i + j] + local char_str = str_charlist[j] + if not case_sensitive then + char_txt = Utf8Proc.lowercase(util.fixUtf8(char_txt, "?")) + end + if char_txt ~= char_str then + found = 0 + break + end + found = found + 1 + end + if found == str_len then + char_pos = i + 1 + break + end + end + return char_pos +end + local WrappedFunction_mt = { __call = function(self, ...) if self.before_callback then