Merge branch 'py3' of https://github.com/mwgabby-li/calibre into selrtl

This commit is contained in:
Kovid Goyal 2020-08-31 09:59:50 +05:30
commit 569c38f505
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 306 additions and 87 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" width="94" height="64" viewBox="0 0 25.4 16.933333" version="1.1">
<path
style="stroke-width:1.2"
d="M 0.40531915,0.22517714 C 0.57570388,5.3511725 1.2577531,11.548814 5.6244322,14.821275 9.1147504,16.85191 12.195909,12.670186 13.111224,9.6410928 c 1.028474,-2.9493579 1.716378,-6.4799769 1.424227,-9.41591566 -4.7100448,0 -9.4200882,0 -14.13013185,0 z m 14.49602685,0.00368 c 3.49955,0 6.999102,0 10.498654,0 -3.499552,0 -6.999104,0 -10.498654,0 z"/>
</svg>

After

Width:  |  Height:  |  Size: 570 B

View File

@ -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:

View File

@ -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,16 +392,56 @@ class SelectionBar:
@property
def current_handle_position(self):
lh, rh = self.left_handle, self.right_handle
lbr, rbr = lh.getBoundingClientRect(), rh.getBoundingClientRect()
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': lh.style.display is not 'none',
'x': Math.round(lbr.right), 'y': Math.round(lbr.bottom - self.left_line_height // 2)
'onscreen': sh.style.display is not 'none',
'x': Math.round(sbr.right), 'y': Math.round(sbr.bottom - self.start_line_length // 2)
},
'end': {
'onscreen': rh.style.display is not 'none',
'x': Math.round(rbr.left), 'y': Math.round(rbr.bottom - self.right_line_height // 2)
'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
# 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'
s.transform = 'none'
if not self.vertical:
bottom = boundary.y + boundary.height
top = bottom - height
s.top = f'{top}px'
if is_left:
# Horizontal, start, LTR
if is_start and self.ltr:
s.left = (boundary.x - width) + 'px'
self.left_line_height = boundary.height
else:
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.right_line_height = boundary.height
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:
# 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

View File

@ -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)

View File

@ -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)
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)
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 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: