diff --git a/imgsrc/srv/swatch.svg b/imgsrc/srv/swatch.svg
deleted file mode 100644
index 84b0207e21..0000000000
--- a/imgsrc/srv/swatch.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
diff --git a/src/pyj/range_utils.pyj b/src/pyj/range_utils.pyj
index f521409550..4676327f42 100644
--- a/src/pyj/range_utils.pyj
+++ b/src/pyj/range_utils.pyj
@@ -106,7 +106,7 @@ def create_wrapper_function(wrapper_elem, r, intersecting_wrappers, process_wrap
wrapper_counter = 0
-def wrap_text_in_range(style, r, process_wrapper):
+def wrap_text_in_range(styler, r, process_wrapper):
if not r:
sel = window.getSelection()
if not sel or not sel.rangeCount:
@@ -117,12 +117,8 @@ def wrap_text_in_range(style, r, process_wrapper):
wrapper_elem = document.createElement('span')
wrapper_elem.dataset.calibreRangeWrapper = v'++wrapper_counter' + ''
- if style:
- style = style.rstrip()
- if !style.endsWith(';'):
- style += ';'
- style = str.replace(style, ';', ' !important;')
- wrapper_elem.setAttribute('style', style)
+ if styler:
+ styler(wrapper_elem)
intersecting_wrappers = {}
wrap_node = create_wrapper_function(wrapper_elem, r, intersecting_wrappers, process_wrapper)
@@ -152,27 +148,38 @@ def set_selection_to_highlight():
return crw or None
+def get_annot_id_for(node, offset, annot_id_uuid_map):
+
+ if not node:
+ return
+ if node.nodeType is Node.ELEMENT_NODE:
+ if node.dataset.calibreRangeWrapper:
+ return annot_id_uuid_map[node.dataset.calibreRangeWrapper]
+ if offset is 0:
+
+ if node.firstChild?.nodeType is Node.ELEMENT_NODE and node.firstChild.dataset.calibreRangeWrapper:
+ return annot_id_uuid_map[node.firstChild.dataset.calibreRangeWrapper]
+ elif offset < node.childNodes.length:
+ node = node.childNodes[offset]
+ return get_annot_id_for(node, 0)
+ elif node.nodeType is Node.TEXT_NODE:
+ if node.parentNode?.nodeType is Node.ELEMENT_NODE and node.parentNode.dataset.calibreRangeWrapper:
+ return annot_id_uuid_map[node.parentNode.dataset.calibreRangeWrapper]
+
+
def highlight_associated_with_selection(sel, annot_id_uuid_map):
- # Return the annotation id for a highlight that contains the focus or
- # anchor of the selection
+ # Return the annotation id for a highlight intersecting the selection
+ if sel.rangeCount:
+ annot_id = get_annot_id_for(sel.focusNode, sel.focusOffset, annot_id_uuid_map) or get_annot_id_for(sel.anchorNode, sel.anchorOffset, annot_id_uuid_map)
+ if annot_id:
+ return annot_id
- def get_annot_id_for(node, offset):
- if not node:
- return
- if node.nodeType is Node.ELEMENT_NODE:
- if node.dataset.calibreRangeWrapper:
- return annot_id_uuid_map[node.dataset.calibreRangeWrapper]
- if offset is 0:
- if node.firstChild?.nodeType is Node.ELEMENT_NODE and node.firstChild.dataset.calibreRangeWrapper:
- return annot_id_uuid_map[node.firstChild.dataset.calibreRangeWrapper]
- elif offset < node.childNodes.length:
- node = node.childNodes[offset]
- return get_annot_id_for(node, 0)
- elif node.nodeType is Node.TEXT_NODE:
- if node.parentNode?.nodeType is Node.ELEMENT_NODE and node.parentNode.dataset.calibreRangeWrapper:
- return annot_id_uuid_map[node.parentNode.dataset.calibreRangeWrapper]
-
- annot_id = get_annot_id_for(sel.focusNode, sel.focusOffset)
- if not annot_id:
- annot_id = get_annot_id_for(sel.anchorNode, sel.anchorOffset)
- return annot_id
+ all_wrappers = document.querySelectorAll('span[data-calibre-range-wrapper]')
+ for v'var i = 0; i < sel.rangeCount; i++':
+ r = sel.getRangeAt(i)
+ for v'var x = 0; x < all_wrappers.length; x++':
+ wrapper = all_wrappers[x]
+ if r.intersectsNode(wrapper):
+ annot_id = annot_id_uuid_map[wrapper.dataset.calibreRangeWrapper]
+ if annot_id:
+ return annot_id
diff --git a/src/pyj/read_book/highlights.pyj b/src/pyj/read_book/highlights.pyj
index 8c769bf7a8..b7514c3931 100644
--- a/src/pyj/read_book/highlights.pyj
+++ b/src/pyj/read_book/highlights.pyj
@@ -2,6 +2,15 @@
# License: GPL v3 Copyright: 2020, Kovid Goyal
from __python__ import bound_methods, hash_literals
+from elementmaker import E
+from gettext import gettext as _
+
+from book_list.globals import get_session_data
+from book_list.theme import get_color
+from dom import unique_id
+from widgets import create_button
+
+ICON_SIZE = '3ex'
builtin_colors_light = {
'yellow': '#ffeb6b',
'green': '#c0ed72',
@@ -27,29 +36,237 @@ def default_color(is_dark):
return builtin_color('yellow', is_dark)
+def all_builtin_styles():
+ ans = v'[]'
+ for col in builtin_colors_light:
+ ans.push({'type': 'builtin', 'kind': 'color', 'which': col})
+ return ans
+
+
+def custom_color_theme(bg):
+ return {'type': 'custom', 'kind': 'color', 'which': bg}
+
+
class HighlightStyle:
def __init__(self, style):
+ if jstype(style) is 'string':
+ style = JSON.parse(style)
self.style = style or {'type': 'builtin', 'kind': 'color', 'which': 'yellow'}
+ self.key = f'type:{style.type} kind:{style.kind} which: {style.which} bg: {style["background-color"]}'
+
+ def make_swatch(self, container, is_dark):
+ s = container.style
+ s.width = s.height = s.minimumWidth = s.minimumHeight = ICON_SIZE
+ bg = None
+ if s.type is 'builtin':
+ if s.kind is 'color':
+ bg = builtin_color(s.which, is_dark)
+ if bg is None and s['background-color']:
+ bg = s['background-color']
+ if bg:
+ s.backgroundColor = bg
+ s.borderRadius = '4px'
def highlight_shade(self, is_dark):
s = self.style
if s.type is 'builtin':
if s.kind is 'color':
return builtin_color(s.which, is_dark)
- return default_color(is_dark)
return s['background-color'] or default_color(is_dark)
- def as_css(self, is_dark, foreground):
- s = self.style
+ def serialized(self):
+ return JSON.stringify(self.style)
+
+
+def highlight_style_as_css(s, is_dark, foreground):
+
+ def styler(node):
+ node = node.style
if s.type is 'builtin':
if s.kind is 'color':
- ans = 'background-color: ' + builtin_color(s.which, is_dark) + ';'
+ node.backgroundColor = builtin_color(s.which, is_dark)
if foreground:
- ans += 'color: ' + foreground + ';'
- return ans
- ans = 'background-color: ' + (s['background-color'] or default_color(is_dark)) + ';'
+ node.color = foreground
+ return
+ node.backgroundColor = s['background-color'] or default_color(is_dark)
fg = s.color or foreground
if fg:
- ans += 'color: ' + fg + ';'
- return ans
+ node.color = fg
+
+ return styler
+
+
+def custom_styles_equal(a, b):
+ seen = {}
+ for k in a:
+ seen[k] = True
+ if a[k] is not b[k]:
+ return False
+ for k in b:
+ if not seen[k]:
+ if a[k] is not b[k]:
+ return False
+ return True
+
+
+class EditNotesAndColors: # {{{
+
+ def __init__(self, container, is_dark_theme, current_notes, current_style, close_editor):
+ self.initial_style = current_style
+ self.is_dark_theme = is_dark_theme
+
+ def separator():
+ return E.hr(style='max-width: 80em; width: 80vw; border-top: solid 1px; margin: auto; margin-top: 2ex; margin-bottom: 2ex')
+
+ def finish():
+ close_editor(True)
+
+ def abort():
+ close_editor(False)
+
+ def handle_keypress(ev):
+ ev.stopPropagation()
+ if ev.key is 'Escape':
+ abort()
+ elif ev.key is 'Enter' and ev.ctrlKey:
+ finish()
+
+ c = E.div(
+ style=f'background: {get_color("window-background")}; margin: auto; padding: 1rem',
+ onclick=def(ev): ev.stopPropagation();,
+ id=unique_id(),
+ E.h3(_('Add notes for this highlight')),
+ E.textarea(
+ current_notes or '',
+ rows='10', spellcheck='true', style='resize: none; width: 80vw; max-width: 80em; margin: 1ex',
+ onkeydown=handle_keypress,
+ ),
+ E.div(
+ style='margin: 1ex; font-size: smaller',
+ _('Double click or long tap on a highlight to see its notes')
+ ),
+
+ separator(),
+
+ E.h3(_('Choose the color for this highlight'), style='margin-bottom: 2ex'),
+ E.div(
+ class_='color-block',
+ style=f'display: flex; flex-wrap: wrap; max-width: calc({ICON_SIZE} * 8); margin: auto',
+ ),
+ E.div(
+ style='max-width: 80em; width: 80vw; margin: auto; margin-top: 2ex; display: flex; justify-content: space-between; align-items: center',
+ E.div(
+ E.label(_('New color:'), ' ', E.input(type='color', onchange=self.add_custom_color))
+ ),
+ E.div(
+ E.a(_('Remove color'), class_='simple-link remove-custom-color', onclick=self.remove_custom_color),
+ ),
+ ),
+
+ separator(),
+
+ E.div(
+ style='max-width: 80em; width: 80vw; margin: auto; display: flex; justify-content: space-between',
+ create_button(_('Cancel'), 'close', abort, _('Abort') + ' [Esc]'),
+ create_button(_('Finish'), 'check', finish, _('Finish editing highlight') + ' [Ctrl+Enter]', True),
+ )
+
+ )
+ self.container_id = c.id
+ container.appendChild(c)
+ self.seen_colors = {}
+ custom_highlight_styles = get_session_data().get('custom_highlight_styles')
+ for raw in custom_highlight_styles:
+ self.add_color(HighlightStyle(raw)).classList.add('custom-style')
+ for raw in all_builtin_styles():
+ self.add_color(HighlightStyle(raw))
+ if not c.querySelector('.current-swatch'):
+ self.add_color(self.initial_style)
+
+ self.set_visibility_of_remove_button()
+ self.notes_edit.focus()
+
+ def set_visibility_of_remove_button(self):
+ c = self.container
+ item = c.querySelector('.current-swatch.custom-style')
+ visibility = 'unset' if item else 'hidden'
+ c.querySelector('.remove-custom-color').style.visibility = visibility
+
+ def add_color(self, hs, at_start):
+ if self.seen_colors[hs.key]:
+ return
+ self.seen_colors[hs.key] = True
+ ic = E.div()
+ hs.make_swatch(ic, self.is_dark_theme)
+ ic.classList.add('simple-link')
+ is_current = hs.key is self.initial_style.key
+ sqbg = get_color('window-background2') if is_current else 'unset'
+ item = E.div(
+ ic, style=f'padding: 4px; background-color: {sqbg}; margin: 4px',
+ onclick=self.change_color
+ )
+ if is_current:
+ item.classList.add('current-swatch')
+ item.dataset.style = hs.serialized()
+ parent = self.container.getElementsByClassName('color-block')[0]
+ if at_start:
+ parent.insertBefore(item, parent.firstChild)
+ else:
+ parent.appendChild(item)
+ return item
+
+ def add_custom_color(self):
+ bg = self.container.querySelector('input[type=color]').value
+ cct = custom_color_theme(bg)
+ item = self.add_color(HighlightStyle(cct), True)
+ item.classList.add('custom-style')
+ self.make_swatch_current(item)
+ sd = get_session_data()
+ custom_highlight_styles = sd.get('custom_highlight_styles')
+ custom_highlight_styles.unshift(cct)
+ sd.set('custom_highlight_styles', custom_highlight_styles)
+
+ def remove_custom_color(self):
+ item = self.container.getElementsByClassName('current-swatch')[0]
+ cct = JSON.parse(item.dataset.style)
+ p = item.parentNode
+ p.removeChild(item)
+ self.make_swatch_current(p.firstChild)
+ sd = get_session_data()
+ custom_highlight_styles = sd.get('custom_highlight_styles')
+ ans = v'[]'
+ for x in custom_highlight_styles:
+ if not custom_styles_equal(x, cct):
+ ans.push(x)
+ sd.set('custom_highlight_styles', ans)
+
+ @property
+ def container(self):
+ return document.getElementById(self.container_id)
+
+ @property
+ def notes_edit(self):
+ return self.container.getElementsByTagName('textarea')[0]
+
+ def change_color(self, evt):
+ evt.stopPropagation()
+ self.make_swatch_current(evt.currentTarget)
+
+ def make_swatch_current(self, item):
+ for child in item.parentNode.childNodes:
+ child.style.backgroundColor = 'unset'
+ child.classList.remove('current-swatch')
+ item.style.backgroundColor = get_color('window-background2')
+ item.classList.add('current-swatch')
+ self.notes_edit.focus()
+ self.set_visibility_of_remove_button()
+
+ @property
+ def current_notes(self):
+ return self.notes_edit.value or ''
+
+ @property
+ def current_style(self):
+ return HighlightStyle(self.container.getElementsByClassName('current-swatch')[0].dataset.style)
+# }}}
diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj
index 48a827b295..748c4f48c0 100644
--- a/src/pyj/read_book/iframe.pyj
+++ b/src/pyj/read_book/iframe.pyj
@@ -35,6 +35,7 @@ from read_book.globals import (
current_spine_item, runtime, set_boss, set_current_spine_item, set_layout_mode,
set_toc_anchor_map
)
+from read_book.highlights import highlight_style_as_css
from read_book.mathjax import apply_mathjax
from read_book.paged_mode import (
anchor_funcs as paged_anchor_funcs,
@@ -782,7 +783,8 @@ class IframeBoss:
if not sel.rangeCount:
return
bounds = cfi_for_selection()
- annot_id, intersecting_wrappers = wrap_text_in_range(data.style, None, self.add_highlight_listeners)
+ style = highlight_style_as_css(data.style, opts.is_dark_theme, opts.color_scheme.foreground)
+ annot_id, intersecting_wrappers = wrap_text_in_range(style, None, self.add_highlight_listeners)
removed_highlights = v'[]'
if annot_id is not None:
intersecting_uuids = {annot_id_uuid_map[x]:True for x in intersecting_wrappers}
@@ -821,7 +823,7 @@ class IframeBoss:
r = range_from_cfi(h.start_cfi, h.end_cfi)
if not r:
continue
- style = f'color: {h.style.color}; background-color: {h.style["background-color"]}'
+ style = highlight_style_as_css(h.style, opts.is_dark_theme, opts.color_scheme.foreground)
annot_id, intersecting_wrappers = wrap_text_in_range(style, r, self.add_highlight_listeners)
if annot_id is not None:
annot_id_uuid_map[annot_id] = h.uuid
diff --git a/src/pyj/read_book/selection_bar.pyj b/src/pyj/read_book/selection_bar.pyj
index 800e6949ff..846b86e509 100644
--- a/src/pyj/read_book/selection_bar.pyj
+++ b/src/pyj/read_book/selection_bar.pyj
@@ -9,9 +9,8 @@ from book_list.globals import get_session_data
from book_list.theme import get_color
from dom import clear, svgicon, unique_id
from read_book.globals import runtime, ui_operations
-from read_book.highlights import HighlightStyle
+from read_book.highlights import ICON_SIZE, EditNotesAndColors, HighlightStyle
-ICON_SIZE = '3ex'
DRAG_SCROLL_ZONE_MIN_HEIGHT = 10
# Utils {{{
@@ -220,6 +219,7 @@ class SelectionBar:
self.drag_scroll_timer = None
self.last_drag_scroll_at = -100000
self.left_line_height = self.right_line_height = 0
+ self.current_editor = None
left_handle = selection_handle(True)
left_handle.id = self.left_handle_id
@@ -235,6 +235,7 @@ class SelectionBar:
'left: 0; top: 0; display: flex; flex-direction: column;'
))
container.appendChild(E.div(id=self.editor_id))
+ container.lastChild.addEventListener('click', self.editor_container_clicked, {'passive': False})
# bar and handles markup {{{
@@ -262,7 +263,7 @@ class SelectionBar:
]:
bar_container.appendChild(x)
bar = bar_container.firstChild
- hs = self.current_highlight_style.highlight_shade
+ hs = self.current_highlight_style.highlight_shade(self.view.current_color_scheme.is_dark_theme)
def cb(ac, callback):
ans = ac.icon_function(hs)
@@ -381,7 +382,7 @@ class SelectionBar:
def container_clicked(self, ev):
ev.stopPropagation(), ev.preventDefault()
if self.state is EDITING:
- return # TODO: accept edit and apply highlight
+ self.hide_editor(True)
if self.state is WAITING:
for x in (self.bar, self.left_handle, self.right_handle):
if near_element(x, ev.clientX, ev.clientY):
@@ -498,6 +499,7 @@ class SelectionBar:
self.editor.style.display = 'none'
self.set_handle_colors()
if self.state is DRAGGING:
+ self.show()
return
self.left_handle.style.display = 'none'
self.right_handle.style.display = 'none'
@@ -516,7 +518,7 @@ class SelectionBar:
self.show()
if self.state is EDITING:
- return self.show_editor() # TODO: Implement this
+ return
self.bar.style.display = self.left_handle.style.display = self.right_handle.style.display = 'block'
start = map_boundary(cs.start)
end = map_boundary(cs.end)
@@ -591,7 +593,24 @@ class SelectionBar:
start, end = end, start
place_single_handle(left_handle, start, True)
place_single_handle(right_handle, end, False)
+ # }}}
+ # Editor {{{
+ def show_editor(self, highlight_style, notes):
+ for x in (self.bar, self.left_handle, self.right_handle):
+ x.style.display = 'none'
+ container = self.editor
+ clear(container)
+ container.style.display = 'block'
+ self.state = EDITING
+ self.current_editor = EditNotesAndColors(
+ container, self.view.current_color_scheme.is_dark_theme, notes, highlight_style, self.hide_editor)
+
+ def hide_editor(self, apply):
+ pass # TODO: Implement this
+
+ def editor_container_clicked(self, ev):
+ ev.stopPropagation(), ev.preventDefault()
# }}}
# Actions {{{
@@ -617,7 +636,17 @@ class SelectionBar:
self.hide()
def create_highlight(self):
- pass # TODO: Implement this
+ cs = self.view.currently_showing.selection
+ hs = self.current_highlight_style
+ notes = ''
+ if cs.annot_id:
+ am = self.view.annotations_manager
+ q = am.style_for_highlight(cs.annot_id)
+ if q:
+ hs = HighlightStyle(q)
+ notes = am.notes_for_highlight(cs.annot_id) or notes
+ self.show()
+ self.show_editor(hs, notes)
def quick_highlight(self):
cs = self.view.currently_showing.selection
diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj
index cf7a4013c7..b1036bdeea 100644
--- a/src/pyj/session.pyj
+++ b/src/pyj/session.pyj
@@ -63,7 +63,7 @@ defaults = {
'user_stylesheet': '',
'word_actions': v'[]',
'highlight_style': None,
- 'custom_highlight_colors': v'[]',
+ 'custom_highlight_styles': v'[]',
'show_selection_bar': True,
'net_search_url': 'https://google.com/search?q={q}',
'selection_bar_actions': v"['copy', 'lookup', 'highlight', 'remove_highlight', 'search_net', 'clear']",