From 0b202f78ca9a087c3504a57826b231bc44555f6b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 22 Aug 2020 14:20:15 +0530 Subject: [PATCH 1/2] String changes --- src/calibre/gui2/book_details.py | 2 +- src/calibre/gui2/dialogs/quickview.py | 14 +++++++------- src/calibre/gui2/tag_browser/ui.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index fd9e24cb08..fd591428eb 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -104,7 +104,7 @@ def init_find_in_tag_browser(menu, ac, field, value): hidden_cats = get_gui().tags_view.model().hidden_categories if field not in hidden_cats: ac.setIcon(QIcon(I('search.png'))) - ac.setText(_('Find %s in Tag browser') % value) + ac.setText(_('Find %s in the Tag browser') % value) ac.current_fmt = field, value menu.addAction(ac) diff --git a/src/calibre/gui2/dialogs/quickview.py b/src/calibre/gui2/dialogs/quickview.py index c17c3f71b5..92e9ffb43f 100644 --- a/src/calibre/gui2/dialogs/quickview.py +++ b/src/calibre/gui2/dialogs/quickview.py @@ -245,7 +245,7 @@ class Quickview(QDialog, Ui_Quickview): self.close_button_tooltip = _('The Quickview shortcut ({0}) shows/hides the Quickview panel') if self.is_pane: self.dock_button.setText(_('Undock')) - self.dock_button.setToolTip(_('Pop up the quickview panel into its own floating window')) + self.dock_button.setToolTip(_('Show the Quickview panel in its own floating window')) self.dock_button.setIcon(QIcon(I('arrow-up.png'))) # Remove the ampersands from the buttons because shortcuts exist. self.lock_qv.setText(_('Lock Quickview contents')) @@ -294,9 +294,9 @@ class Quickview(QDialog, Ui_Quickview): def show_item_context_menu(self, point): item = self.items.currentItem() self.context_menu = QMenu(self) - self.context_menu.addAction(self.search_icon, _('Search for item in tag browser'), + self.context_menu.addAction(self.search_icon, _('Search for item in the Tag browser'), partial(self.item_doubleclicked, item)) - self.context_menu.addAction(self.search_icon, _('Search for item in library'), + self.context_menu.addAction(self.search_icon, _('Search for item in the library'), partial(self.do_search, follow_library_view=False)) self.context_menu.popup(self.items.mapToGlobal(point)) self.context_menu = QMenu(self) @@ -311,10 +311,10 @@ class Quickview(QDialog, Ui_Quickview): book_id = int(item.data(Qt.UserRole)) book_displayed = self.book_displayed_in_library_view(book_id) m = self.context_menu = QMenu(self) - a = m.addAction(self.select_book_icon, _('Select book in library'), + a = m.addAction(self.select_book_icon, _('Select book in the library'), partial(self.select_book, book_id)) a.setEnabled(book_displayed) - m.addAction(self.search_icon, _('Search for item in library'), + m.addAction(self.search_icon, _('Search for item in the library'), partial(self.do_search, follow_library_view=False)) a = m.addAction(self.edit_metadata_icon, _('Edit book metadata'), partial(self.edit_metadata, book_id, follow_library_view=False)) @@ -324,7 +324,7 @@ class Quickview(QDialog, Ui_Quickview): a.setEnabled(self.is_category(self.column_order[column]) and book_displayed and not self.lock_qv.isChecked()) m.addSeparator() - m.addAction(self.view_icon, _('Open book in viewer'), + m.addAction(self.view_icon, _('Open book in the E-book viewer'), partial(self.view_plugin._view_calibre_books, [book_id])) self.context_menu.popup(self.books_table.mapToGlobal(point)) return True @@ -680,7 +680,7 @@ class Quickview(QDialog, Ui_Quickview): error_dialog(self, _('Quickview: Book not in library view'), _('The book you selected is not currently displayed in ' 'the library view, perhaps because of a search or a ' - 'virtual library, so Quickview cannot select it.'), + 'Virtual library, so Quickview cannot select it.'), show=True, show_copy_button=False) diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index 1c100e1316..002ff8e23c 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -495,7 +495,7 @@ class TagBrowserBar(QWidget): # {{{ ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser find button', - _('Find in Tag browser'), default_keys=(), + _('Find in the Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.search_button.click) From 9b79d6a5fa28098ebfb83bdf4188ca6cc060c093 Mon Sep 17 00:00:00 2001 From: "Mark W. Gabby-Li" Date: Fri, 28 Aug 2020 00:18:47 -0700 Subject: [PATCH 2/2] Support Selection Handles on Vertical/RTL Books - Pass vertical/rtl mode into selection code. - Added new image for vertical selection handle. selection_bar.pyj: - Made code agnostic to text direction. - Changed names to start and end rather than left and right to reflect new behavior. - Track vertical/rtl state from selection message. - Handle selection position modified to support all possible text orientations. - Switch to vertical selection handle in vertical mode. - Cap selection size at 60px to avoid comically large (and unusable) handles when selecting large element, such as an image. select.pyj: - Improved selection behavior when selected range extents are on nodes by search the node's DOM tree for something with a reasonable bounding box. - To work around bugs with collapsed range rects and vertical text, use character bounding boxes in most cases. - Add width to selection range extents. --- imgsrc/srv/selection-handle-vertical.svg | 6 + src/pyj/read_book/iframe.pyj | 3 +- src/pyj/read_book/selection_bar.pyj | 266 +++++++++++++++++------ src/pyj/read_book/view.pyj | 3 +- src/pyj/select.pyj | 115 ++++++++-- 5 files changed, 306 insertions(+), 87 deletions(-) create mode 100644 imgsrc/srv/selection-handle-vertical.svg diff --git a/imgsrc/srv/selection-handle-vertical.svg b/imgsrc/srv/selection-handle-vertical.svg new file mode 100644 index 0000000000..f6f9237941 --- /dev/null +++ b/imgsrc/srv/selection-handle-vertical.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 7ae3531185..966255b418 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -543,7 +543,8 @@ class IframeBoss: self.send_message( 'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id, drag_mouse_position=drag_mouse_position, selection_change_caused_by_search=by_search, - selection_extents=selection_extents(current_layout_mode() is 'flow')) + selection_extents=selection_extents(current_layout_mode() is 'flow'), + rtl = scroll_viewport.rtl, vertical = scroll_viewport.vertical_writing_mode) def onresize_stage2(self): if scroll_viewport.width() is self.last_window_width and scroll_viewport.height() is self.last_window_height: diff --git a/src/pyj/read_book/selection_bar.pyj b/src/pyj/read_book/selection_bar.pyj index de80c621df..dfce9c6221 100644 --- a/src/pyj/read_book/selection_bar.pyj +++ b/src/pyj/read_book/selection_bar.pyj @@ -8,7 +8,7 @@ from uuid import short_uuid from book_list.globals import get_session_data from book_list.theme import get_color -from dom import clear, svgicon, unique_id +from dom import change_icon_image, clear, svgicon, unique_id from modals import error_dialog, question_dialog from read_book.globals import runtime, ui_operations from read_book.highlights import ( @@ -28,11 +28,27 @@ def get_margins(): } -def map_boundaries(cs): +def map_boundaries(cs, vertical, rtl): margins = get_margins() - def map_boundary(x): - return {'x': (x.x or 0) + margins.left, 'y': (x.y or 0) + margins.top, 'height': x.height or 0, 'onscreen': x.onscreen} + def map_boundary(b): + x_offset = 0 + y_offset = 0 + if not vertical: + # Horizontal LTR + if not rtl: + if b.selected_prev: + x_offset = b.width + # Horizontal RTL + else: + if not b.selected_prev: + x_offset = b.width + else: + # Vertical: + if b.selected_prev: + y_offset = b.height + + return {'x': (b.x or 0) + x_offset + margins.left, 'y': (b.y or 0) + y_offset + margins.top, 'height': b.height or 0, 'width': b.width or 0, 'onscreen': b.onscreen} return map_boundary(cs.start), map_boundary(cs.end) @@ -182,11 +198,9 @@ def all_actions(): return all_actions.ans -def selection_handle(is_left): +def selection_handle(): ans = svgicon('selection-handle') s = ans.style - if not is_left: - s.transform = 'scaleX(-1)' s.position = 'absolute' s.boxSizing = 'border-box' s.touchAction = 'none' @@ -217,10 +231,14 @@ class SelectionBar: self.current_highlight_style = HighlightStyle(get_session_data().get('highlight_style')) self.current_notes = '' self.state = HIDDEN - self.left_handle_id = unique_id('handle') - self.right_handle_id = unique_id('handle') + self.start_handle_id = unique_id('handle') + self.end_handle_id = unique_id('handle') self.bar_id = unique_id('bar') self.editor_id = unique_id('editor') + # Sensible defaults until we get information from a selection message. + self.ltr = True + self.rtl = False + self.vertical = False container = self.container container.style.overflow = 'hidden' container.addEventListener('click', self.container_clicked, {'passive': False}) @@ -237,14 +255,14 @@ class SelectionBar: self.active_touch = None self.drag_scroll_timer = None self.last_drag_scroll_at = None - self.left_line_height = self.right_line_height = 0 + self.start_line_length = self.end_line_length = 0 self.current_editor = None - left_handle = selection_handle(True) - left_handle.id = self.left_handle_id - right_handle = selection_handle(False) - right_handle.id = self.right_handle_id - for h in (left_handle, right_handle): + start_handle = selection_handle() + start_handle.id = self.start_handle_id + end_handle = selection_handle() + end_handle.id = self.end_handle_id + for h in (start_handle, end_handle): h.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False}) h.addEventListener('touchstart', self.touchstart_on_handle, {'passive': False}) container.appendChild(h) @@ -263,7 +281,7 @@ class SelectionBar: def set_handle_colors(self): handle_fill = get_color('window-background') fg = self.view.current_color_scheme.foreground - for h in (self.left_handle, self.right_handle): + for h in (self.start_handle, self.end_handle): set_handle_color(h, handle_fill, fg) def build_bar(self, annot_id): @@ -357,12 +375,12 @@ class SelectionBar: return document.getElementById(self.bar_id) @property - def left_handle(self): - return document.getElementById(self.left_handle_id) + def start_handle(self): + return document.getElementById(self.start_handle_id) @property - def right_handle(self): - return document.getElementById(self.right_handle_id) + def end_handle(self): + return document.getElementById(self.end_handle_id) @property def editor(self): @@ -374,18 +392,58 @@ class SelectionBar: @property def current_handle_position(self): - lh, rh = self.left_handle, self.right_handle - lbr, rbr = lh.getBoundingClientRect(), rh.getBoundingClientRect() - return { - 'start': { - 'onscreen': lh.style.display is not 'none', - 'x': Math.round(lbr.right), 'y': Math.round(lbr.bottom - self.left_line_height // 2) - }, - 'end': { - 'onscreen': rh.style.display is not 'none', - 'x': Math.round(rbr.left), 'y': Math.round(rbr.bottom - self.right_line_height // 2) + sh, eh = self.start_handle, self.end_handle + sbr, ebr = sh.getBoundingClientRect(), eh.getBoundingClientRect() + + if not self.vertical: + # Horizontal LTR (i.e. English) + if not self.rtl: + return { + 'start': { + 'onscreen': sh.style.display is not 'none', + 'x': Math.round(sbr.right), 'y': Math.round(sbr.bottom - self.start_line_length // 2) + }, + 'end': { + 'onscreen': eh.style.display is not 'none', + 'x': Math.round(ebr.left), 'y': Math.round(ebr.bottom - self.end_line_length // 2) + } + } + # Horizontal RTL (i.e. Hebrew, Arabic) + else: + return { + 'start': { + 'onscreen': sh.style.display is not 'none', + 'x': Math.round(sbr.left), 'y': Math.round(sbr.bottom - self.start_line_length // 2) + }, + 'end': { + 'onscreen': eh.style.display is not 'none', + 'x': Math.round(ebr.right), 'y': Math.round(ebr.bottom - self.end_line_length // 2) + } + } + # Vertical RTL (i.e. Traditional Chinese, Japanese) + else if self.rtl: + return { + 'start': { + 'onscreen': sh.style.display is not 'none', + 'x': Math.round(sbr.left + self.start_line_length // 2), 'y': Math.round(sbr.bottom) + }, + 'end': { + 'onscreen': eh.style.display is not 'none', + 'x': Math.round(ebr.right - self.end_line_length // 2), 'y': Math.round(ebr.top) + } + } + # Vertical LTR (i.e. Mongolian) + else: + return { + 'start': { + 'onscreen': sh.style.display is not 'none', + 'x': Math.round(sbr.right - self.end_line_length // 2), 'y': Math.round(sbr.bottom) + }, + 'end': { + 'onscreen': eh.style.display is not 'none', + 'x': Math.round(ebr.left + self.start_line_length // 2), 'y': Math.round(ebr.top) + } } - } # }}} @@ -419,7 +477,7 @@ class SelectionBar: if self.last_double_click_at and now - self.last_double_click_at < 500: self.send_message('extend-to-paragraph') return - for x in (self.bar, self.left_handle, self.right_handle): + for x in (self.bar, self.start_handle, self.end_handle): if near_element(x, ev.clientX, ev.clientY): return self.clear_selection() @@ -446,7 +504,7 @@ class SelectionBar: s.top = (ev.clientY - self.position_in_handle.y) + 'px' margins = get_margins() pos = self.current_handle_position - if self.dragging_handle is self.left_handle_id: + if self.dragging_handle is self.start_handle_id: start = True position = map_to_iframe_coords(pos.start, margins) else: @@ -524,10 +582,10 @@ class SelectionBar: if self.last_drag_scroll_at is None: # dont jump a page immediately in paged mode if in_flow_mode: - self.send_drag_scroll_message(backwards, 'left' if self.dragging_handle is self.left_handle_id else 'right', True) + self.send_drag_scroll_message(backwards, 'left' if self.dragging_handle is self.start_handle_id else 'right', True) self.last_drag_scroll_at = now elif now - self.last_drag_scroll_at > interval: - self.send_drag_scroll_message(backwards, 'left' if self.dragging_handle is self.left_handle_id else 'right', True) + self.send_drag_scroll_message(backwards, 'left' if self.dragging_handle is self.start_handle_id else 'right', True) self.last_drag_scroll_at = now def send_drag_scroll_message(self, backwards, handle, extend_selection): @@ -575,13 +633,13 @@ class SelectionBar: self.position_undragged_handle() return if self.state is EDITING: - self.left_handle.style.display = 'none' - self.right_handle.style.display = 'none' + self.start_handle.style.display = 'none' + self.end_handle.style.display = 'none' self.show() self.place_editor() return - self.left_handle.style.display = 'none' - self.right_handle.style.display = 'none' + self.start_handle.style.display = 'none' + self.end_handle.style.display = 'none' self.editor.style.display = 'none' if not cs or cs.empty or jstype(cs.drag_mouse_position.x) is 'number' or cs.selection_change_caused_by_search: @@ -590,9 +648,20 @@ class SelectionBar: if not cs.start.onscreen and not cs.end.onscreen: return self.hide() + self.rtl = cs.rtl + self.ltr = not self.rtl + self.vertical = cs.vertical + for h in (self.start_handle, self.end_handle): + if h.vertical is not self.vertical: + h.vertical = self.vertical + if self.vertical: + change_icon_image(h, 'selection-handle-vertical') + else: + change_icon_image(h, 'selection-handle') + self.show() - self.bar.style.display = self.left_handle.style.display = self.right_handle.style.display = 'block' - start, end = map_boundaries(cs) + self.bar.style.display = self.start_handle.style.display = self.end_handle.style.display = 'block' + start, end = map_boundaries(cs, self.vertical, self.rtl) bar = self.build_bar(cs.annot_id) bar_height = bar.offsetHeight bar_width = bar.offsetWidth @@ -602,8 +671,8 @@ class SelectionBar: # - 10 ensures we dont cover scroll bar 'left': buffer, 'right': container.offsetWidth - bar_width - buffer - 10 } - left_handle, right_handle = self.left_handle, self.right_handle - self.position_handles(left_handle, right_handle, start, end) + start_handle, end_handle = self.start_handle, self.end_handle + self.position_handles(start_handle, end_handle, start, end) def place_vertically(pos, put_below): if put_below: @@ -617,7 +686,7 @@ class SelectionBar: # We try to place the bar near the last dragged handle so it shows up # close to current mouse position. We assume it is the "end" handle. - if dragged_handle and dragged_handle is not self.right_handle_id: + if dragged_handle and dragged_handle is not self.end_handle_id: start, end = end, start if not end.onscreen and start.onscreen: start, end = end, start @@ -636,54 +705,107 @@ class SelectionBar: left = end.x - bar_width // 2 left = max(limits.left, min(left, limits.right)) bar.style.left = left + 'px' - lh, rh = left_handle.getBoundingClientRect(), right_handle.getBoundingClientRect() - changed = position_bar_avoiding_handles(lh, rh, left, top, bar_width, bar_height, container.offsetWidth - 10, container.offsetHeight, buffer) + sh, eh = start_handle.getBoundingClientRect(), end_handle.getBoundingClientRect() + changed = position_bar_avoiding_handles(sh, eh, left, top, bar_width, bar_height, container.offsetWidth - 10, container.offsetHeight, buffer) if changed: if changed.top?: place_vertically(changed.top, changed.put_below) if changed.left?: bar.style.left = changed.left + 'px' - def place_single_handle(self, handle_height, handle, boundary, is_left): + def place_single_handle(self, selection_size, handle, boundary, is_start): s = handle.style s.display = 'block' if boundary.onscreen else 'none' - height = handle_height * 2 - width = int(height * 2 / 3) + + # Cap this to prevent very large handles when selecting images. + selection_size = min(60, selection_size) + + if not self.vertical: + height = selection_size * 2 + width = int(height * 2 / 3) + else: + width = selection_size * 2 + height = int(width * 2 / 3) + s.width = f'{width}px' s.height = f'{height}px' - bottom = boundary.y + boundary.height - top = bottom - height - s.top = f'{top}px' - if is_left: - s.left = (boundary.x - width) + 'px' - self.left_line_height = boundary.height + s.transform = 'none' + if not self.vertical: + bottom = boundary.y + boundary.height + top = bottom - height + s.top = f'{top}px' + # Horizontal, start, LTR + if is_start and self.ltr: + s.left = (boundary.x - width) + 'px' + self.start_line_length = selection_size + # Horizontal, start, RTL + else if is_start: + s.left = (boundary.x) + 'px' + self.start_line_length = selection_size + s.transform = 'scaleX(-1)' + # Horizontal, end, LTR + else if self.ltr: + s.left = boundary.x + 'px' + self.end_line_length = selection_size + s.transform = 'scaleX(-1)' + # Horizontal, end, RTL + else: + s.left = (boundary.x - width) + 'px' + self.end_line_length = selection_size else: - s.left = boundary.x + 'px' - self.right_line_height = boundary.height + # Vertical, start, RTL + if is_start and self.rtl: + s.top = boundary.y - height + 'px' + s.left = boundary.x + 'px' + self.start_line_length = selection_size + s.transform = f'scaleX(-1) scaleY(-1)' + # Vertical, start, LTR + else if is_start: + s.top = boundary.y - height + 'px' + s.left = boundary.x - width + boundary.width + 'px' + self.start_line_length = selection_size + s.transform = f'scaleY(-1)' + # Vertical, end, RTL + else if self.rtl: + s.top = boundary.y + 'px' + s.left = boundary.x - width + boundary.width + 'px' + self.end_line_length = selection_size + # Vertical, end, LTR + else: + s.top = boundary.y + 'px' + s.left = boundary.x + 'px' + self.end_line_length = selection_size + s.transform = f'scaleX(-1)' - def position_handles(self, left_handle, right_handle, start, end): - handle_height = max(start.height, end.height) - self.place_single_handle(handle_height, left_handle, start, True) - self.place_single_handle(handle_height, right_handle, end, False) + def position_handles(self, start_handle, end_handle, start, end): + if not self.vertical: + selection_size = max(start.height, end.height) + else: + selection_size = max(start.width, end.width) + self.place_single_handle(selection_size, start_handle, start, True) + self.place_single_handle(selection_size, end_handle, end, False) def position_undragged_handle(self): cs = self.view.currently_showing.selection - start, end = map_boundaries(cs) - handle_height = max(start.height, end.height) - if self.dragging_handle is self.left_handle_id: - handle = self.right_handle - boundary = end - is_left = False + start, end = map_boundaries(cs, self.vertical, self.rtl) + if not self.vertical: + selection_size = max(start.height, end.height) else: - handle = self.left_handle + selection_size = max(start.width, end.width) + if self.dragging_handle is self.start_handle_id: + handle = self.end_handle + boundary = end + is_start = False + else: + handle = self.start_handle boundary = start - is_left = True - self.place_single_handle(handle_height, handle, boundary, is_left) + is_start = True + self.place_single_handle(selection_size, handle, boundary, is_start) # }}} # Editor {{{ def show_editor(self, highlight_style, notes): - for x in (self.bar, self.left_handle, self.right_handle): + for x in (self.bar, self.start_handle, self.end_handle): x.style.display = 'none' container = self.editor clear(container) @@ -699,7 +821,7 @@ class SelectionBar: return ed = self.editor cs = self.view.currently_showing.selection - start, end = map_boundaries(cs) + start, end = map_boundaries(cs, self.vertical, self.rtl) if not start.onscreen and not end.onscreen: return width, height = ed.offsetWidth, ed.offsetHeight diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index b865ce0fb8..6915570101 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -543,7 +543,8 @@ class View: 'text': data.text, 'empty': data.empty, 'start': data.selection_extents.start, 'end': data.selection_extents.end, 'annot_id': data.annot_id, 'drag_mouse_position': data.drag_mouse_position, - 'selection_change_caused_by_search': data.selection_change_caused_by_search + 'selection_change_caused_by_search': data.selection_change_caused_by_search, + 'rtl': data.rtl, 'vertical': data.vertical } if ui_operations.selection_changed: ui_operations.selection_changed(self.currently_showing.selection.text, self.currently_showing.selection.annot_id) diff --git a/src/pyj/select.pyj b/src/pyj/select.pyj index 0f3de7a5e6..b8de2fce12 100644 --- a/src/pyj/select.pyj +++ b/src/pyj/select.pyj @@ -44,10 +44,32 @@ def word_at_point(x, y): def empty_range_extents(): return { - 'start': {'x': 0, 'y': 0, 'height': 0, 'onscreen': False, 'is_empty': True}, - 'end': {'x': 0, 'y': 0, 'height': 0, 'onscreen': False, 'is_empty': True} + 'start': {'x': 0, 'y': 0, 'height': 0, 'width': 0, 'onscreen': False, 'selected_prev': False}, + 'end': {'x': 0, 'y': 0, 'height': 0, 'width': 0, 'onscreen': False, 'selected_prev': False} } +# Returns a node that we know will produce a reasonable bounding box closest to the start or end +# of the DOM tree from the specified node. +# Currently: BR, IMG, and text nodes. +# This a depth first traversal, with the modification that if a node is reached that meets the criteria, +# the traversal stops, because higher nodes in the DOM tree always have larger bounds than their children. +def get_selection_node_at_boundary(node, start): + stack = [] + stack.push({'node': node, 'visited': False}) + while stack.length > 0: + top = stack[-1] + # If the top node is a target type, we know that no nodes below it can be more to the start or end + # than it, so return it immediately. + if top.node.nodeType is Node.TEXT_NODE or top.node.nodeName.upper() == 'IMG' or top.node.nodeName.upper() == 'BR': + return top.node + # Otherwise, depth-first traversal. + else if top.visited: + stack.pop() + else: + top.visited = True + for c in top.node.childNodes if start else reversed(top.node.childNodes): + stack.push({'node': c, 'visited': False}) + return None def range_extents(q, in_flow_mode): ans = empty_range_extents() @@ -55,21 +77,40 @@ def range_extents(q, in_flow_mode): return ans start = q.cloneRange() end = q.cloneRange() - start.collapse(True) - end.collapse(False) - def for_boundary(r, ans): + def rect_onscreen(r): + if r.right <= window.innerWidth and r.bottom <= window.innerHeight and r.left >= 0 and r.top >= 0: + return True + return False + + def for_boundary(r, ans, is_start): rect = r.getBoundingClientRect() - if rect.height is 0: + if rect.height is 0 and rect.width is 0: # this tends to happen when moving the mouse downwards # at the boundary between paragraphs if r.startContainer?.nodeType is Node.ELEMENT_NODE: node = r.startContainer if r.startOffset and node.childNodes.length > r.startOffset: node = node.childNodes[r.startOffset] + + boundary_node = get_selection_node_at_boundary(node, is_start) + # If we found a node that will produce a reasonable bounding box at a boundary, use it: + if boundary_node: + if boundary_node.nodeType is Node.TEXT_NODE: + if is_start: + r.setStart(boundary_node, boundary_node.length - 1) + r.setEnd(boundary_node, boundary_node.length) + else: + r.setStart(boundary_node, 0) + r.setEnd(boundary_node, 1) + rect = r.getBoundingClientRect() + else: + rect = boundary_node.getBoundingClientRect() + if not is_start: + ans.selected_prev = True # we cant use getBoundingClientRect as the node might be split # among multiple columns - if node.getClientRects: + else if node.getClientRects: rects = node.getClientRects() if rects.length: erect = rects[0] @@ -77,13 +118,61 @@ def range_extents(q, in_flow_mode): ans.x = Math.round(rect.left) ans.y = Math.round(rect.top) ans.height = rect.height - if rect.right <= window.innerWidth and rect.bottom <= window.innerHeight and rect.left >= 0 and rect.top >= 0: - ans.onscreen = True + ans.width = rect.width + ans.onscreen = rect_onscreen(rect) - for_boundary(start, ans.start) - for_boundary(end, ans.end) - ans.start.is_empty = ans.start.height <= 0 - ans.end.is_empty = ans.end.height <= 0 + if q.startContainer.nodeType is Node.ELEMENT_NODE: + start.collapse(True) + for_boundary(start, ans.start, True) + else if q.startOffset is 0 and q.startContainer.length is 0: + start.collapse(True) + for_boundary(start, ans.start, True) + else if q.startOffset == q.startContainer.length: + start.setStart(q.startContainer, q.startOffset - 1) + start.setEnd(q.startContainer, q.startOffset) + rect = start.getBoundingClientRect() + ans.start.x = rect.left + ans.start.y = rect.top + ans.start.height = rect.height + ans.start.width = rect.width + ans.start.selected_prev = True + ans.start.onscreen = rect_onscreen(rect) + else: + start.setStart(q.startContainer, q.startOffset) + start.setEnd(q.startContainer, q.startOffset + 1) + rect = start.getBoundingClientRect() + ans.start.x = rect.left + ans.start.y = rect.top + ans.start.height = rect.height + ans.start.width = rect.width + ans.start.onscreen = rect_onscreen(rect) + + if q.endContainer.nodeType is Node.ELEMENT_NODE: + end.collapse(False) + for_boundary(end, ans.end, False) + else if q.endOffset is 0 and q.endContainer.length is 0: + end.collapse(False) + for_boundary(end, ans.end, False) + else if q.endOffset is q.endContainer.length: + end.setStart(q.endContainer, q.endOffset - 1) + end.setEnd(q.endContainer, q.endOffset) + rect = end.getBoundingClientRect() + ans.end.x = rect.left + ans.end.y = rect.top + ans.end.height = rect.height + ans.end.width = rect.width + ans.end.selected_prev = True + ans.end.onscreen = rect_onscreen(rect) + else: + end.setStart(q.endContainer, q.endOffset) + end.setEnd(q.endContainer, q.endOffset + 1) + rect = end.getBoundingClientRect() + ans.end.x = rect.left + ans.end.y = rect.top + ans.end.height = rect.height + ans.end.width = rect.width + ans.end.onscreen = rect_onscreen(rect) + if ans.end.height is 2 and ans.start.height > 2: ans.end.height = ans.start.height if ans.start.height is 2 and ans.end.height > 2: