mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-02-16 00:09:36 -05:00
269 lines
9.0 KiB
Plaintext
269 lines
9.0 KiB
Plaintext
# vim:fileencoding=utf-8
|
||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||
from __python__ import hash_literals
|
||
|
||
from complete import create_search_bar
|
||
from dom import set_css, svgicon, ensure_id
|
||
from elementmaker import E
|
||
from gettext import gettext as _
|
||
from modals import error_dialog
|
||
from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view
|
||
from read_book.globals import toc_anchor_map, set_toc_anchor_map, current_spine_item, current_layout_mode, current_book
|
||
from read_book.viewport import scroll_viewport
|
||
|
||
|
||
def update_visible_toc_nodes(visible_anchors):
|
||
update_visible_toc_nodes.data = visible_anchors
|
||
update_visible_toc_nodes.data = {}
|
||
|
||
|
||
def iter_toc_descendants(node, callback):
|
||
for child in node.children:
|
||
if callback(child):
|
||
return
|
||
iter_toc_descendants(child, callback)
|
||
|
||
|
||
def get_toc_nodes_bordering_spine_item(toc, csi):
|
||
# Find the ToC entries that point to the closest files on either side of the
|
||
# spine item
|
||
toc = toc or current_book().manifest.toc
|
||
csi = csi or current_spine_item()
|
||
spine = current_book().manifest.spine
|
||
spine_before, spine_after = {}, {}
|
||
which = spine_before
|
||
before = after = prev = None
|
||
for name in spine:
|
||
if name is csi:
|
||
which = spine_after
|
||
else:
|
||
which[name] = True
|
||
iter_toc_descendants(toc, def(node):
|
||
nonlocal prev, before, after
|
||
if node.dest:
|
||
if spine_before[node.dest]:
|
||
prev = node
|
||
elif spine_after[node.dest]:
|
||
if not before:
|
||
before = prev
|
||
if not after:
|
||
after = node
|
||
return True
|
||
)
|
||
if not before and prev is not None:
|
||
before = prev
|
||
return before, after
|
||
|
||
|
||
def get_border_nodes(toc, id_map):
|
||
data = update_visible_toc_nodes.data
|
||
before, after = data.before, data.after
|
||
if before:
|
||
before = id_map[before]
|
||
if after:
|
||
after = id_map[after]
|
||
if before and after:
|
||
# Both border nodes are in the current spine item
|
||
return before, after
|
||
sb, sa = get_toc_nodes_bordering_spine_item(toc)
|
||
before = before or sb
|
||
after = after or sa
|
||
return before, after
|
||
|
||
|
||
def family_for_toc_node(toc_node_id, parent_map, id_map):
|
||
if not id_map or not parent_map:
|
||
toc = current_book().manifest.toc
|
||
parent_map, id_map = get_toc_maps(toc)
|
||
family = v'[]'
|
||
node = id_map[toc_node_id]
|
||
while node and node.title:
|
||
family.unshift(node)
|
||
parent = parent_map[node.id]
|
||
node = None
|
||
if parent:
|
||
node = id_map[parent.id]
|
||
return family
|
||
|
||
|
||
def get_current_toc_nodes():
|
||
toc = current_book().manifest.toc
|
||
parent_map, id_map = get_toc_maps(toc)
|
||
data = update_visible_toc_nodes.data
|
||
ans = {}
|
||
if data.has_visible:
|
||
ans = data.visible_anchors
|
||
else:
|
||
if data.before:
|
||
ans[data.before] = True
|
||
else:
|
||
before = get_border_nodes(toc, id_map)[0]
|
||
if before:
|
||
ans[before.id] = True
|
||
r = v'[]'
|
||
for x in Object.keys(ans):
|
||
fam = family_for_toc_node(x, parent_map, id_map)
|
||
if fam?.length:
|
||
r.push(fam)
|
||
return r
|
||
|
||
|
||
def get_highlighted_toc_nodes(toc, parent_map, id_map, skip_parents):
|
||
data = update_visible_toc_nodes.data
|
||
ans = {}
|
||
if data.has_visible:
|
||
ans = data.visible_anchors
|
||
else:
|
||
if data.before:
|
||
ans[data.before] = True
|
||
else:
|
||
before = get_border_nodes(toc, id_map)[0]
|
||
if before:
|
||
ans[before.id] = True
|
||
if not skip_parents:
|
||
for node_id in Object.keys(ans):
|
||
p = parent_map[node_id]
|
||
while p and p.title:
|
||
ans[p.id] = True
|
||
p = parent_map[p.id]
|
||
return ans
|
||
|
||
def get_toc_maps(toc):
|
||
if not toc:
|
||
toc = current_book().manifest.toc
|
||
parent_map, id_map = {}, {}
|
||
|
||
def process_node(node, parent):
|
||
id_map[node.id] = node
|
||
parent_map[node.id] = parent
|
||
for c in node.children:
|
||
process_node(c, node)
|
||
|
||
process_node(toc)
|
||
return parent_map, id_map
|
||
|
||
def get_book_mark_title():
|
||
toc = current_book().manifest.toc
|
||
parent_map, id_map = get_toc_maps(toc)
|
||
highlighted_toc_nodes = get_highlighted_toc_nodes(toc, parent_map, id_map, True)
|
||
for node_id in Object.keys(highlighted_toc_nodes):
|
||
node = id_map[node_id]
|
||
if node.title:
|
||
return node.title
|
||
return ''
|
||
|
||
|
||
def create_toc_tree(toc, onclick):
|
||
parent_map, id_map = get_toc_maps(toc)
|
||
highlighted_toc_nodes = get_highlighted_toc_nodes(toc, parent_map, id_map)
|
||
|
||
def populate_data(node, li, a):
|
||
li.dataset.tocDest = node.dest or ''
|
||
li.dataset.tocFrag = node.frag or ''
|
||
title = node.title or ''
|
||
if highlighted_toc_nodes[node.id]:
|
||
a.appendChild(E.b(E.i(title)))
|
||
else:
|
||
a.textContent = title
|
||
|
||
return create_tree(toc, populate_data, onclick)
|
||
|
||
def do_search(text):
|
||
container = document.getElementById(this)
|
||
a = find_text_in_tree(container, text)
|
||
if not text:
|
||
return
|
||
if not a:
|
||
return error_dialog(_('No matches found'), _(
|
||
'The text "{}" was not found in the Table of Contents').format(text))
|
||
scroll_tree_item_into_view(a)
|
||
|
||
def create_toc_panel(book, container, onclick):
|
||
def handle_click(event, li):
|
||
if event.button is 0:
|
||
onclick(li.dataset.tocDest, li.dataset.tocFrag)
|
||
toc_panel = create_toc_tree(book.manifest.toc, handle_click)
|
||
toc_panel_id = ensure_id(toc_panel)
|
||
set_css(container, display='flex', flex_direction='column', height='100%', min_height='100%', overflow='hidden', max_height='100vh', max_width='100vw')
|
||
set_css(toc_panel, flex_grow='10')
|
||
container.appendChild(toc_panel)
|
||
search_button = E.div(class_='simple-link', svgicon('search'))
|
||
t = _('Search Table of Contents')
|
||
search_bar = create_search_bar(do_search.bind(toc_panel_id), 'search-book-toc', button=search_button, placeholder=t)
|
||
set_css(search_bar, flex_grow='10', margin_right='1em')
|
||
container.appendChild(E.div(style='margin: 1ex 1em; display: flex; align-items: center', search_bar, search_button))
|
||
for child in container.childNodes:
|
||
child.style.flexShrink = '0'
|
||
toc_panel.style.flexGrow = '100'
|
||
toc_panel.style.flexShrink = '1'
|
||
toc_panel.style.overflow = 'auto'
|
||
|
||
|
||
def current_toc_anchor_map(tam, anchor_funcs):
|
||
current_map = toc_anchor_map()
|
||
if not (current_map and current_map.layout_mode is current_layout_mode() and current_map.width is scroll_viewport.width() and current_map.height is scroll_viewport.height()):
|
||
name = current_spine_item().name
|
||
am = {}
|
||
anchors = v'[]'
|
||
pos_map = {}
|
||
for i, anchor in enumerate(tam[name] or v'[]'):
|
||
val = anchor_funcs.pos_for_elem()
|
||
if anchor.frag:
|
||
elem = document.getElementById(anchor.frag)
|
||
if elem:
|
||
val = anchor_funcs.pos_for_elem(elem)
|
||
am[anchor.id] = val
|
||
anchors.push(anchor.id)
|
||
pos_map[anchor.id] = i
|
||
# stable sort by position in document
|
||
anchors.sort(def (a, b): return anchor_funcs.cmp(am[a], am[b]) or (pos_map[a] - pos_map[b]);)
|
||
|
||
current_map = {'layout_mode': current_layout_mode(), 'width': scroll_viewport.width(), 'height': scroll_viewport.height(), 'pos_map': am, 'sorted_anchors':anchors}
|
||
set_toc_anchor_map(current_map)
|
||
return current_map
|
||
|
||
|
||
def update_visible_toc_anchors(toc_anchor_map, anchor_funcs):
|
||
tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs)
|
||
before = after = None
|
||
visible_anchors = {}
|
||
has_visible = False
|
||
|
||
for anchor_id in tam.sorted_anchors:
|
||
pos = tam.pos_map[anchor_id]
|
||
visibility = anchor_funcs.visibility(pos)
|
||
if visibility < 0:
|
||
before = anchor_id
|
||
elif visibility is 0:
|
||
has_visible = True
|
||
visible_anchors[anchor_id] = True
|
||
elif visibility > 0:
|
||
after = anchor_id
|
||
break
|
||
|
||
return {'visible_anchors':visible_anchors, 'has_visible':has_visible, 'before':before, 'after':after, 'sorted_anchors':tam.sorted_anchors}
|
||
|
||
|
||
def find_anchor_before_range(r, toc_anchor_map, anchor_funcs):
|
||
name = current_spine_item().name
|
||
prev_anchor = None
|
||
tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs)
|
||
anchors = toc_anchor_map[name]
|
||
if anchors:
|
||
amap = {x.id: x for x in anchors}
|
||
for anchor_id in tam.sorted_anchors:
|
||
anchor = amap[anchor_id]
|
||
is_before = True
|
||
if anchor.frag:
|
||
elem = document.getElementById(anchor.frag)
|
||
if elem:
|
||
q = document.createRange()
|
||
q.selectNode(elem)
|
||
if q.compareBoundaryPoints(window.Range.START_TO_START, r) > 0:
|
||
is_before = False
|
||
if is_before:
|
||
prev_anchor = anchor
|
||
else:
|
||
break
|
||
return prev_anchor
|