From 4f849c23aba37ba902c119d5f9fb4e9378e3c2f0 Mon Sep 17 00:00:00 2001 From: Philip Chan Date: Sat, 12 Mar 2022 19:16:50 +0800 Subject: [PATCH] Non-touch: highlight support (#8877) readerhighlight: non-touch support focusmanager: fix same type container share same selected field radiobuttonwidget: non touch support sortwidget: non touch support openwithdialog: fix layout contains textinput, checkboxes added to layout twice --- .../apps/reader/modules/readerhighlight.lua | 193 +++++++++++++++++- frontend/apps/reader/modules/readerview.lua | 23 +++ frontend/ui/elements/reader_menu_order.lua | 2 + frontend/ui/widget/focusmanager.lua | 28 ++- frontend/ui/widget/openwithdialog.lua | 4 +- frontend/ui/widget/radiobuttonwidget.lua | 34 ++- frontend/ui/widget/sortwidget.lua | 63 +++--- 7 files changed, 282 insertions(+), 65 deletions(-) diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index fe16e28f6..cc9ad1e38 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -13,6 +13,7 @@ local UIManager = require("ui/uimanager") local dbg = require("dbg") local logger = require("logger") local util = require("util") +local Size = require("ui/size") local ffiUtil = require("ffi/util") local _ = require("gettext") local C_ = _.pgettext @@ -47,6 +48,28 @@ end function ReaderHighlight:init() self.select_mode = false -- extended highlighting + self._start_indicator_highlight = false + self._current_indicator_pos = nil + self._previous_indicator_pos = nil + + if Device:hasDPad() then + -- Used for text selection with dpad/keys + local QUICK_INDICTOR_MOVE = true + self.key_events.StopHighlightIndicator = { {Device.input.group.Back}, doc = "Stop non-touch highlight", args = true } -- true: clear highlight selection + self.key_events.UpHighlightIndicator = { {"Up"}, doc = "move indicator up", event = "MoveHighlightIndicator", args = {0, -1} } + self.key_events.DownHighlightIndicator = { {"Down"}, doc = "move indicator down", event = "MoveHighlightIndicator", args = {0, 1} } + -- let FewKeys device can move indicator left + self.key_events.LeftHighlightIndicator = { {"Left"}, doc = "move indicator left", event = "MoveHighlightIndicator", args = {-1, 0} } + self.key_events.RightHighlightIndicator = { {"Right"}, doc = "move indicator right", event = "MoveHighlightIndicator", args = {1, 0} } + self.key_events.HighlightPress = { {"Press"}, doc = "highlight start or end" } + if Device:hasKeys() then + self.key_events.QuicklyUpHighlightIndicator = { {"Shift", "Up"}, doc = "quick move indicator up", event = "MoveHighlightIndicator", args = {0, -1, QUICK_INDICTOR_MOVE} } + self.key_events.QuicklyDownHighlightIndicator = { {"Shift", "Down"}, doc = "quick move indicator down", event = "MoveHighlightIndicator", args = {0, 1, QUICK_INDICTOR_MOVE} } + self.key_events.QuicklyLeftHighlightIndicator = { {"Shift", "Left"}, doc = "quick move indicator left", event = "MoveHighlightIndicator", args = {-1, 0, QUICK_INDICTOR_MOVE} } + self.key_events.QuicklyRightHighlightIndicator = { {"Shift", "Right"}, doc = "quick move indicator right", event = "MoveHighlightIndicator", args = {1, 0, QUICK_INDICTOR_MOVE} } + self.key_events.StartHighlightIndicator = { {"H"}, doc = "start non-touch highlight" } + end + end self._highlight_buttons = { -- highlight and add_note are for the document itself, @@ -295,6 +318,14 @@ local long_press_action = { function ReaderHighlight:addToMainMenu(menu_items) -- insert table to main reader menu + if Device:hasDPad() then + menu_items.start_content_selection = { + text = _("Start content selection"), + callback = function() + self:onStartHighlightIndicator() + end, + } + end menu_items.highlight_options = { text = _("Highlight style"), sub_item_table = {}, @@ -361,14 +392,35 @@ function ReaderHighlight:addToMainMenu(menu_items) end menu_items.translation_settings = Translator:genSettingsMenu() - if not Device:isTouchDevice() then - -- Menu items below aren't needed. - return - end - menu_items.long_press = { text = _("Long-press on text"), sub_item_table = { + { + text = _("Highlight long-press interval"), + keep_menu_open = true, + callback = function() + local SpinWidget = require("ui/widget/spinwidget") + local items = SpinWidget:new{ + title_text = _("Highlight long-press interval"), + info_text = _([[ +If a touch is not released in this interval, it is considered a long-press. On document text, single word selection will not be triggered. + +The interval value is in seconds and can range from 3 to 20 seconds.]]), + width = math.floor(Screen:getWidth() * 0.75), + value = G_reader_settings:readSetting("highlight_long_hold_threshold", 3), + value_min = 3, + value_max = 20, + value_step = 1, + value_hold_step = 5, + ok_text = _("Set interval"), + default_value = 3, + callback = function(spin) + G_reader_settings:saveSetting("highlight_long_hold_threshold", spin.value) + end + } + UIManager:show(items) + end, + }, { text = _("Dictionary on single word selection"), checked_func = function() @@ -397,6 +449,12 @@ function ReaderHighlight:addToMainMenu(menu_items) end, }) end + -- long_press menu is under taps_and_gestures menu which is not available for non touch device + -- Clone long_press menu and change label making much meaning for non touch devices + if Device:hasDPad() then + menu_items.selection_text = util.tableDeepCopy(menu_items.long_press) + menu_items.selection_text.text = _("Select on text") + end end function ReaderHighlight:genPanelZoomMenu() @@ -907,6 +965,7 @@ function ReaderHighlight:onHold(arg, ges) fullscreen = true, } UIManager:show(imgviewer) + self:onStopHighlightIndicator() return true end @@ -1365,7 +1424,8 @@ function ReaderHighlight:onHoldRelease() local long_final_hold = false if self.hold_last_tv then local hold_duration = TimeVal:now() - self.hold_last_tv - if hold_duration > TimeVal:new{ sec = 3, usec = 0 } then + local long_hold_threshold = G_reader_settings:readSetting("highlight_long_hold_threshold", 3) + if hold_duration > TimeVal:new{ sec = long_hold_threshold, usec = 0 } then -- We stayed 3 seconds before release without updating selection long_final_hold = true end @@ -1806,4 +1866,125 @@ function ReaderHighlight:onClose() self:clear() end +function ReaderHighlight:onHighlightPress() + if self._current_indicator_pos then + if not self._start_indicator_highlight then + -- try a tap at current indicator position to open any existing highlight + if not self:onTap(nil, self:_createHighlightGesture("tap")) then + -- no existing highlight at current indicator position: start hold + self._start_indicator_highlight = true + self:onHold(nil, self:_createHighlightGesture("hold")) + if self.selected_text and self.selected_text.sboxes and #self.selected_text.sboxes then + local pos = self.selected_text.sboxes[1] + -- set hold_pos to center of selected_test to make center selection more stable, not jitted at edge + self.hold_pos = self.view:screenToPageTransform({ + x = pos.x + pos.w / 2, + y = pos.y + pos.h / 2 + }) + -- move indicator to center selected text making succeed same row selection much accurate. + UIManager:setDirty(self.dialog, "ui", self._current_indicator_pos) + self._current_indicator_pos.x = pos.x + pos.w / 2 - self._current_indicator_pos.w / 2 + self._current_indicator_pos.y = pos.y + pos.h / 2 - self._current_indicator_pos.h / 2 + UIManager:setDirty(self.dialog, "ui", self._current_indicator_pos) + end + else + self:onStopHighlightIndicator(true) -- need_clear_selection=true + end + else + self:onHoldRelease(nil, self:_createHighlightGesture("hold_release")) + self:onStopHighlightIndicator() + end + return true + end + return false +end + +function ReaderHighlight:onStartHighlightIndicator() + if self.view.visible_area and not self._current_indicator_pos then + -- set start position to centor of page + local rect = self._previous_indicator_pos + if not rect then + rect = Geom:new() + rect.x = self.view.visible_area.w / 2 + rect.y = self.view.visible_area.h / 2 + rect.w = Size.item.height_default + rect.h = rect.w + end + self._current_indicator_pos = rect + self.view.highlight.indicator = rect + UIManager:setDirty(self.dialog, "ui", rect) + return true + end + return false +end + +function ReaderHighlight:onStopHighlightIndicator(need_clear_selection) + if self._current_indicator_pos then + local rect = self._current_indicator_pos + self._previous_indicator_pos = rect + self._start_indicator_highlight = false + self._current_indicator_pos = nil + self.view.highlight.indicator = nil + UIManager:setDirty(self.dialog, "ui", rect) + if need_clear_selection then + self:clear() + end + return true + end + return false +end + +function ReaderHighlight:onMoveHighlightIndicator(args) + if self.view.visible_area and self._current_indicator_pos then + local dx, dy, quick_move = unpack(args) + local step_distance = self.view.visible_area.w / 5 -- quick move distance: fifth of visible_area + local y_step_distance = self.view.visible_area.h / 5 + if step_distance > y_step_distance then + -- take the smaller, make all direction move distance much predictable + step_distance = y_step_distance + end + if not quick_move then + step_distance = step_distance / 4 -- twentieth of visible_area + end + local rect = self._current_indicator_pos:copy() + rect.x = rect.x + step_distance * dx + rect.y = rect.y + step_distance * dy + if rect.x < 0 then + rect.x = 0 + end + if rect.x + rect.w > self.view.visible_area.w then + rect.x = self.view.visible_area.w - rect.w + end + if rect.y < 0 then + rect.y = 0 + end + if rect.y + rect.h > self.view.visible_area.h then + rect.y = self.view.visible_area.h - rect.h + end + UIManager:setDirty(self.dialog, "ui", self._current_indicator_pos) + self._current_indicator_pos = rect + self.view.highlight.indicator = rect + UIManager:setDirty(self.dialog, "ui", rect) + if self._start_indicator_highlight then + self:onHoldPan(nil, self:_createHighlightGesture("hold_pan")) + end + return true + end + return false +end + +function ReaderHighlight:_createHighlightGesture(gesture) + local point = self._current_indicator_pos:copy() + point.x = point.x + point.w / 2 + point.y = point.y + point.h / 2 + point.w = 0 + point.h = 0 + return { + ges = gesture, + pos = point, + time = TimeVal:realtime(), + } +end + + return ReaderHighlight diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index 1825afa66..df953dc5c 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -19,6 +19,7 @@ local UIManager = require("ui/uimanager") local dbg = require("dbg") local logger = require("logger") local optionsutil = require("ui/data/optionsutil") +local Size = require("ui/size") local _ = require("gettext") local Screen = Device.screen local T = require("ffi/util").template @@ -44,6 +45,7 @@ local ReaderView = OverlapGroup:extend{ temp = {}, saved_drawer = "lighten", saved = {}, + indicator = nil, -- geom: non-touch highlight position indicator: {x = 50, y=50} }, highlight_visible = true, -- PDF/DjVu continuous paging @@ -199,6 +201,10 @@ function ReaderView:paintTo(bb, x, y) if self.highlight.temp then self:drawTempHighlight(bb, x, y) end + -- draw highlight position indicator for non-touch + if self.highlight.indicator then + self:drawHighlightIndicator(bb, x, y) + end -- paint dogear if self.dogear_visible then self.dogear:paintTo(bb, x, y) @@ -454,6 +460,23 @@ function ReaderView:drawScrollView(bb, x, y) self.state.pos) end +function ReaderView:drawHighlightIndicator(bb, x, y) + local rect = self.highlight.indicator + -- paint big cross line + + bb:paintRect( + rect.x, + rect.y + rect.h / 2 - Size.border.thick / 2, + rect.w, + Size.border.thick + ) + bb:paintRect( + rect.x + rect.w / 2 - Size.border.thick / 2, + rect.y, + Size.border.thick, + rect.h + ) +end + function ReaderView:drawTempHighlight(bb, x, y) for page, boxes in pairs(self.highlight.temp) do for i = 1, #boxes do diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index 3a5be9f15..06e00eb77 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -55,8 +55,10 @@ local order = { "speed_reading_module_perception_expander", "----------------------------", "highlight_options", + "selection_text", -- if Device:hasDPad() "panel_zoom_options", "djvu_render_mode", + "start_content_selection", -- if Device:hasDPad(), put this as last one so it is easy to select with "press" and "up" keys }, setting = { -- common settings diff --git a/frontend/ui/widget/focusmanager.lua b/frontend/ui/widget/focusmanager.lua index 66bd063ae..603f417ae 100644 --- a/frontend/ui/widget/focusmanager.lua +++ b/frontend/ui/widget/focusmanager.lua @@ -33,11 +33,6 @@ local FocusManager = InputContainer:new{ } function FocusManager:init() - if not self.selected then - self.selected = { x = 1, y = 1 } - else - self.selected = self.selected -- make sure current FocusManager has its own selected field - end if Device:hasDPad() then local event_keys = {} -- these will all generate the same event, just with different arguments @@ -97,6 +92,23 @@ function FocusManager:init() end end + +function FocusManager:_init() + InputContainer._init(self) + -- Make sure each FocusManager instance has its own selection field. + -- Take ButtonTable = FocusManager:new{} for example. + -- FocusManager:init method called once and all ButtonTable instances share same selected field. + -- It has problem when + -- 1. ButtonTable A (layout 1 row, 4 columns) shown, and move focus, make selected to (4, 1) + -- 2. ButtonTable A closed and ButtonTable B (layout 2 rows, 2 columns) shown + -- 3. selected (4, 1) is invalid(overflow) for ButtonTable B, and FocusManager ignore all focus move events. + if not self.selected then + self.selected = { x = 1, y = 1 } + else + self.selected = {x = self.selected.x, y = self.selected.y } + end +end + function FocusManager:isAlternativeKey(key) for _, seq in pairs(self.extra_key_events) do for _, oneseq in ipairs(seq) do @@ -219,7 +231,7 @@ function FocusManager:onFocusMove(args) self.selected.y = self.selected.y + dy self.selected.x = self.selected.x + dx end - logger.dbg("Cursor position : ".. self.selected.y .." : "..self.selected.x) + logger.dbg("FocusManager cursor position is:", self.selected.x, ",", self.selected.y) if self.layout[self.selected.y][self.selected.x] ~= current_item or not self.layout[self.selected.y][self.selected.x].is_inactive then @@ -257,7 +269,7 @@ function FocusManager:moveFocusTo(x, y, focus_flags) target_item = self.layout[y][x] end if target_item then - logger.dbg("Move focus position to: " .. y .. ", " .. x) + logger.dbg("FocusManager: Move focus position to:", y, ",", x) self.selected.x = x self.selected.y = y -- widget create new layout on update, previous may be removed from new layout. @@ -351,7 +363,7 @@ function FocusManager:_sendGestureEventToFocusedWidget(gesture) point.y = point.y + point.h / 2 point.w = 0 point.h = 0 - logger.dbg("FocusManager: Send " .. gesture .. " to " .. point.x .. ", " .. point.y) + logger.dbg("FocusManager: Send", gesture, "to", point.x , ",", point.y) UIManager:sendEvent(Event:new("Gesture", { ges = gesture, pos = point, diff --git a/frontend/ui/widget/openwithdialog.lua b/frontend/ui/widget/openwithdialog.lua index 996655b94..ca19749cd 100644 --- a/frontend/ui/widget/openwithdialog.lua +++ b/frontend/ui/widget/openwithdialog.lua @@ -23,7 +23,6 @@ local OpenWithDialog = InputDialog:extend{} function OpenWithDialog:init() -- init title and buttons in base class InputDialog.init(self) - self.element_width = math.floor(self.width * 0.9) self.radio_button_table = RadioButtonTable:new{ @@ -42,6 +41,7 @@ function OpenWithDialog:init() end end } + self.layout = {self.layout[#self.layout]} -- keep bottom buttons self:mergeLayoutInVertical(self.radio_button_table, #self.layout) -- before bottom buttons self._input_widget = self.radio_button_table @@ -86,13 +86,11 @@ function OpenWithDialog:init() text = _("Always use this engine for this file"), parent = self, } - table.insert(self.layout, #self.layout, {self._check_file_button}) -- before bottom buttons self:addWidget(self._check_file_button) self._check_global_button = self._check_global_button or CheckButton:new{ text = _("Always use this engine for file type"), parent = self, } - table.insert(self.layout, #self.layout, {self._check_global_button}) -- before bottom buttons self:addWidget(self._check_global_button) self.dialog_frame = FrameContainer:new{ diff --git a/frontend/ui/widget/radiobuttonwidget.lua b/frontend/ui/widget/radiobuttonwidget.lua index a86a6b34d..13fbaf0af 100644 --- a/frontend/ui/widget/radiobuttonwidget.lua +++ b/frontend/ui/widget/radiobuttonwidget.lua @@ -3,10 +3,10 @@ local ButtonTable = require("ui/widget/buttontable") local CenterContainer = require("ui/widget/container/centercontainer") local Device = require("device") local FrameContainer = require("ui/widget/container/framecontainer") +local FocusManager = require("ui/widget/focusmanager") local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") local HorizontalGroup = require("ui/widget/horizontalgroup") -local InputContainer = require("ui/widget/container/inputcontainer") local MovableContainer = require("ui/widget/container/movablecontainer") local RadioButtonTable = require("ui/widget/radiobuttontable") local Size = require("ui/size") @@ -17,7 +17,7 @@ local WidgetContainer = require("ui/widget/container/widgetcontainer") local _ = require("gettext") local Screen = Device.screen -local RadioButtonWidget = InputContainer:new{ +local RadioButtonWidget = FocusManager:new{ title_text = "", info_text = nil, width = nil, @@ -49,27 +49,24 @@ function RadioButtonWidget:init() self.width = math.floor(math.min(self.screen_width, self.screen_height) * self.width_factor) end if Device:hasKeys() then - self.key_events = { - Close = { {Device.input.group.Back}, doc = "close widget" } - } + self.key_events.Close = { {Device.input.group.Back}, doc = "close widget" } end - if Device:isTouchDevice() then - self.ges_events = { - TapClose = { - GestureRange:new{ - ges = "tap", - range = Geom:new{ - w = self.screen_width, - h = self.screen_height, - } - }, + self.ges_events = { + TapClose = { + GestureRange:new{ + ges = "tap", + range = Geom:new{ + w = self.screen_width, + h = self.screen_height, + } }, - } - end + }, + } self:update() end function RadioButtonWidget:update() + self.layout = {} if self.default_provider then local row, col = self:getButtonIndex(self.default_provider) self.radio_buttons[row][col].text = self.radio_buttons[row][col].text .. "\u{A0}\u{A0}★" @@ -83,6 +80,7 @@ function RadioButtonWidget:update() parent = self, face = self.face, } + self:mergeLayoutInVertical(value_widget) local value_group = HorizontalGroup:new{ align = "center", value_widget, @@ -149,7 +147,7 @@ function RadioButtonWidget:update() zero_sep = true, show_parent = self, } - + self:mergeLayoutInVertical(ok_cancel_buttons) local vgroup = VerticalGroup:new{ align = "left", title_bar, diff --git a/frontend/ui/widget/sortwidget.lua b/frontend/ui/widget/sortwidget.lua index 99d850d8d..ef47500ce 100644 --- a/frontend/ui/widget/sortwidget.lua +++ b/frontend/ui/widget/sortwidget.lua @@ -6,6 +6,7 @@ local CenterContainer = require("ui/widget/container/centercontainer") local CheckMark = require("ui/widget/checkmark") local Device = require("device") local Font = require("ui/font") +local FocusManager = require("ui/widget/focusmanager") local FrameContainer = require("ui/widget/container/framecontainer") local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") @@ -34,20 +35,18 @@ local SortItemWidget = InputContainer:new{ function SortItemWidget:init() self.dimen = Geom:new{w = self.width, h = self.height} - if Device:isTouchDevice() then - self.ges_events.Tap = { - GestureRange:new{ - ges = "tap", - range = self.dimen, - } + self.ges_events.Tap = { + GestureRange:new{ + ges = "tap", + range = self.dimen, } - self.ges_events.Hold = { - GestureRange:new{ - ges = "hold", - range = self.dimen, - } + } + self.ges_events.Hold = { + GestureRange:new{ + ges = "hold", + range = self.dimen, } - end + } local item_checkable = false local item_checked = self.item.checked @@ -69,6 +68,8 @@ function SortItemWidget:init() self[1] = FrameContainer:new{ padding = 0, bordersize = 0, + focusable = true, + focus_border_size = Size.border.thin, LeftContainer:new{ -- needed only for auto UI mirroring dimen = Geom:new{ w = self.width, @@ -113,7 +114,7 @@ function SortItemWidget:onHold() return true end -local SortWidget = InputContainer:new{ +local SortWidget = FocusManager:new{ title = "", width = nil, height = nil, @@ -125,6 +126,7 @@ local SortWidget = InputContainer:new{ } function SortWidget:init() + self.layout = {} -- no item is selected on start self.marked = 0 self.orig_item_table = nil @@ -134,11 +136,9 @@ function SortWidget:init() h = self.height or Screen:getHeight(), } if Device:hasKeys() then - self.key_events = { - --don't get locked in on non touch devices - AnyKeyPressed = { { Device.input.group.Any }, - seqtext = "any key", doc = "close dialog" } - } + self.key_events.Close = { { Device.input.group.Back }, doc = "close dialog" } + self.key_events.NextPage = { { Device.input.group.PgFwd}, doc = "next page"} + self.key_events.PrevPage = { { Device.input.group.PgBack}, doc = "prev page"} end if Device:isTouchDevice() then self.ges_events.Swipe = { @@ -257,6 +257,10 @@ function SortWidget:init() self.footer_last_down, self.footer_ok, } + table.insert(self.layout, { + self.footer_cancel, + self.footer_ok, + }) local bottom_line = LineWidget:new{ dimen = Geom:new{ w = self.item_width, h = Size.line.thick }, background = Blitbuffer.COLOR_DARK_GRAY, @@ -364,6 +368,7 @@ end -- make sure self.item_margin and self.item_height are set before calling this function SortWidget:_populateItems() self.main_content:clear() + self.layout = { self.layout[#self.layout] } -- keep footer local idx_offset = (self.show_page - 1) * self.items_per_page local page_last if idx_offset + self.items_per_page <= #self.item_table then @@ -377,16 +382,18 @@ function SortWidget:_populateItems() if idx == self.marked then invert_status = true end + local item = SortItemWidget:new{ + height = self.item_height, + width = self.item_width, + item = self.item_table[idx], + invert = invert_status, + index = idx, + show_parent = self, + } + table.insert(self.layout, #self.layout, {item}) table.insert( self.main_content, - SortItemWidget:new{ - height = self.item_height, - width = self.item_width, - item = self.item_table[idx], - invert = invert_status, - index = idx, - show_parent = self, - } + item ) end self.footer_page:setText(T(_("Page %1 of %2"), self.show_page, self.pages), self.footer_center_width) @@ -420,10 +427,6 @@ function SortWidget:_populateItems() end) end -function SortWidget:onAnyKeyPressed() - return self:onClose() -end - function SortWidget:onNextPage() self:nextPage() return true