mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Merge branch 'py3' of https://github.com/mwgabby-li/calibre into selrtl
This commit is contained in:
commit
569c38f505
6
imgsrc/srv/selection-handle-vertical.svg
Normal file
6
imgsrc/srv/selection-handle-vertical.svg
Normal 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 |
@ -543,7 +543,8 @@ class IframeBoss:
|
|||||||
self.send_message(
|
self.send_message(
|
||||||
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
||||||
drag_mouse_position=drag_mouse_position, selection_change_caused_by_search=by_search,
|
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):
|
def onresize_stage2(self):
|
||||||
if scroll_viewport.width() is self.last_window_width and scroll_viewport.height() is self.last_window_height:
|
if scroll_viewport.width() is self.last_window_width and scroll_viewport.height() is self.last_window_height:
|
||||||
|
@ -8,7 +8,7 @@ from uuid import short_uuid
|
|||||||
|
|
||||||
from book_list.globals import get_session_data
|
from book_list.globals import get_session_data
|
||||||
from book_list.theme import get_color
|
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 modals import error_dialog, question_dialog
|
||||||
from read_book.globals import runtime, ui_operations
|
from read_book.globals import runtime, ui_operations
|
||||||
from read_book.highlights import (
|
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()
|
margins = get_margins()
|
||||||
|
|
||||||
def map_boundary(x):
|
def map_boundary(b):
|
||||||
return {'x': (x.x or 0) + margins.left, 'y': (x.y or 0) + margins.top, 'height': x.height or 0, 'onscreen': x.onscreen}
|
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)
|
return map_boundary(cs.start), map_boundary(cs.end)
|
||||||
|
|
||||||
@ -182,11 +198,9 @@ def all_actions():
|
|||||||
return all_actions.ans
|
return all_actions.ans
|
||||||
|
|
||||||
|
|
||||||
def selection_handle(is_left):
|
def selection_handle():
|
||||||
ans = svgicon('selection-handle')
|
ans = svgicon('selection-handle')
|
||||||
s = ans.style
|
s = ans.style
|
||||||
if not is_left:
|
|
||||||
s.transform = 'scaleX(-1)'
|
|
||||||
s.position = 'absolute'
|
s.position = 'absolute'
|
||||||
s.boxSizing = 'border-box'
|
s.boxSizing = 'border-box'
|
||||||
s.touchAction = 'none'
|
s.touchAction = 'none'
|
||||||
@ -217,10 +231,14 @@ class SelectionBar:
|
|||||||
self.current_highlight_style = HighlightStyle(get_session_data().get('highlight_style'))
|
self.current_highlight_style = HighlightStyle(get_session_data().get('highlight_style'))
|
||||||
self.current_notes = ''
|
self.current_notes = ''
|
||||||
self.state = HIDDEN
|
self.state = HIDDEN
|
||||||
self.left_handle_id = unique_id('handle')
|
self.start_handle_id = unique_id('handle')
|
||||||
self.right_handle_id = unique_id('handle')
|
self.end_handle_id = unique_id('handle')
|
||||||
self.bar_id = unique_id('bar')
|
self.bar_id = unique_id('bar')
|
||||||
self.editor_id = unique_id('editor')
|
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 = self.container
|
||||||
container.style.overflow = 'hidden'
|
container.style.overflow = 'hidden'
|
||||||
container.addEventListener('click', self.container_clicked, {'passive': False})
|
container.addEventListener('click', self.container_clicked, {'passive': False})
|
||||||
@ -237,14 +255,14 @@ class SelectionBar:
|
|||||||
self.active_touch = None
|
self.active_touch = None
|
||||||
self.drag_scroll_timer = None
|
self.drag_scroll_timer = None
|
||||||
self.last_drag_scroll_at = 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
|
self.current_editor = None
|
||||||
|
|
||||||
left_handle = selection_handle(True)
|
start_handle = selection_handle()
|
||||||
left_handle.id = self.left_handle_id
|
start_handle.id = self.start_handle_id
|
||||||
right_handle = selection_handle(False)
|
end_handle = selection_handle()
|
||||||
right_handle.id = self.right_handle_id
|
end_handle.id = self.end_handle_id
|
||||||
for h in (left_handle, right_handle):
|
for h in (start_handle, end_handle):
|
||||||
h.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False})
|
h.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False})
|
||||||
h.addEventListener('touchstart', self.touchstart_on_handle, {'passive': False})
|
h.addEventListener('touchstart', self.touchstart_on_handle, {'passive': False})
|
||||||
container.appendChild(h)
|
container.appendChild(h)
|
||||||
@ -263,7 +281,7 @@ class SelectionBar:
|
|||||||
def set_handle_colors(self):
|
def set_handle_colors(self):
|
||||||
handle_fill = get_color('window-background')
|
handle_fill = get_color('window-background')
|
||||||
fg = self.view.current_color_scheme.foreground
|
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)
|
set_handle_color(h, handle_fill, fg)
|
||||||
|
|
||||||
def build_bar(self, annot_id):
|
def build_bar(self, annot_id):
|
||||||
@ -357,12 +375,12 @@ class SelectionBar:
|
|||||||
return document.getElementById(self.bar_id)
|
return document.getElementById(self.bar_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def left_handle(self):
|
def start_handle(self):
|
||||||
return document.getElementById(self.left_handle_id)
|
return document.getElementById(self.start_handle_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def right_handle(self):
|
def end_handle(self):
|
||||||
return document.getElementById(self.right_handle_id)
|
return document.getElementById(self.end_handle_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def editor(self):
|
def editor(self):
|
||||||
@ -374,18 +392,58 @@ class SelectionBar:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def current_handle_position(self):
|
def current_handle_position(self):
|
||||||
lh, rh = self.left_handle, self.right_handle
|
sh, eh = self.start_handle, self.end_handle
|
||||||
lbr, rbr = lh.getBoundingClientRect(), rh.getBoundingClientRect()
|
sbr, ebr = sh.getBoundingClientRect(), eh.getBoundingClientRect()
|
||||||
return {
|
|
||||||
'start': {
|
if not self.vertical:
|
||||||
'onscreen': lh.style.display is not 'none',
|
# Horizontal LTR (i.e. English)
|
||||||
'x': Math.round(lbr.right), 'y': Math.round(lbr.bottom - self.left_line_height // 2)
|
if not self.rtl:
|
||||||
},
|
return {
|
||||||
'end': {
|
'start': {
|
||||||
'onscreen': rh.style.display is not 'none',
|
'onscreen': sh.style.display is not 'none',
|
||||||
'x': Math.round(rbr.left), 'y': Math.round(rbr.bottom - self.right_line_height // 2)
|
'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:
|
if self.last_double_click_at and now - self.last_double_click_at < 500:
|
||||||
self.send_message('extend-to-paragraph')
|
self.send_message('extend-to-paragraph')
|
||||||
return
|
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):
|
if near_element(x, ev.clientX, ev.clientY):
|
||||||
return
|
return
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
@ -446,7 +504,7 @@ class SelectionBar:
|
|||||||
s.top = (ev.clientY - self.position_in_handle.y) + 'px'
|
s.top = (ev.clientY - self.position_in_handle.y) + 'px'
|
||||||
margins = get_margins()
|
margins = get_margins()
|
||||||
pos = self.current_handle_position
|
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
|
start = True
|
||||||
position = map_to_iframe_coords(pos.start, margins)
|
position = map_to_iframe_coords(pos.start, margins)
|
||||||
else:
|
else:
|
||||||
@ -524,10 +582,10 @@ class SelectionBar:
|
|||||||
if self.last_drag_scroll_at is None:
|
if self.last_drag_scroll_at is None:
|
||||||
# dont jump a page immediately in paged mode
|
# dont jump a page immediately in paged mode
|
||||||
if in_flow_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
|
self.last_drag_scroll_at = now
|
||||||
elif now - self.last_drag_scroll_at > interval:
|
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
|
self.last_drag_scroll_at = now
|
||||||
|
|
||||||
def send_drag_scroll_message(self, backwards, handle, extend_selection):
|
def send_drag_scroll_message(self, backwards, handle, extend_selection):
|
||||||
@ -575,13 +633,13 @@ class SelectionBar:
|
|||||||
self.position_undragged_handle()
|
self.position_undragged_handle()
|
||||||
return
|
return
|
||||||
if self.state is EDITING:
|
if self.state is EDITING:
|
||||||
self.left_handle.style.display = 'none'
|
self.start_handle.style.display = 'none'
|
||||||
self.right_handle.style.display = 'none'
|
self.end_handle.style.display = 'none'
|
||||||
self.show()
|
self.show()
|
||||||
self.place_editor()
|
self.place_editor()
|
||||||
return
|
return
|
||||||
self.left_handle.style.display = 'none'
|
self.start_handle.style.display = 'none'
|
||||||
self.right_handle.style.display = 'none'
|
self.end_handle.style.display = 'none'
|
||||||
self.editor.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:
|
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:
|
if not cs.start.onscreen and not cs.end.onscreen:
|
||||||
return self.hide()
|
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.show()
|
||||||
self.bar.style.display = self.left_handle.style.display = self.right_handle.style.display = 'block'
|
self.bar.style.display = self.start_handle.style.display = self.end_handle.style.display = 'block'
|
||||||
start, end = map_boundaries(cs)
|
start, end = map_boundaries(cs, self.vertical, self.rtl)
|
||||||
bar = self.build_bar(cs.annot_id)
|
bar = self.build_bar(cs.annot_id)
|
||||||
bar_height = bar.offsetHeight
|
bar_height = bar.offsetHeight
|
||||||
bar_width = bar.offsetWidth
|
bar_width = bar.offsetWidth
|
||||||
@ -602,8 +671,8 @@ class SelectionBar:
|
|||||||
# - 10 ensures we dont cover scroll bar
|
# - 10 ensures we dont cover scroll bar
|
||||||
'left': buffer, 'right': container.offsetWidth - bar_width - buffer - 10
|
'left': buffer, 'right': container.offsetWidth - bar_width - buffer - 10
|
||||||
}
|
}
|
||||||
left_handle, right_handle = self.left_handle, self.right_handle
|
start_handle, end_handle = self.start_handle, self.end_handle
|
||||||
self.position_handles(left_handle, right_handle, start, end)
|
self.position_handles(start_handle, end_handle, start, end)
|
||||||
|
|
||||||
def place_vertically(pos, put_below):
|
def place_vertically(pos, put_below):
|
||||||
if 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
|
# 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.
|
# 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
|
start, end = end, start
|
||||||
if not end.onscreen and start.onscreen:
|
if not end.onscreen and start.onscreen:
|
||||||
start, end = end, start
|
start, end = end, start
|
||||||
@ -636,54 +705,107 @@ class SelectionBar:
|
|||||||
left = end.x - bar_width // 2
|
left = end.x - bar_width // 2
|
||||||
left = max(limits.left, min(left, limits.right))
|
left = max(limits.left, min(left, limits.right))
|
||||||
bar.style.left = left + 'px'
|
bar.style.left = left + 'px'
|
||||||
lh, rh = left_handle.getBoundingClientRect(), right_handle.getBoundingClientRect()
|
sh, eh = start_handle.getBoundingClientRect(), end_handle.getBoundingClientRect()
|
||||||
changed = position_bar_avoiding_handles(lh, rh, left, top, bar_width, bar_height, container.offsetWidth - 10, container.offsetHeight, buffer)
|
changed = position_bar_avoiding_handles(sh, eh, left, top, bar_width, bar_height, container.offsetWidth - 10, container.offsetHeight, buffer)
|
||||||
if changed:
|
if changed:
|
||||||
if changed.top?:
|
if changed.top?:
|
||||||
place_vertically(changed.top, changed.put_below)
|
place_vertically(changed.top, changed.put_below)
|
||||||
if changed.left?:
|
if changed.left?:
|
||||||
bar.style.left = changed.left + 'px'
|
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 = handle.style
|
||||||
s.display = 'block' if boundary.onscreen else 'none'
|
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.width = f'{width}px'
|
||||||
s.height = f'{height}px'
|
s.height = f'{height}px'
|
||||||
bottom = boundary.y + boundary.height
|
s.transform = 'none'
|
||||||
top = bottom - height
|
if not self.vertical:
|
||||||
s.top = f'{top}px'
|
bottom = boundary.y + boundary.height
|
||||||
if is_left:
|
top = bottom - height
|
||||||
s.left = (boundary.x - width) + 'px'
|
s.top = f'{top}px'
|
||||||
self.left_line_height = boundary.height
|
# 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:
|
else:
|
||||||
s.left = boundary.x + 'px'
|
# Vertical, start, RTL
|
||||||
self.right_line_height = boundary.height
|
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):
|
def position_handles(self, start_handle, end_handle, start, end):
|
||||||
handle_height = max(start.height, end.height)
|
if not self.vertical:
|
||||||
self.place_single_handle(handle_height, left_handle, start, True)
|
selection_size = max(start.height, end.height)
|
||||||
self.place_single_handle(handle_height, right_handle, end, False)
|
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):
|
def position_undragged_handle(self):
|
||||||
cs = self.view.currently_showing.selection
|
cs = self.view.currently_showing.selection
|
||||||
start, end = map_boundaries(cs)
|
start, end = map_boundaries(cs, self.vertical, self.rtl)
|
||||||
handle_height = max(start.height, end.height)
|
if not self.vertical:
|
||||||
if self.dragging_handle is self.left_handle_id:
|
selection_size = max(start.height, end.height)
|
||||||
handle = self.right_handle
|
|
||||||
boundary = end
|
|
||||||
is_left = False
|
|
||||||
else:
|
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
|
boundary = start
|
||||||
is_left = True
|
is_start = True
|
||||||
self.place_single_handle(handle_height, handle, boundary, is_left)
|
self.place_single_handle(selection_size, handle, boundary, is_start)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Editor {{{
|
# Editor {{{
|
||||||
def show_editor(self, highlight_style, notes):
|
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'
|
x.style.display = 'none'
|
||||||
container = self.editor
|
container = self.editor
|
||||||
clear(container)
|
clear(container)
|
||||||
@ -699,7 +821,7 @@ class SelectionBar:
|
|||||||
return
|
return
|
||||||
ed = self.editor
|
ed = self.editor
|
||||||
cs = self.view.currently_showing.selection
|
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:
|
if not start.onscreen and not end.onscreen:
|
||||||
return
|
return
|
||||||
width, height = ed.offsetWidth, ed.offsetHeight
|
width, height = ed.offsetWidth, ed.offsetHeight
|
||||||
|
@ -543,7 +543,8 @@ class View:
|
|||||||
'text': data.text, 'empty': data.empty, 'start': data.selection_extents.start,
|
'text': data.text, 'empty': data.empty, 'start': data.selection_extents.start,
|
||||||
'end': data.selection_extents.end, 'annot_id': data.annot_id,
|
'end': data.selection_extents.end, 'annot_id': data.annot_id,
|
||||||
'drag_mouse_position': data.drag_mouse_position,
|
'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:
|
if ui_operations.selection_changed:
|
||||||
ui_operations.selection_changed(self.currently_showing.selection.text, self.currently_showing.selection.annot_id)
|
ui_operations.selection_changed(self.currently_showing.selection.text, self.currently_showing.selection.annot_id)
|
||||||
|
@ -44,10 +44,32 @@ def word_at_point(x, y):
|
|||||||
|
|
||||||
def empty_range_extents():
|
def empty_range_extents():
|
||||||
return {
|
return {
|
||||||
'start': {'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, 'onscreen': False, 'is_empty': True}
|
'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):
|
def range_extents(q, in_flow_mode):
|
||||||
ans = empty_range_extents()
|
ans = empty_range_extents()
|
||||||
@ -55,21 +77,40 @@ def range_extents(q, in_flow_mode):
|
|||||||
return ans
|
return ans
|
||||||
start = q.cloneRange()
|
start = q.cloneRange()
|
||||||
end = 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()
|
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
|
# this tends to happen when moving the mouse downwards
|
||||||
# at the boundary between paragraphs
|
# at the boundary between paragraphs
|
||||||
if r.startContainer?.nodeType is Node.ELEMENT_NODE:
|
if r.startContainer?.nodeType is Node.ELEMENT_NODE:
|
||||||
node = r.startContainer
|
node = r.startContainer
|
||||||
if r.startOffset and node.childNodes.length > r.startOffset:
|
if r.startOffset and node.childNodes.length > r.startOffset:
|
||||||
node = node.childNodes[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
|
# we cant use getBoundingClientRect as the node might be split
|
||||||
# among multiple columns
|
# among multiple columns
|
||||||
if node.getClientRects:
|
else if node.getClientRects:
|
||||||
rects = node.getClientRects()
|
rects = node.getClientRects()
|
||||||
if rects.length:
|
if rects.length:
|
||||||
erect = rects[0]
|
erect = rects[0]
|
||||||
@ -77,13 +118,61 @@ def range_extents(q, in_flow_mode):
|
|||||||
ans.x = Math.round(rect.left)
|
ans.x = Math.round(rect.left)
|
||||||
ans.y = Math.round(rect.top)
|
ans.y = Math.round(rect.top)
|
||||||
ans.height = rect.height
|
ans.height = rect.height
|
||||||
if rect.right <= window.innerWidth and rect.bottom <= window.innerHeight and rect.left >= 0 and rect.top >= 0:
|
ans.width = rect.width
|
||||||
ans.onscreen = True
|
ans.onscreen = rect_onscreen(rect)
|
||||||
|
|
||||||
for_boundary(start, ans.start)
|
if q.startContainer.nodeType is Node.ELEMENT_NODE:
|
||||||
for_boundary(end, ans.end)
|
start.collapse(True)
|
||||||
ans.start.is_empty = ans.start.height <= 0
|
for_boundary(start, ans.start, True)
|
||||||
ans.end.is_empty = ans.end.height <= 0
|
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:
|
if ans.end.height is 2 and ans.start.height > 2:
|
||||||
ans.end.height = ans.start.height
|
ans.end.height = ans.start.height
|
||||||
if ans.start.height is 2 and ans.end.height > 2:
|
if ans.start.height is 2 and ans.end.height > 2:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user