diff --git a/frontend/ui/gesturedetector.lua b/frontend/ui/gesturedetector.lua index c58b3a93f..02af6557d 100644 --- a/frontend/ui/gesturedetector.lua +++ b/frontend/ui/gesturedetector.lua @@ -51,10 +51,12 @@ Current detectable gestures: * pinch * spread * rotate + * hold_pan * double_tap * inward_pan * outward_pan * pan_release + * hold_release * two_finger_tap * two_finger_pan * two_finger_swipe @@ -474,6 +476,7 @@ function GestureDetector:handlePan(tev) y = self.last_tevs[slot].y, w = 0, h = 0, } + --DEBUG(pan_ev.ges, pan_ev.pos, pan_ev.direction, pan_ev.distance, "detected") return pan_ev end end @@ -576,8 +579,7 @@ function GestureDetector:holdState(tev, hold) }, time = tev.timev, } - end - if tev.id == -1 and self.last_tevs[slot] ~= nil then + elseif tev.id == -1 and self.last_tevs[slot] ~= nil then -- end of hold, signal hold release DEBUG("hold_release detected in slot", slot) local last_x = self.last_tevs[slot].x @@ -592,6 +594,10 @@ function GestureDetector:holdState(tev, hold) }, time = tev.timev, } + else + local ges_ev = self:handlePan(tev) + ges_ev.ges = "hold_pan" + return ges_ev end end diff --git a/frontend/ui/reader/readerdictionary.lua b/frontend/ui/reader/readerdictionary.lua index e6f719f21..2c0dd74ce 100644 --- a/frontend/ui/reader/readerdictionary.lua +++ b/frontend/ui/reader/readerdictionary.lua @@ -39,6 +39,10 @@ end function ReaderDictionary:stardictLookup(word) DEBUG("lookup word:", word) if word then + -- strip punctuation characters around selected word + word = string.gsub(word, "^%p+", '') + word = string.gsub(word, "%p+$", '') + DEBUG("stripped word:", word) -- escape quotes and other funny characters in word local std_out = io.popen("./sdcv -nj "..("%q"):format(word), "r") local results_str = std_out:read("*all") @@ -67,6 +71,7 @@ function ReaderDictionary:showDict(result) -- }) if result then UIManager:show(DictQuickLookup:new{ + ui = self.ui, dict = result.dict, definition = result.definition, id = result.ID, diff --git a/frontend/ui/reader/readerhighlight.lua b/frontend/ui/reader/readerhighlight.lua index b25d06890..775d1ed4d 100644 --- a/frontend/ui/reader/readerhighlight.lua +++ b/frontend/ui/reader/readerhighlight.lua @@ -1,3 +1,4 @@ +require "ui/widget/buttontable" ReaderHighlight = InputContainer:new{} @@ -33,6 +34,27 @@ function ReaderHighlight:initGesListener() } } }, + HoldRelease = { + GestureRange:new{ + ges = "hold_release", + range = Geom:new{ + x = 0, y = 0, + w = Screen:getWidth(), + h = Screen:getHeight() + } + } + }, + HoldPan = { + GestureRange:new{ + ges = "hold_pan", + range = Geom:new{ + x = 0, y = 0, + w = Screen:getWidth(), + h = Screen:getHeight() + }, + rate = 2.0, + } + }, } end @@ -44,91 +66,302 @@ function ReaderHighlight:onSetDimensions(dimen) end function ReaderHighlight:onTap(arg, ges) - if self.view.highlight.rect then - self.view.highlight.rect = nil + local function inside_box(box) + local pos = self.view:screenToPageTransform(ges.pos) + local x, y = pos.x, pos.y + if box.x <= x and box.y <= y + and box.x + box.w >= x + and box.y + box.h >= y then + return true + end + return false + end + if self.hold_pos then + self.view.highlight.temp[self.hold_pos.page] = nil UIManager:setDirty(self.dialog, "partial") + self.hold_pos = nil return true end + local pages = self.view:getCurrentPageList() + for _, page in pairs(pages) do + local items = self.view.highlight.saved[page] + if not items then items = {} end + for i = 1, #items do + for j = 1, #items[i].boxes do + if inside_box(items[i].boxes[j]) then + DEBUG("Tap on hightlight") + self.edit_highlight_dialog = ButtonTable:new{ + buttons = { + { + { + text="Delete", + callback = function() self:deleteHighlight(page, i) end, + }, + { + text="Edit", + callback = function() self:editHighlight() end, + }, + }, + }, + tap_close_callback = function() self.ui:handleEvent(Event:new("Tap")) end, + } + UIManager:show(self.edit_highlight_dialog) + return true + end + end + end + end end function ReaderHighlight:onHold(arg, ges) - self.pos = self.view:screenToPageTransform(ges.pos) - DEBUG("hold position in page", self.pos) - if not self.pos then + self.hold_pos = self.view:screenToPageTransform(ges.pos) + DEBUG("hold position in page", self.hold_pos) + if not self.hold_pos then DEBUG("not inside page area") return true end - local text_boxes = self.ui.document:getTextBoxes(self.pos.page) - --DEBUG("page text", text_boxes) + self.page_boxes = self.ui.document:getTextBoxes(self.hold_pos.page) + --DEBUG("page text", page_boxes) - if not text_boxes or #text_boxes == 0 then - DEBUG("no text box detected") + if not self.page_boxes or #self.page_boxes == 0 then + DEBUG("no page boxes detected") return true end - self.word_info = self:getWordFromBoxes(text_boxes, self.pos) - DEBUG("hold word info in page", self.word_info) - if self.word_info then - -- if we extracted text directly - if self.word_info.word then - self.ui:handleEvent(Event:new("LookupWord", self.word_info.word)) - -- or we will do OCR - else - UIManager:scheduleIn(0.1, function() - local word_box = self.word_info.box - word_box.x = word_box.x - math.floor(word_box.h * 0.2) - word_box.y = word_box.y - math.floor(word_box.h * 0.4) - word_box.w = word_box.w + math.floor(word_box.h * 0.4) - word_box.h = word_box.h + math.floor(word_box.h * 0.6) - local word = self.ui.document:getOCRWord(self.pos.page, word_box) - DEBUG("OCRed word:", word) - self.ui:handleEvent(Event:new("LookupWord", word)) - end) - end - - local screen_rect = self.view:pageToScreenTransform(self.pos.page, self.word_info.box) - DEBUG("highlight word rect", screen_rect) - if screen_rect then - screen_rect.x = screen_rect.x - screen_rect.h * 0.2 - screen_rect.y = screen_rect.y - screen_rect.h * 0.2 - screen_rect.w = screen_rect.w + screen_rect.h * 0.4 - screen_rect.h = screen_rect.h + screen_rect.h * 0.4 - self.view.highlight.rect = screen_rect - UIManager:setDirty(self.dialog, "partial") - end + self.selected_word = self:getWordFromPosition(self.page_boxes, self.hold_pos) + DEBUG("selected word:", self.selected_word) + if self.selected_word then + local boxes = {} + table.insert(boxes, self.selected_word.box) + self.view.highlight.temp[self.hold_pos.page] = boxes + UIManager:setDirty(self.dialog, "partial") end return true end -function ReaderHighlight:getWordFromBoxes(boxes, pos) - local function ges_inside(x0, y0, x1, y1) +function ReaderHighlight:onHoldPan(arg, ges) + self.selected_word = nil + if not self.page_boxes or #self.page_boxes == 0 then + DEBUG("no page boxes detected") + return true + end + self.holdpan_pos = self.view:screenToPageTransform(ges.pos) + DEBUG("holdpan position in page", self.holdpan_pos) + self.selected_text = self:getTextFromPositions(self.page_boxes, self.hold_pos, self.holdpan_pos) + DEBUG("selected text:", self.selected_text) + if self.selected_text then + self.view.highlight.temp[self.hold_pos.page] = self.selected_text.boxes + UIManager:setDirty(self.dialog, "partial") + end +end + +function ReaderHighlight:onHoldRelease(arg, ges) + if self.selected_word then + -- if we extracted text directly + if self.selected_word.word then + self.ui:handleEvent(Event:new("LookupWord", self.selected_word.word)) + -- or we will do OCR + else + local word_box = self.selected_word.box + word_box.x = word_box.x - math.floor(word_box.h * 0.1) + word_box.y = word_box.y - math.floor(word_box.h * 0.2) + word_box.w = word_box.w + math.floor(word_box.h * 0.2) + word_box.h = word_box.h + math.floor(word_box.h * 0.4) + local word = self.ui.document:getOCRWord(self.hold_pos.page, word_box) + DEBUG("OCRed word:", word) + self.ui:handleEvent(Event:new("LookupWord", word)) + end + self.selected_word = nil + elseif self.selected_text then + DEBUG("show highlight dialog") + self.highlight_dialog = ButtonTable:new{ + buttons = { + { + { + text="Highlight", + callback = function() self:saveHighlight() end, + }, + { + text="Add Note", + callback = function() self:addNote() end, + }, + }, + { + { + text="Share", + callback = function() self:shareHighlight() end, + }, + { + text="More", + callback = function() self:moreAction() end, + }, + }, + }, + tap_close_callback = function() self.ui:handleEvent(Event:new("Tap")) end, + } + UIManager:show(self.highlight_dialog) + end + return true +end + +function ReaderHighlight:saveHighlight() + DEBUG("save highlight") + UIManager:close(self.highlight_dialog) + local page = self.hold_pos.page + if self.hold_pos and self.selected_text then + if not self.view.highlight.saved[page] then + self.view.highlight.saved[page] = {} + end + local hl_item = {} + hl_item["text"] = self.selected_text.text + hl_item["boxes"] = self.selected_text.boxes + table.insert(self.view.highlight.saved[page], hl_item) + end + --DEBUG("saved hightlights", self.view.highlight.saved[page]) +end + +function ReaderHighlight:addNote() + DEBUG("add Note") + UIManager:close(self.highlight_dialog) +end + +function ReaderHighlight:shareHighlight() + DEBUG("share highlight") + UIManager:close(self.highlight_dialog) +end + +function ReaderHighlight:moreAction() + DEBUG("more action") + UIManager:close(self.highlight_dialog) +end + +function ReaderHighlight:deleteHighlight(page, i) + DEBUG("delete highlight") + UIManager:close(self.edit_highlight_dialog) + table.remove(self.view.highlight.saved[page], i) +end + +function ReaderHighlight:editHighlight() + DEBUG("edit highlight") + UIManager:close(self.edit_highlight_dialog) +end + +--[[ +get index of nearest word box around pos +--]] +local function getWordBoxIndices(boxes, pos) + local function inside_box(box) local x, y = pos.x, pos.y - if x0 ~= nil and y0 ~= nil and x1 ~= nil and y1 ~= nil then - if x0 <= x and y0 <= y and x1 >= x and y1 >= y then - return true - end + if box.x0 <= x and box.y0 <= y and box.x1 >= x and box.y1 >= y then + return true end return false end - + local function box_distance(i, j) + local wb = boxes[i][j] + if inside_box(wb) then + return 0 + else + local x0, y0 = pos.x, pos.y + local x1, y1 = (wb.x0 + wb.x1) / 2, (wb.y0 + wb.y1) / 2 + return (x0 - x1)*(x0 - x1) + (y0 - y1)*(y0 - y1) + end + end + + local m, n = 1, 1 for i = 1, #boxes do - local l = boxes[i] - if ges_inside(l.x0, l.y0, l.x1, l.y1) then - --DEBUG("line box", l.x0, l.y0, l.x1, l.y1) - for j = 1, #boxes[i] do - local w = boxes[i][j] - if ges_inside(w.x0, w.y0, w.x1, w.y1) then - local box = Geom:new{ - x = w.x0, y = w.y0, - w = w.x1 - w.x0, - h = w.y1 - w.y0, - } - return { - word = w.word, - box = box, - } - end -- end if inside word box - end -- end for each word - end -- end if inside line box - end -- end for each line + for j = 1, #boxes[i] do + if box_distance(i, j) < box_distance(m, n) then + m, n = i, j + end + end + end + return m, n +end + +--[[ +get word and word box around pos +--]] +function ReaderHighlight:getWordFromPosition(boxes, pos) + local i, j = getWordBoxIndices(boxes, pos) + local lb = boxes[i] + local wb = boxes[i][j] + if lb and wb then + local box = Geom:new{ + x = wb.x0, y = lb.y0, + w = wb.x1 - wb.x0, + h = lb.y1 - lb.y0, + } + return { + word = wb.word, + box = box, + } + end +end + +--[[ +get text and text boxes between pos0 and pos1 +--]] +function ReaderHighlight:getTextFromPositions(boxes, pos0, pos1) + local line_text = "" + local line_boxes = {} + local i_start, j_start = getWordBoxIndices(boxes, pos0) + local i_stop, j_stop = getWordBoxIndices(boxes, pos1) + if i_start == i_stop and j_start > j_stop or i_start > i_stop then + i_start, i_stop = i_stop, i_start + j_start, j_stop = j_stop, j_start + end + for i = i_start, i_stop do + -- insert line words + local j0 = i > i_start and 1 or j_start + local j1 = i < i_stop and #boxes[i] or j_stop + for j = j0, j1 do + local word = boxes[i][j].word + if word then + -- if last character of this word is an ascii char then append a space + local space = (word:match("[%z\194-\244][\128-\191]*$") or j == j1) + and "" or " " + line_text = line_text..word..space + end + end + -- insert line box + local lb = boxes[i] + if i > i_start and i < i_stop then + local line_box = Geom:new{ + x = lb.x0, y = lb.y0, + w = lb.x1 - lb.x0, + h = lb.y1 - lb.y0, + } + table.insert(line_boxes, line_box) + elseif i == i_start and i < i_stop then + local wb = boxes[i][j_start] + local line_box = Geom:new{ + x = wb.x0, y = lb.y0, + w = lb.x1 - wb.x0, + h = lb.y1 - lb.y0, + } + table.insert(line_boxes, line_box) + elseif i > i_start and i == i_stop then + local wb = boxes[i][j_stop] + local line_box = Geom:new{ + x = lb.x0, y = lb.y0, + w = wb.x1 - lb.x0, + h = lb.y1 - lb.y0, + } + table.insert(line_boxes, line_box) + elseif i == i_start and i == i_stop then + local wb_start = boxes[i][j_start] + local wb_stop = boxes[i][j_stop] + local line_box = Geom:new{ + x = wb_start.x0, y = lb.y0, + w = wb_stop.x1 - wb_start.x0, + h = lb.y1 - lb.y0, + } + table.insert(line_boxes, line_box) + end + end + return { + text = line_text, + boxes = line_boxes, + } end diff --git a/frontend/ui/reader/readerview.lua b/frontend/ui/reader/readerview.lua index f5dfbc7de..0b8386ced 100644 --- a/frontend/ui/reader/readerview.lua +++ b/frontend/ui/reader/readerview.lua @@ -18,8 +18,12 @@ ReaderView = OverlapGroup:new{ outer_page_color = 0, -- hightlight highlight = { - drawer = "marker" -- show as inverted block instead of underline + temp_drawer = "invert", + temp = {}, + saved_drawer = "lighten", + saved = {}, }, + highlight_visible = true, -- PDF/DjVu continuous paging page_scroll = nil, page_bgcolor = 0, @@ -101,9 +105,14 @@ function ReaderView:paintTo(bb, x, y) ) end - -- draw highlight - if self.highlight.rect then - self:drawHightlight(bb, x, y, self.highlight.rect) + -- draw saved highlight + if self.highlight_visible then + self:drawSavedHighlight(bb, x, y) + end + + -- draw temporary highlight + if self.highlight.temp then + self:drawTempHighlight(bb, x, y) end -- paint dogear @@ -190,6 +199,20 @@ function ReaderView:drawScrollPages(bb, x, y) end) end +function ReaderView:getCurrentPageList() + local pages = {} + if self.ui.document.info.has_pages then + if self.page_scroll then + for _, state in ipairs(self.page_states) do + table.insert(pages, state.page) + end + else + table.insert(pages, self.state.page) + end + end + return pages +end + function ReaderView:getScrollPagePosition(pos) local x_s, y_s = pos.x, pos.y local x_p, y_p = nil, nil @@ -213,7 +236,7 @@ end function ReaderView:getScrollPageRect(page, rect_p) local rect_s = Geom:new{} for _, state in ipairs(self.page_states) do - local trans_p = rect_p:copy() + local trans_p = Geom:new(rect_p) trans_p:transformByScale(state.zoom, state.zoom) if page == state.page and state.visible_area:contains(trans_p) then rect_s.x = rect_s.x + state.offset.x + trans_p.x - state.visible_area.x @@ -263,7 +286,7 @@ end function ReaderView:getSinglePageRect(rect_p) local rect_s = Geom:new{} - local trans_p = rect_p:copy() + local trans_p = Geom:new(rect_p) trans_p:transformByScale(self.state.zoom, self.state.zoom) if self.visible_area:contains(trans_p) then rect_s.x = self.state.offset.x + trans_p.x - self.visible_area.x @@ -292,17 +315,45 @@ function ReaderView:drawScrollView(bb, x, y) self.state.pos) end -function ReaderView:drawHightlight(bb, x, y, rect) +function ReaderView:drawTempHighlight(bb, x, y) + for page, boxes in pairs(self.highlight.temp) do + for i = 1, #boxes do + local rect = self:pageToScreenTransform(page, boxes[i]) + if rect then + self:drawHighlightRect(bb, x, y, rect, self.highlight.temp_drawer) + end + end + end +end + +function ReaderView:drawSavedHighlight(bb, x, y) + local pages = self:getCurrentPageList() + for _, page in pairs(pages) do + local items = self.highlight.saved[page] + if not items then items = {} end + for i = 1, #items do + for j = 1, #items[i].boxes do + local rect = self:pageToScreenTransform(page, items[i].boxes[j]) + if rect then + self:drawHighlightRect(bb, x, y, rect, self.highlight.saved_drawer) + end + end -- end for each box + end -- end for each hightlight + end -- end for each page +end + +function ReaderView:drawHighlightRect(bb, x, y, rect, drawer) local x, y, w, h = rect.x, rect.y, rect.w, rect.h - self.highlight.drawer = self.highlight.drawer or "underscore" - if self.highlight.drawer == "underscore" then + if drawer == "underscore" then self.highlight.line_width = self.highlight.line_width or 2 self.highlight.line_color = self.highlight.line_color or 5 bb:paintRect(x, y+h-1, w, self.highlight.line_width, self.highlight.line_color) - elseif self.highlight.drawer == "marker" then + elseif drawer == "lighten" then + bb:lightenRect(x, y, w, h, 0.1) + elseif drawer == "invert" then bb:invertRect(x, y, w, h) end end @@ -438,6 +489,7 @@ function ReaderView:onReadSettings(config) page_scroll = self.document.configurable.page_scroll end self.page_scroll = page_scroll == 1 and true or false + self.highlight.saved = config:readSetting("highlight") or {} end function ReaderView:onPageUpdate(new_page_no) @@ -498,4 +550,5 @@ function ReaderView:onCloseDocument() self.ui.doc_settings:saveSetting("render_mode", self.render_mode) self.ui.doc_settings:saveSetting("screen_mode", self.screen_mode) self.ui.doc_settings:saveSetting("gamma", self.state.gamma) + self.ui.doc_settings:saveSetting("highlight", self.highlight.saved) end diff --git a/frontend/ui/widget/buttontable.lua b/frontend/ui/widget/buttontable.lua new file mode 100644 index 000000000..842e16564 --- /dev/null +++ b/frontend/ui/widget/buttontable.lua @@ -0,0 +1,90 @@ +require "ui/widget/base" +require "ui/widget/line" + +ButtonTable = InputContainer:new{ + buttons = { + { + {text="OK", callback=nil}, + {text="Cancel", callback=nil}, + }, + }, + tap_close_callback = nil, +} + +function ButtonTable:init() + if Device:hasKeyboard() then + key_events = { + AnyKeyPressed = { { Input.group.Any }, + seqtext = "any key", doc = _("close dialog") } + } + else + self.ges_events.TapClose = { + GestureRange:new{ + ges = "tap", + range = Geom:new{ + x = 0, y = 0, + w = Screen:getWidth(), + h = Screen:getHeight(), + } + } + } + end + local vertical_group = VerticalGroup:new{} + local horizontal_sep = LineWidget:new{ + background = 8, + dimen = Geom:new{ + w = Screen:getWidth()*0.9, + h = 1, + } + } + for i = 1, #self.buttons do + local horizontal_group = HorizontalGroup:new{} + local line = self.buttons[i] + for j = 1, #line do + local button = Button:new{ + text = line[j].text, + callback = line[j].callback, + width = Screen:getWidth()*0.9/#line, + bordersize = 0, + text_font_face = "cfont", + text_font_size = scaleByDPI(18), + } + local button_dim = button:getSize() + local vertical_sep = LineWidget:new{ + background = 8, + dimen = Geom:new{ + w = scaleByDPI(1), + h = button_dim.h, + } + } + table.insert(horizontal_group, button) + if j < #line then + table.insert(horizontal_group, vertical_sep) + end + end -- end for each button + table.insert(vertical_group, horizontal_group) + if i < #self.buttons then + table.insert(vertical_group, VerticalSpan:new{ width = scaleByDPI(2) }) + table.insert(vertical_group, horizontal_sep) + table.insert(vertical_group, VerticalSpan:new{ width = scaleByDPI(2) }) + end + end -- end for each button line + self[1] = CenterContainer:new{ + dimen = Screen:getSize(), + FrameContainer:new{ + vertical_group, + background = 0, + bordersize = 2, + radius = 7, + padding = 2, + }, + } +end + +function ButtonTable:onTapClose() + UIManager:close(self) + if self.tap_close_callback then + self.tap_close_callback() + end + return true +end diff --git a/frontend/ui/widget/dict.lua b/frontend/ui/widget/dict.lua index 1a8922ea5..3cd7b0c42 100644 --- a/frontend/ui/widget/dict.lua +++ b/frontend/ui/widget/dict.lua @@ -64,5 +64,6 @@ end function DictQuickLookup:onTapClose() UIManager:close(self) + self.ui:handleEvent(Event:new("Tap")) return true end