From 46d197954b56fc65a8b0e9c7a63e2a6ebe3c2480 Mon Sep 17 00:00:00 2001 From: HW Date: Wed, 11 Apr 2012 22:52:48 +0200 Subject: [PATCH] put highlight implementation into unireader --- crereader.lua | 2 + djvureader.lua | 602 +--------------------------------------------- pdfreader.lua | 22 ++ unireader.lua | 630 ++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 648 insertions(+), 608 deletions(-) diff --git a/crereader.lua b/crereader.lua index 725883872..a2f67b0e7 100644 --- a/crereader.lua +++ b/crereader.lua @@ -241,6 +241,8 @@ function CREReader:adjustCreReaderCommands() self.commands:del(KEY_D, MOD_ALT, "D") self.commands:del(KEY_F, MOD_SHIFT, "F") self.commands:del(KEY_F, MOD_ALT, "F") + self.commands:del(KEY_N, nil, "N") -- highlight + self.commands:del(KEY_N, MOD_SHIFT, "N") -- show highlights -- overwrite commands self.commands:add(KEY_PGFWD, MOD_SHIFT_OR_ALT, ">", diff --git a/djvureader.lua b/djvureader.lua index 9d92229c6..38efbedd8 100644 --- a/djvureader.lua +++ b/djvureader.lua @@ -21,21 +21,6 @@ end function DJVUReader:adjustDjvuReaderCommand() self.commands:del(KEY_J, MOD_SHIFT, "J") self.commands:del(KEY_K, MOD_SHIFT, "K") - - self.commands:add(KEY_N, nil, "N", - "start highlight mode", - function(unireader) - unireader:startHighLightMode() - unireader:goto(unireader.pageno) - end - ) - self.commands:add(KEY_N, MOD_SHIFT, "N", - "display all highlights", - function(unireader) - unireader:showHighLight() - unireader:goto(unireader.pageno) - end - ) end @@ -49,7 +34,7 @@ end -- down left conner, i.e. y is upside down. This method -- only transform these coordinates. ---------------------------------------------------- -function DJVUReader:_rectCoordTransform(x0, y0, x1, y1) +function DJVUReader:rectCoordTransform(x0, y0, x1, y1) return self.offset_x + x0 * self.globalzoom, self.offset_y + self.cur_full_height - (y1 * self.globalzoom), @@ -57,587 +42,6 @@ function DJVUReader:_rectCoordTransform(x0, y0, x1, y1) (y1 - y0) * self.globalzoom end --- make sure the whole word can be seen in screen -function DJVUReader:_isEntireWordInScreenRange(w) - return self:_isEntireWordInScreenHeightRange(w) and - self:_isEntireWordInScreenWidthRange(w) +function DJVUReader:getText(pageno) + return self.doc:getPageText(pageno) end - --- y axel in djvulibre starts from bottom -function DJVUReader:_isEntireWordInScreenHeightRange(w) - return (w ~= nil) and - (self.cur_full_height - (w.y1 * self.globalzoom) >= - -self.offset_y) and - (self.cur_full_height - (w.y0 * self.globalzoom) <= - -self.offset_y + G_height) -end - -function DJVUReader:_isEntireWordInScreenWidthRange(w) - return (w ~= nil) and - (w.x0 * self.globalzoom >= -self.offset_x) and - (w.x1 * self.globalzoom <= -self.offset_x + G_width) -end - --- make sure at least part of the word can be seen in screen -function DJVUReader:_isWordInScreenRange(w) - return (w ~= nil) and - (self.cur_full_height - (w.y0 * self.globalzoom) >= - -self.offset_y) and - (self.cur_full_height - (w.y1 * self.globalzoom) <= - -self.offset_y + G_height) and - (w.x1 * self.globalzoom >= -self.offset_x) and - (w.x0 * self.globalzoom <= -self.offset_x + G_width) -end - -function DJVUReader:toggleTextHighLight(word_list) - for _,text_item in ipairs(word_list) do - for _,line_item in ipairs(text_item) do - -- make sure that line is in screen range - if self:_isEntireWordInScreenHeightRange(line_item) then - local x, y, w, h = self:_rectCoordTransform( - line_item.x0, line_item.y0, - line_item.x1, line_item.y1) - -- slightly enlarge the highlight height - -- for better viewing experience - x = x - y = y - h * 0.1 - w = w - h = h * 1.2 - - self.highlight.drawer = self.highlight.drawer or "underscore" - if self.highlight.drawer == "underscore" then - self.highlight.line_width = self.highlight.line_width or 2 - self.highlight.line_color = self.highlight.line_color or 5 - fb.bb:paintRect(x, y+h-1, w, - self.highlight.line_width, - self.highlight.line_color) - elseif self.highlight.drawer == "marker" then - fb.bb:invertRect(x, y, w, h) - end - end -- EOF if isEntireWordInScreenHeightRange - end -- EOF for line_item - end -- EOF for text_item -end - -function DJVUReader:_wordIterFromRange(t, l0, w0, l1, w1) - local i = l0 - local j = w0 - 1 - return function() - if i <= l1 then - -- if in line range, loop through lines - if i == l1 then - -- in last line - if j < w1 then - j = j + 1 - else - -- out of range return nil - return nil, nil - end - else - if j < #t[i] then - j = j + 1 - else - -- goto next line - i = i + 1 - j = 1 - end - end - return i, j - end - end -- EOF closure -end - -function DJVUReader:_toggleWordHighLight(t, l, w) - x, y, w, h = self:_rectCoordTransform(t[l][w].x0, t[l].y0, - t[l][w].x1, t[l].y1) - -- slightly enlarge the highlight range for better viewing experience - x = x - w * 0.05 - y = y - h * 0.05 - w = w * 1.1 - h = h * 1.1 - - fb.bb:invertRect(x, y, w, h) -end - -function DJVUReader:_toggleTextHighLight(t, l0, w0, l1, w1) - --print("# toggle range", l0, w0, l1, w1) - -- make sure (l0, w0) is smaller than (l1, w1) - if l0 > l1 then - l0, l1 = l1, l0 - w0, w1 = w1, w0 - elseif l0 == l1 and w0 > w1 then - w0, w1 = w1, w0 - end - - for _l, _w in self:_wordIterFromRange(t, l0, w0, l1, w1) do - if self:_isWordInScreenRange(t[_l][_w]) then - -- blitbuffer module will take care of the out of screen range part. - self:_toggleWordHighLight(t, _l, _w) - end - end -end - --- remember to clear cursor before calling this -function DJVUReader:drawCursorAfterWord(t, l, w) - self.cursor:setHeight((t[l].y1 - t[l].y0) * self.globalzoom) - self.cursor:moveTo( - self.offset_x + t[l][w].x1 * self.globalzoom, - self.offset_y + self.cur_full_height - (t[l].y1 * self.globalzoom)) - self.cursor:draw() -end - -function DJVUReader:drawCursorBeforeWord(t, l, w) - self.cursor:setHeight((t[l].y1 - t[l].y0) - * self.globalzoom) - self.cursor:moveTo( - self.offset_x + t[l][w].x0 * self.globalzoom - self.cursor.w, - self.offset_y + self.cur_full_height - t[l].y1 * self.globalzoom) - self.cursor:draw() -end - -function DJVUReader:startHighLightMode() - local t = self.doc:getPageText(self.pageno) - - local function _findFirstWordInView(t) - for i=1, #t, 1 do - if self:_isEntireWordInScreenRange(t[i][1]) then - return i, 1 - end - end - - return nil - end - - local function _prevWord(t, cur_l, cur_w) - if cur_l == 1 then - if cur_w == 1 then - -- already the first word - return 1, 1 - else - -- in first line, but not first word - return cur_l, cur_w -1 - end - end - - if cur_w <= 1 then - -- first word in current line, goto previous line - return cur_l - 1, #t[cur_l-1] - else - return cur_l, cur_w - 1 - end - end - - local function _nextWord(t, cur_l, cur_w) - if cur_l == #t then - if cur_w == #(t[cur_l]) then - -- already the last word - return cur_l, cur_w - else - -- in last line, but not last word - return cur_l, cur_w + 1 - end - end - - if cur_w < #t[cur_l] then - return cur_l, cur_w + 1 - else - -- last word in current line, move to next line - return cur_l + 1, 1 - end - end - - local function _wordInNextLine(t, cur_l, cur_w) - if cur_l == #t then - -- already in last line, return the last word - return cur_l, #(t[cur_l]) - else - return cur_l + 1, math.min(cur_w, #t[cur_l+1]) - end - end - - local function _wordInPrevLine(t, cur_l, cur_w) - if cur_l == 1 then - -- already in first line, return the first word - return 1, 1 - else - return cur_l - 1, math.min(cur_w, #t[cur_l-1]) - end - end - - local function _isMovingForward(l, w) - return l.cur > l.start or (l.cur == l.start and w.cur > w.start) - end - - local l = {} - local w = {} - - l.start, w.start = _findFirstWordInView(t) - if not l.start then - print("# no text in current view!") - return - end - - l.cur, w.cur = l.start, w.start - l.new, w.new = l.cur, w.cur - local is_meet_start = false - local is_meet_end = false - local running = true - - self.cursor = Cursor:new { - x_pos = t[l.cur][w.cur].x1*self.globalzoom, - y_pos = self.offset_y + (self.cur_full_height - - (t[l.cur][w.cur].y1 * self.globalzoom)), - h = (t[l.cur][w.cur].y1 - t[l.cur][w.cur].y0) * self.globalzoom, - line_width_factor = 4, - } - self.cursor:draw() - fb:refresh(1) - - -- first use cursor to place start pos for highlight - while running do - local ev = input.waitForEvent() - ev.code = adjustKeyEvents(ev) - if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then - if ev.code == KEY_FW_LEFT then - if w.cur == 1 then - w.cur = 0 - w.new = 0 - else - if w.cur == 0 then - -- already at the left end of current line, - -- goto previous line (_prevWord does not understand - -- zero w.cur) - w.cur = 1 - end - l.new, w.new = _prevWord(t, l.cur, w.cur) - end - - self.cursor:clear() - if w.new ~= 0 - and not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) - and self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then - -- word is in previous view - local pageno = self:prevView() - self:goto(pageno) - end - - -- update cursor - if w.cur == 0 then - -- meet line left end, must be handled as special case - if self:_isEntireWordInScreenRange(t[l.cur][1]) then - self:drawCursorBeforeWord(t, l.cur, 1) - end - else - if self:_isEntireWordInScreenRange(t[l.new][w.new]) then - self:drawCursorAfterWord(t, l.new, w.new) - end - end - elseif ev.code == KEY_FW_RIGHT then - if w.cur == 0 then - w.cur = 1 - w.new = 1 - else - l.new, w.new = _nextWord(t, l.cur, w.cur) - if w.new == 1 then - -- Must be come from the right end of previous line, - -- so goto the left end of current line. - w.cur = 0 - w.new = 0 - end - end - - self.cursor:clear() - - local tmp_w = w.new - if w.cur == 0 then - tmp_w = 1 - end - if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) - and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then - local pageno = self:nextView() - self:goto(pageno) - end - - if w.cur == 0 then - -- meet line left end, must be handled as special case - if self:_isEntireWordInScreenRange(t[l.new][1]) then - self:drawCursorBeforeWord(t, l.new, 1) - end - else - if self:_isEntireWordInScreenRange(t[l.new][w.new]) then - self:drawCursorAfterWord(t, l.new, w.new) - end - end - elseif ev.code == KEY_FW_UP then - if w.cur == 0 then - -- goto left end of last line - l.new = math.max(l.cur - 1, 1) - elseif l.cur == 1 and w.cur == 1 then - -- already first word, to the left end of first line - w.new = 0 - else - l.new, w.new = _wordInPrevLine(t, l.cur, w.cur) - end - - self.cursor:clear() - - local tmp_w = w.new - if w.cur == 0 then - tmp_w = 1 - end - if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) - and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then - -- goto next view of current page - local pageno = self:prevView() - self:goto(pageno) - end - - if w.new == 0 then - if self:_isEntireWordInScreenRange(t[l.new][1]) then - self:drawCursorBeforeWord(t, l.new, 1) - end - else - if self:_isEntireWordInScreenRange(t[l.new][w.new]) then - self:drawCursorAfterWord(t, l.new, w.new) - end - end - elseif ev.code == KEY_FW_DOWN then - if w.cur == 0 then - -- on the left end of current line, - -- goto left end of next line - l.new = math.min(l.cur + 1, #t) - else - l.new, w.new = _wordInNextLine(t, l.cur, w.cur) - end - - self.cursor:clear() - - local tmp_w = w.new - if w.cur == 0 then - tmp_w = 1 - end - if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) - and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then - -- goto next view of current page - local pageno = self:nextView() - self:goto(pageno) - end - - if w.cur == 0 then - if self:_isEntireWordInScreenRange(t[l.new][1]) then - self:drawCursorBeforeWord(t, l.new, 1) - end - else - if self:_isEntireWordInScreenRange(t[l.new][w.new]) then - self:drawCursorAfterWord(t, l.new, w.new) - end - end - elseif ev.code == KEY_DEL then - if self.highlight[self.pageno] then - for k, text_item in ipairs(self.highlight[self.pageno]) do - for _, line_item in ipairs(text_item) do - if t[l.cur][w.cur].y0 >= line_item.y0 - and t[l.cur][w.cur].y1 <= line_item.y1 - and t[l.cur][w.cur].x0 >= line_item.x0 - and t[l.cur][w.cur].x1 <= line_item.x1 then - self.highlight[self.pageno][k] = nil - end - end -- EOF for line_item - end -- EOF for text_item - end -- EOF if not highlight table - if #self.highlight[self.pageno] == 0 then - self.highlight[self.pageno] = nil - end - return - elseif ev.code == KEY_FW_PRESS then - if w.cur == 0 then - w.cur = 1 - l.cur, w.cur = _prevWord(t, l.cur, w.cur) - end - l.new, w.new = l.cur, w.cur - l.start, w.start = l.cur, w.cur - running = false - self.cursor:clear() - elseif ev.code == KEY_BACK then - running = false - return - end -- EOF if key event - l.cur, w.cur = l.new, w.new - fb:refresh(1) - end - end -- EOF while - --print("start", l.cur, w.cur, l.start, w.start) - - -- two helper functions for highlight - local function _togglePrevWordHighLight(t, l, w) - l.new, w.new = _prevWord(t, l.cur, w.cur) - - if l.cur == 1 and w.cur == 1 then - is_meet_start = true - -- left end of first line must be handled as special case - w.new = 0 - end - - if w.new ~= 0 and - not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) then - -- word out of left and right sides of current view should - -- not trigger pan by page - if self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then - -- word is in previous view - local pageno = self:prevView() - self:goto(pageno) - end - - local l0 = l.start - local w0 = w.start - local l1 = l.cur - local w1 = w.cur - if _isMovingForward(l, w) then - l0, w0 = _nextWord(t, l0, w0) - l1, w1 = l.new, w.new - end - self:_toggleTextHighLight(t, l0, w0, - l1, w1) - else - self:_toggleWordHighLight(t, l.cur, w.cur) - end - - l.cur, w.cur = l.new, w.new - return l, w, (is_meet_start or false) - end - - local function _toggleNextWordHighLight(t, l, w) - if w.cur == 0 then - w.new = 1 - else - l.new, w.new = _nextWord(t, l.cur, w.cur) - end - if l.new == #t and w.new == #t[#t] then - is_meet_end = true - end - - if not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) then - if self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then - local pageno = self:nextView() - self:goto(pageno) - end - - local tmp_l = l.start - local tmp_w = w.start - if _isMovingForward(l, w) then - tmp_l, tmp_w = _nextWord(t, tmp_l, tmp_w) - end - self:_toggleTextHighLight(t, tmp_l, tmp_w, - l.new, w.new) - else - self:_toggleWordHighLight(t, l.new, w.new) - end - - l.cur, w.cur = l.new, w.new - return l, w, (is_meet_end or false) - end - - - -- go into highlight mode - running = true - while running do - local ev = input.waitForEvent() - ev.code = adjustKeyEvents(ev) - if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then - if ev.code == KEY_FW_LEFT then - is_meet_end = false - if not is_meet_start then - l, w, is_meet_start = _togglePrevWordHighLight(t, l, w) - end - elseif ev.code == KEY_FW_RIGHT then - is_meet_start = false - if not is_meet_end then - l, w, is_meet_end = _toggleNextWordHighLight(t, l, w) - end -- EOF if is not is_meet_end - elseif ev.code == KEY_FW_UP then - is_meet_end = false - if not is_meet_start then - if l.cur == 1 then - -- handle left end of first line as special case - tmp_l = 1 - tmp_w = 0 - else - tmp_l, tmp_w = _wordInPrevLine(t, l.cur, w.cur) - end - while not (tmp_l == l.cur and tmp_w == w.cur) do - l, w, is_meet_start = _togglePrevWordHighLight(t, l, w) - end - end - elseif ev.code == KEY_FW_DOWN then - is_meet_start = false - if not is_meet_end then - if w.cur == 0 then - -- handle left end of first line as special case - tmp_l = math.min(tmp_l + 1, #t) - tmp_w = 1 - else - tmp_l, tmp_w = _wordInNextLine(t, l.new, w.new) - end - while not (tmp_l == l.cur and tmp_w == w.cur) do - l, w, is_meet_end = _toggleNextWordHighLight(t, l, w) - end - end - elseif ev.code == KEY_FW_PRESS then - local l0, w0, l1, w1 - - -- find start and end of highlight text - if _isMovingForward(l, w) then - l0, w0 = _nextWord(t, l.start, w.start) - l1, w1 = l.cur, w.cur - else - l0, w0 = _nextWord(t, l.cur, w.cur) - l1, w1 = l.start, w.start - end - -- remove selection area - self:_toggleTextHighLight(t, l0, w0, l1, w1) - - -- put text into highlight table of current page - local hl_item = {} - local s = "" - local prev_l = l0 - local prev_w = w0 - local l_item = { - x0 = t[l0][w0].x0, - y0 = t[l0].y0, - y1 = t[l0].y1, - } - for _l,_w in self:_wordIterFromRange(t, l0, w0, l1, w1) do - local word_item = t[_l][_w] - if _l > prev_l then - -- in next line, add previous line to highlight item - l_item.x1 = t[prev_l][prev_w].x1 - table.insert(hl_item, l_item) - -- re initialize l_item for new line - l_item = { - x0 = word_item.x0, - y0 = t[_l].y0, - y1 = t[_l].y1, - } - end - s = s .. word_item.word .. " " - prev_l, prev_w = _l, _w - end - -- insert last line of text in line item - l_item.x1 = t[prev_l][prev_w].x1 - table.insert(hl_item, l_item) - hl_item.text = s - - if not self.highlight[self.pageno] then - self.highlight[self.pageno] = {} - end - table.insert(self.highlight[self.pageno], hl_item) - - running = false - elseif ev.code == KEY_BACK then - running = false - end -- EOF if key event - fb:refresh(1) - end - end -- EOF while -end - diff --git a/pdfreader.lua b/pdfreader.lua index 334a69d85..f998d4349 100644 --- a/pdfreader.lua +++ b/pdfreader.lua @@ -30,3 +30,25 @@ function PDFReader:open(filename) end return true end + +-----------[ highlight support ]---------- + +function PDFReader:rectCoordTransform(x0, y0, x1, y1) + return + x0 * self.globalzoom, + y1 * self.globalzoom - self.offset_y, + x1 - x0, + y1 - y0 +end + +function PDFReader:getText(pageno) + local ok, page = pcall(self.doc.openPage, self.doc, pageno) + if not ok then + -- TODO: error handling + return nil + end + local text = page:getPageText() + print(dump(text)) + page:close() + return text +end diff --git a/unireader.lua b/unireader.lua index 8b9e0887f..89ac81773 100644 --- a/unireader.lua +++ b/unireader.lua @@ -112,23 +112,620 @@ function UniReader:init() self:addAllCommands() end ----------------------------------------------------- --- You need to overwrite following two methods if your --- reader supports highlight feature. ----------------------------------------------------- +-----------[ highlight support ]---------- -function UniReader:startHighLightMode() - return +---------------------------------------------------- +-- Given coordinates of four conners and return +-- coordinate of upper left conner with with and height +---------------------------------------------------- +function UniReader:rectCoordTransform(x0, y0, x1, y1) + return + x0 * self.globalzoom, + y1 * self.globalzoom - self.offset_y, + (x1 - x0) * self.globalzoom, + (y1 - y0) * self.globalzoom end -function UniReader:highLightText() - return +-- make sure the whole word can be seen in screen +function UniReader:_isEntireWordInScreenRange(w) + return self:_isEntireWordInScreenHeightRange(w) and + self:_isEntireWordInScreenWidthRange(w) +end + +-- y axel in djvulibre starts from bottom +function UniReader:_isEntireWordInScreenHeightRange(w) + return (w ~= nil) and + (self.cur_full_height - (w.y1 * self.globalzoom) >= + -self.offset_y) and + (self.cur_full_height - (w.y0 * self.globalzoom) <= + -self.offset_y + G_height) +end + +function UniReader:_isEntireWordInScreenWidthRange(w) + return (w ~= nil) and + (w.x0 * self.globalzoom >= -self.offset_x) and + (w.x1 * self.globalzoom <= -self.offset_x + G_width) +end + +-- make sure at least part of the word can be seen in screen +function UniReader:_isWordInScreenRange(w) + return (w ~= nil) and + (self.cur_full_height - (w.y0 * self.globalzoom) >= + -self.offset_y) and + (self.cur_full_height - (w.y1 * self.globalzoom) <= + -self.offset_y + G_height) and + (w.x1 * self.globalzoom >= -self.offset_x) and + (w.x0 * self.globalzoom <= -self.offset_x + G_width) end function UniReader:toggleTextHighLight(word_list) - return + for _,text_item in ipairs(word_list) do + for _,line_item in ipairs(text_item) do + -- make sure that line is in screen range + if self:_isEntireWordInScreenHeightRange(line_item) then + local x, y, w, h = self:rectCoordTransform( + line_item.x0, line_item.y0, + line_item.x1, line_item.y1) + -- slightly enlarge the highlight height + -- for better viewing experience + x = x + y = y - h * 0.1 + w = w + h = h * 1.2 + + self.highlight.drawer = self.highlight.drawer or "underscore" + if self.highlight.drawer == "underscore" then + self.highlight.line_width = self.highlight.line_width or 2 + self.highlight.line_color = self.highlight.line_color or 5 + fb.bb:paintRect(x, y+h-1, w, + self.highlight.line_width, + self.highlight.line_color) + elseif self.highlight.drawer == "marker" then + fb.bb:invertRect(x, y, w, h) + end + end -- EOF if isEntireWordInScreenHeightRange + end -- EOF for line_item + end -- EOF for text_item end +function UniReader:_wordIterFromRange(t, l0, w0, l1, w1) + local i = l0 + local j = w0 - 1 + return function() + if i <= l1 then + -- if in line range, loop through lines + if i == l1 then + -- in last line + if j < w1 then + j = j + 1 + else + -- out of range return nil + return nil, nil + end + else + if j < #t[i] then + j = j + 1 + else + -- goto next line + i = i + 1 + j = 1 + end + end + return i, j + end + end -- EOF closure +end + +function UniReader:_toggleWordHighLight(t, l, w) + x, y, w, h = self:rectCoordTransform(t[l][w].x0, t[l].y0, + t[l][w].x1, t[l].y1) + -- slightly enlarge the highlight range for better viewing experience + x = x - w * 0.05 + y = y - h * 0.05 + w = w * 1.1 + h = h * 1.1 + + fb.bb:invertRect(x, y, w, h) +end + +function UniReader:_toggleTextHighLight(t, l0, w0, l1, w1) + --print("# toggle range", l0, w0, l1, w1) + -- make sure (l0, w0) is smaller than (l1, w1) + if l0 > l1 then + l0, l1 = l1, l0 + w0, w1 = w1, w0 + elseif l0 == l1 and w0 > w1 then + w0, w1 = w1, w0 + end + + for _l, _w in self:_wordIterFromRange(t, l0, w0, l1, w1) do + if self:_isWordInScreenRange(t[_l][_w]) then + -- blitbuffer module will take care of the out of screen range part. + self:_toggleWordHighLight(t, _l, _w) + end + end +end + +-- remember to clear cursor before calling this +function UniReader:drawCursorAfterWord(t, l, w) + local x, y, w, h = self:rectCoordTransform(t[l].x0, t[l].y0, t[l].x1, t[l].y1) + self.cursor:setHeight(h) + self.cursor:moveTo(x+w, y) + self.cursor:draw() +end + +function UniReader:drawCursorBeforeWord(t, l, w) + local x, y, w, h = self:rectCoordTransform(t[l].x0, t[l].y0, t[l].x1, t[l].y1) + self.cursor:setHeight(h) + self.cursor:moveTo(x, y) + self.cursor:draw() +end + +function UniReader:getText(pageno) + -- define a sensible implementation when your reader supports it + return nil +end + +function UniReader:startHighLightMode() + local t = self:getText(self.pageno) + if not t or #t == 0 then + return nil + end + + local function _findFirstWordInView(t) + for i=1, #t, 1 do + if self:_isEntireWordInScreenRange(t[i][1]) then + return i, 1 + end + end + + return nil + end + + local function _prevWord(t, cur_l, cur_w) + if cur_l == 1 then + if cur_w == 1 then + -- already the first word + return 1, 1 + else + -- in first line, but not first word + return cur_l, cur_w -1 + end + end + + if cur_w <= 1 then + -- first word in current line, goto previous line + return cur_l - 1, #t[cur_l-1] + else + return cur_l, cur_w - 1 + end + end + + local function _nextWord(t, cur_l, cur_w) + if cur_l == #t then + if cur_w == #(t[cur_l]) then + -- already the last word + return cur_l, cur_w + else + -- in last line, but not last word + return cur_l, cur_w + 1 + end + end + + if cur_w < #t[cur_l] then + return cur_l, cur_w + 1 + else + -- last word in current line, move to next line + return cur_l + 1, 1 + end + end + + local function _wordInNextLine(t, cur_l, cur_w) + if cur_l == #t then + -- already in last line, return the last word + return cur_l, #(t[cur_l]) + else + return cur_l + 1, math.min(cur_w, #t[cur_l+1]) + end + end + + local function _wordInPrevLine(t, cur_l, cur_w) + if cur_l == 1 then + -- already in first line, return the first word + return 1, 1 + else + return cur_l - 1, math.min(cur_w, #t[cur_l-1]) + end + end + + local function _isMovingForward(l, w) + return l.cur > l.start or (l.cur == l.start and w.cur > w.start) + end + + local l = {} + local w = {} + + l.start, w.start = _findFirstWordInView(t) + if not l.start then + print("# no text in current view!") + return + end + + l.cur, w.cur = l.start, w.start + l.new, w.new = l.cur, w.cur + local is_meet_start = false + local is_meet_end = false + local running = true + + local cx, cy, cw, ch = self:rectCoordTransform( + t[l.cur][w.cur].x0, + t[l.cur][w.cur].y0, + t[l.cur][w.cur].x1, + t[l.cur][w.cur].y1) + + self.cursor = Cursor:new { + x_pos = cx+cw, + y_pos = cy, + h = ch, + line_width_factor = 4, + } + self.cursor:draw() + fb:refresh(1) + + -- first use cursor to place start pos for highlight + while running do + local ev = input.waitForEvent() + ev.code = adjustKeyEvents(ev) + if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then + if ev.code == KEY_FW_LEFT then + if w.cur == 1 then + w.cur = 0 + w.new = 0 + else + if w.cur == 0 then + -- already at the left end of current line, + -- goto previous line (_prevWord does not understand + -- zero w.cur) + w.cur = 1 + end + l.new, w.new = _prevWord(t, l.cur, w.cur) + end + + self.cursor:clear() + if w.new ~= 0 + and not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) + and self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then + -- word is in previous view + local pageno = self:prevView() + self:goto(pageno) + end + + -- update cursor + if w.cur == 0 then + -- meet line left end, must be handled as special case + if self:_isEntireWordInScreenRange(t[l.cur][1]) then + self:drawCursorBeforeWord(t, l.cur, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_FW_RIGHT then + if w.cur == 0 then + w.cur = 1 + w.new = 1 + else + l.new, w.new = _nextWord(t, l.cur, w.cur) + if w.new == 1 then + -- Must be come from the right end of previous line, + -- so goto the left end of current line. + w.cur = 0 + w.new = 0 + end + end + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + local pageno = self:nextView() + self:goto(pageno) + end + + if w.cur == 0 then + -- meet line left end, must be handled as special case + if self:_isEntireWordInScreenRange(t[l.new][1]) then + self:drawCursorBeforeWord(t, l.new, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_FW_UP then + if w.cur == 0 then + -- goto left end of last line + l.new = math.max(l.cur - 1, 1) + elseif l.cur == 1 and w.cur == 1 then + -- already first word, to the left end of first line + w.new = 0 + else + l.new, w.new = _wordInPrevLine(t, l.cur, w.cur) + end + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + -- goto next view of current page + local pageno = self:prevView() + self:goto(pageno) + end + + if w.new == 0 then + if self:_isEntireWordInScreenRange(t[l.new][1]) then + self:drawCursorBeforeWord(t, l.new, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_FW_DOWN then + if w.cur == 0 then + -- on the left end of current line, + -- goto left end of next line + l.new = math.min(l.cur + 1, #t) + else + l.new, w.new = _wordInNextLine(t, l.cur, w.cur) + end + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + -- goto next view of current page + local pageno = self:nextView() + self:goto(pageno) + end + + if w.cur == 0 then + if self:_isEntireWordInScreenRange(t[l.new][1]) then + self:drawCursorBeforeWord(t, l.new, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_DEL then + if self.highlight[self.pageno] then + for k, text_item in ipairs(self.highlight[self.pageno]) do + for _, line_item in ipairs(text_item) do + if t[l.cur][w.cur].y0 >= line_item.y0 + and t[l.cur][w.cur].y1 <= line_item.y1 + and t[l.cur][w.cur].x0 >= line_item.x0 + and t[l.cur][w.cur].x1 <= line_item.x1 then + self.highlight[self.pageno][k] = nil + end + end -- EOF for line_item + end -- EOF for text_item + end -- EOF if not highlight table + if #self.highlight[self.pageno] == 0 then + self.highlight[self.pageno] = nil + end + return + elseif ev.code == KEY_FW_PRESS then + if w.cur == 0 then + w.cur = 1 + l.cur, w.cur = _prevWord(t, l.cur, w.cur) + end + l.new, w.new = l.cur, w.cur + l.start, w.start = l.cur, w.cur + running = false + self.cursor:clear() + elseif ev.code == KEY_BACK then + running = false + return + end -- EOF if key event + l.cur, w.cur = l.new, w.new + fb:refresh(1) + end + end -- EOF while + --print("start", l.cur, w.cur, l.start, w.start) + + -- two helper functions for highlight + local function _togglePrevWordHighLight(t, l, w) + l.new, w.new = _prevWord(t, l.cur, w.cur) + + if l.cur == 1 and w.cur == 1 then + is_meet_start = true + -- left end of first line must be handled as special case + w.new = 0 + end + + if w.new ~= 0 and + not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) then + -- word out of left and right sides of current view should + -- not trigger pan by page + if self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then + -- word is in previous view + local pageno = self:prevView() + self:goto(pageno) + end + + local l0 = l.start + local w0 = w.start + local l1 = l.cur + local w1 = w.cur + if _isMovingForward(l, w) then + l0, w0 = _nextWord(t, l0, w0) + l1, w1 = l.new, w.new + end + self:_toggleTextHighLight(t, l0, w0, + l1, w1) + else + self:_toggleWordHighLight(t, l.cur, w.cur) + end + + l.cur, w.cur = l.new, w.new + return l, w, (is_meet_start or false) + end + + local function _toggleNextWordHighLight(t, l, w) + if w.cur == 0 then + w.new = 1 + else + l.new, w.new = _nextWord(t, l.cur, w.cur) + end + if l.new == #t and w.new == #t[#t] then + is_meet_end = true + end + + if not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) then + if self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then + local pageno = self:nextView() + self:goto(pageno) + end + + local tmp_l = l.start + local tmp_w = w.start + if _isMovingForward(l, w) then + tmp_l, tmp_w = _nextWord(t, tmp_l, tmp_w) + end + self:_toggleTextHighLight(t, tmp_l, tmp_w, + l.new, w.new) + else + self:_toggleWordHighLight(t, l.new, w.new) + end + + l.cur, w.cur = l.new, w.new + return l, w, (is_meet_end or false) + end + + + -- go into highlight mode + running = true + while running do + local ev = input.waitForEvent() + ev.code = adjustKeyEvents(ev) + if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then + if ev.code == KEY_FW_LEFT then + is_meet_end = false + if not is_meet_start then + l, w, is_meet_start = _togglePrevWordHighLight(t, l, w) + end + elseif ev.code == KEY_FW_RIGHT then + is_meet_start = false + if not is_meet_end then + l, w, is_meet_end = _toggleNextWordHighLight(t, l, w) + end -- EOF if is not is_meet_end + elseif ev.code == KEY_FW_UP then + is_meet_end = false + if not is_meet_start then + if l.cur == 1 then + -- handle left end of first line as special case + tmp_l = 1 + tmp_w = 0 + else + tmp_l, tmp_w = _wordInPrevLine(t, l.cur, w.cur) + end + while not (tmp_l == l.cur and tmp_w == w.cur) do + l, w, is_meet_start = _togglePrevWordHighLight(t, l, w) + end + end + elseif ev.code == KEY_FW_DOWN then + is_meet_start = false + if not is_meet_end then + if w.cur == 0 then + -- handle left end of first line as special case + tmp_l = math.min(tmp_l + 1, #t) + tmp_w = 1 + else + tmp_l, tmp_w = _wordInNextLine(t, l.new, w.new) + end + while not (tmp_l == l.cur and tmp_w == w.cur) do + l, w, is_meet_end = _toggleNextWordHighLight(t, l, w) + end + end + elseif ev.code == KEY_FW_PRESS then + local l0, w0, l1, w1 + + -- find start and end of highlight text + if _isMovingForward(l, w) then + l0, w0 = _nextWord(t, l.start, w.start) + l1, w1 = l.cur, w.cur + else + l0, w0 = _nextWord(t, l.cur, w.cur) + l1, w1 = l.start, w.start + end + -- remove selection area + self:_toggleTextHighLight(t, l0, w0, l1, w1) + + -- put text into highlight table of current page + local hl_item = {} + local s = "" + local prev_l = l0 + local prev_w = w0 + local l_item = { + x0 = t[l0][w0].x0, + y0 = t[l0].y0, + y1 = t[l0].y1, + } + for _l,_w in self:_wordIterFromRange(t, l0, w0, l1, w1) do + local word_item = t[_l][_w] + if _l > prev_l then + -- in next line, add previous line to highlight item + l_item.x1 = t[prev_l][prev_w].x1 + table.insert(hl_item, l_item) + -- re initialize l_item for new line + l_item = { + x0 = word_item.x0, + y0 = t[_l].y0, + y1 = t[_l].y1, + } + end + s = s .. word_item.word .. " " + prev_l, prev_w = _l, _w + end + -- insert last line of text in line item + l_item.x1 = t[prev_l][prev_w].x1 + table.insert(hl_item, l_item) + hl_item.text = s + + if not self.highlight[self.pageno] then + self.highlight[self.pageno] = {} + end + table.insert(self.highlight[self.pageno], hl_item) + + running = false + elseif ev.code == KEY_BACK then + running = false + end -- EOF if key event + fb:refresh(1) + end + end -- EOF while +end + + + + + + + ---------------------------------------------------- -- Renderer memory ---------------------------------------------------- @@ -1289,5 +1886,20 @@ function UniReader:addAllCommands() end end) -- end panning + -- highlight mode + self.commands:add(KEY_N, nil, "N", + "start highlight mode", + function(unireader) + unireader:startHighLightMode() + unireader:goto(unireader.pageno) + end + ) + self.commands:add(KEY_N, MOD_SHIFT, "N", + "display all highlights", + function(unireader) + unireader:showHighLight() + unireader:goto(unireader.pageno) + end + ) print("## defined commands "..dump(self.commands.map)) end