%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/tweak_book/ |
| Current File : //lib/calibre/calibre/gui2/tweak_book/live_css.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import sys
from qt.core import (
QWidget, QTimer, QStackedLayout, QLabel, QScrollArea, QVBoxLayout,
QPainter, Qt, QPalette, QRect, QSize, QSizePolicy, pyqtSignal,
QColor, QMenu, QApplication, QIcon, QUrl)
from calibre.constants import FAKE_HOST, FAKE_PROTOCOL
from calibre.gui2.tweak_book import editors, actions, tprefs
from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color
from calibre.gui2.tweak_book.editor.text import default_font_family
from css_selectors import parse, SelectorError
lowest_specificity = (-sys.maxsize, 0, 0, 0, 0, 0)
class Heading(QWidget): # {{{
toggled = pyqtSignal(object)
context_menu_requested = pyqtSignal(object, object)
def __init__(self, text, expanded=True, parent=None):
QWidget.__init__(self, parent)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.text = text
self.expanded = expanded
self.hovering = False
self.do_layout()
@property
def lines_for_copy(self):
return [self.text]
def do_layout(self):
try:
f = self.parent().font()
except AttributeError:
return
f.setBold(True)
self.setFont(f)
def mousePressEvent(self, ev):
if ev.button() == Qt.MouseButton.LeftButton:
ev.accept()
self.expanded ^= True
self.toggled.emit(self)
self.update()
else:
return QWidget.mousePressEvent(self, ev)
@property
def rendered_text(self):
return ('▾' if self.expanded else '▸') + '\xa0' + self.text
def sizeHint(self):
fm = self.fontMetrics()
sz = fm.boundingRect(self.rendered_text).size()
return sz
def paintEvent(self, ev):
p = QPainter(self)
p.setClipRect(ev.rect())
bg = self.palette().color(QPalette.ColorRole.AlternateBase)
if self.hovering:
bg = bg.lighter(115)
p.fillRect(self.rect(), bg)
try:
p.drawText(self.rect(), Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter|Qt.TextFlag.TextSingleLine, self.rendered_text)
finally:
p.end()
def enterEvent(self, ev):
self.hovering = True
self.update()
return QWidget.enterEvent(self, ev)
def leaveEvent(self, ev):
self.hovering = False
self.update()
return QWidget.leaveEvent(self, ev)
def contextMenuEvent(self, ev):
self.context_menu_requested.emit(self, ev)
# }}}
class Cell: # {{{
__slots__ = ('rect', 'text', 'right_align', 'color_role', 'override_color', 'swatch', 'is_overriden')
SIDE_MARGIN = 5
FLAGS = Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextSingleLine | Qt.TextFlag.TextIncludeTrailingSpaces
def __init__(self, text, rect, right_align=False, color_role=QPalette.ColorRole.WindowText, swatch=None, is_overriden=False):
self.rect, self.text = rect, text
self.right_align = right_align
self.is_overriden = is_overriden
self.color_role = color_role
self.override_color = None
self.swatch = swatch
if swatch is not None:
self.swatch = QColor(swatch[0], swatch[1], swatch[2], int(255 * swatch[3]))
def draw(self, painter, width, palette):
flags = self.FLAGS | (Qt.AlignmentFlag.AlignRight if self.right_align else Qt.AlignmentFlag.AlignLeft)
rect = QRect(self.rect)
if self.right_align:
rect.setRight(width - self.SIDE_MARGIN)
painter.setPen(palette.color(self.color_role) if self.override_color is None else self.override_color)
br = painter.drawText(rect, flags, self.text)
if self.swatch is not None:
r = QRect(br.right() + self.SIDE_MARGIN // 2, br.top() + 2, br.height() - 4, br.height() - 4)
painter.fillRect(r, self.swatch)
br.setRight(r.right())
if self.is_overriden:
painter.setPen(palette.color(QPalette.ColorRole.WindowText))
painter.drawLine(br.left(), br.top() + br.height() // 2, br.right(), br.top() + br.height() // 2)
# }}}
class Declaration(QWidget):
hyperlink_activated = pyqtSignal(object)
context_menu_requested = pyqtSignal(object, object)
def __init__(self, html_name, data, is_first=False, parent=None):
QWidget.__init__(self, parent)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum)
self.data = data
self.is_first = is_first
self.html_name = html_name
self.lines_for_copy = []
self.do_layout()
self.setMouseTracking(True)
def do_layout(self):
fm = self.fontMetrics()
bounding_rect = lambda text: fm.boundingRect(0, 0, 10000, 10000, Cell.FLAGS, text)
line_spacing = 2
side_margin = Cell.SIDE_MARGIN
self.rows = []
ypos = line_spacing + (1 if self.is_first else 0)
if 'href' in self.data:
name = self.data['href']
if isinstance(name, list):
name = self.html_name
br1 = bounding_rect(name)
sel = self.data['selector'] or ''
if self.data['type'] == 'inline':
sel = 'style=""'
br2 = bounding_rect(sel)
self.hyperlink_rect = QRect(side_margin, ypos, br1.width(), br1.height())
self.rows.append([
Cell(name, self.hyperlink_rect, color_role=QPalette.ColorRole.Link),
Cell(sel, QRect(br1.right() + side_margin, ypos, br2.width(), br2.height()), right_align=True)
])
ypos += max(br1.height(), br2.height()) + 2 * line_spacing
self.lines_for_copy.append(name + ' ' + sel)
for prop in self.data['properties']:
text = prop.name + ':\xa0'
br1 = bounding_rect(text)
vtext = prop.value + '\xa0' + ('!' if prop.important else '') + prop.important
br2 = bounding_rect(vtext)
self.rows.append([
Cell(text, QRect(side_margin, ypos, br1.width(), br1.height()), color_role=QPalette.ColorRole.LinkVisited, is_overriden=prop.is_overriden),
Cell(vtext, QRect(br1.right() + side_margin, ypos, br2.width(), br2.height()), swatch=prop.color, is_overriden=prop.is_overriden)
])
self.lines_for_copy.append(text + vtext)
if prop.is_overriden:
self.lines_for_copy[-1] += ' [overridden]'
ypos += max(br1.height(), br2.height()) + line_spacing
self.lines_for_copy.append('--------------------------\n')
self.height_hint = ypos + line_spacing
self.width_hint = max(row[-1].rect.right() + side_margin for row in self.rows) if self.rows else 0
def sizeHint(self):
return QSize(self.width_hint, self.height_hint)
def paintEvent(self, ev):
p = QPainter(self)
p.setClipRect(ev.rect())
palette = self.palette()
p.setPen(palette.color(QPalette.ColorRole.WindowText))
if not self.is_first:
p.drawLine(0, 0, self.width(), 0)
try:
for row in self.rows:
for cell in row:
p.save()
try:
cell.draw(p, self.width(), palette)
finally:
p.restore()
finally:
p.end()
def mouseMoveEvent(self, ev):
if hasattr(self, 'hyperlink_rect'):
pos = ev.pos()
hovering = self.hyperlink_rect.contains(pos)
self.update_hover(hovering)
cursor = Qt.CursorShape.ArrowCursor
for r, row in enumerate(self.rows):
for cell in row:
if cell.rect.contains(pos):
cursor = Qt.CursorShape.PointingHandCursor if cell.rect is self.hyperlink_rect else Qt.CursorShape.IBeamCursor
if r == 0:
break
if cursor != Qt.CursorShape.ArrowCursor:
break
self.setCursor(cursor)
return QWidget.mouseMoveEvent(self, ev)
def mousePressEvent(self, ev):
if hasattr(self, 'hyperlink_rect') and ev.button() == Qt.MouseButton.LeftButton:
pos = ev.pos()
if self.hyperlink_rect.contains(pos):
self.emit_hyperlink_activated()
return QWidget.mousePressEvent(self, ev)
def emit_hyperlink_activated(self):
dt = self.data['type']
data = {'type':dt, 'name':self.html_name, 'syntax':'html'}
if dt == 'inline': # style attribute
data['sourceline_address'] = self.data['href']
elif dt == 'elem': # <style> tag
data['sourceline_address'] = self.data['href']
data['rule_address'] = self.data['rule_address']
else: # stylesheet
data['name'] = self.data['href']
data['rule_address'] = self.data['rule_address']
data['syntax'] = 'css'
self.hyperlink_activated.emit(data)
def leaveEvent(self, ev):
self.update_hover(False)
self.setCursor(Qt.CursorShape.ArrowCursor)
return QWidget.leaveEvent(self, ev)
def update_hover(self, hovering):
cell = self.rows[0][0]
if (hovering and cell.override_color is None) or (
not hovering and cell.override_color is not None):
cell.override_color = QColor(Qt.GlobalColor.red) if hovering else None
self.update()
def contextMenuEvent(self, ev):
self.context_menu_requested.emit(self, ev)
class Box(QWidget):
hyperlink_activated = pyqtSignal(object)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QVBoxLayout(self)
l.setAlignment(Qt.AlignmentFlag.AlignTop)
self.setLayout(l)
self.widgets = []
def show_data(self, data):
for w in self.widgets:
self.layout().removeWidget(w)
for x in ('toggled', 'hyperlink_activated', 'context_menu_requested'):
if hasattr(w, x):
try:
getattr(w, x).disconnect()
except TypeError:
pass
w.deleteLater()
self.widgets = []
for node in data['nodes']:
node_name = node['name'] + ' @%s' % node['sourceline']
if node['ancestor_specificity'] != 0:
title = _('Inherited from %s') % node_name
else:
title = _('Matched CSS rules for %s') % node_name
h = Heading(title, parent=self)
h.toggled.connect(self.heading_toggled)
self.widgets.append(h), self.layout().addWidget(h)
for i, declaration in enumerate(node['css']):
d = Declaration(data['html_name'], declaration, is_first=i == 0, parent=self)
d.hyperlink_activated.connect(self.hyperlink_activated)
self.widgets.append(d), self.layout().addWidget(d)
h = Heading(_('Computed final style'), parent=self)
h.toggled.connect(self.heading_toggled)
self.widgets.append(h), self.layout().addWidget(h)
ccss = data['computed_css']
declaration = {'properties':[Property([k, ccss[k][0], '', ccss[k][1]]) for k in sorted(ccss)]}
d = Declaration(None, declaration, is_first=True, parent=self)
self.widgets.append(d), self.layout().addWidget(d)
for w in self.widgets:
w.context_menu_requested.connect(self.context_menu_requested)
def heading_toggled(self, heading):
for i, w in enumerate(self.widgets):
if w is heading:
for b in self.widgets[i + 1:]:
if isinstance(b, Heading):
break
b.setVisible(heading.expanded)
break
def relayout(self):
for w in self.widgets:
w.do_layout()
w.updateGeometry()
@property
def lines_for_copy(self):
ans = []
for w in self.widgets:
ans += w.lines_for_copy
return ans
def context_menu_requested(self, widget, ev):
if isinstance(widget, Heading):
start = widget
else:
found = False
for w in reversed(self.widgets):
if w is widget:
found = True
elif found and isinstance(w, Heading):
start = w
break
else:
return
found = False
lines = []
for w in self.widgets:
if found and isinstance(w, Heading):
break
if w is start:
found = True
if found:
lines += w.lines_for_copy
if not lines:
return
block = '\n'.join(lines).replace('\xa0', ' ')
heading = lines[0]
m = QMenu(self)
m.addAction(QIcon(I('edit-copy.png')), _('Copy') + ' ' + heading.replace('\xa0', ' '), lambda : QApplication.instance().clipboard().setText(block))
all_lines = []
for w in self.widgets:
all_lines += w.lines_for_copy
all_text = '\n'.join(all_lines).replace('\xa0', ' ')
m.addAction(QIcon(I('edit-copy.png')), _('Copy everything'), lambda : QApplication.instance().clipboard().setText(all_text))
m.exec(ev.globalPos())
class Property:
__slots__ = 'name', 'value', 'important', 'color', 'specificity', 'is_overriden'
def __init__(self, prop, specificity=()):
self.name, self.value, self.important, self.color = prop
self.specificity = tuple(specificity)
self.is_overriden = False
def __repr__(self):
return '<Property name={} value={} important={} color={} specificity={} is_overriden={}>'.format(
self.name, self.value, self.important, self.color, self.specificity, self.is_overriden)
class LiveCSS(QWidget):
goto_declaration = pyqtSignal(object)
def __init__(self, preview, parent=None):
QWidget.__init__(self, parent)
self.preview = preview
preview.live_css_data.connect(self.got_live_css_data)
self.preview_is_refreshing = False
self.refresh_needed = False
preview.refresh_starting.connect(self.preview_refresh_starting)
preview.refreshed.connect(self.preview_refreshed)
self.apply_theme()
self.setAutoFillBackground(True)
self.update_timer = QTimer(self)
self.update_timer.timeout.connect(self.update_data)
self.update_timer.setSingleShot(True)
self.update_timer.setInterval(500)
self.now_showing = (None, None, None)
self.stack = s = QStackedLayout(self)
self.setLayout(s)
self.clear_label = la = QLabel('<h3>' + _(
'No style information found') + '</h3><p>' + _(
'Move the cursor inside a HTML tag to see what styles'
' apply to that tag.'))
la.setWordWrap(True)
la.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
s.addWidget(la)
self.box = box = Box(self)
box.hyperlink_activated.connect(self.goto_declaration, type=Qt.ConnectionType.QueuedConnection)
self.scroll = sc = QScrollArea(self)
sc.setWidget(box)
sc.setWidgetResizable(True)
s.addWidget(sc)
def preview_refresh_starting(self):
self.preview_is_refreshing = True
def preview_refreshed(self):
self.preview_is_refreshing = False
self.refresh_needed = True
self.start_update_timer()
def apply_theme(self):
f = self.font()
f.setFamily(tprefs['editor_font_family'] or default_font_family())
f.setPointSizeF(tprefs['editor_font_size'])
self.setFont(f)
theme = get_theme(tprefs['editor_theme'])
pal = self.palette()
pal.setColor(QPalette.ColorRole.Window, theme_color(theme, 'Normal', 'bg'))
pal.setColor(QPalette.ColorRole.WindowText, theme_color(theme, 'Normal', 'fg'))
pal.setColor(QPalette.ColorRole.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg'))
pal.setColor(QPalette.ColorRole.Link, theme_color(theme, 'Link', 'fg'))
pal.setColor(QPalette.ColorRole.LinkVisited, theme_color(theme, 'Keyword', 'fg'))
self.setPalette(pal)
if hasattr(self, 'box'):
self.box.relayout()
self.update()
def clear(self):
self.stack.setCurrentIndex(0)
def show_data(self, editor_name, sourceline, tags):
if self.preview_is_refreshing:
return
if sourceline is None:
self.clear()
else:
self.preview.request_live_css_data(editor_name, sourceline, tags)
def got_live_css_data(self, result):
maximum_specificities = {}
for node in result['nodes']:
for rule in node['css']:
self.process_rule(rule, node['ancestor_specificity'], maximum_specificities)
for node in result['nodes']:
for rule in node['css']:
for prop in rule['properties']:
if prop.specificity < maximum_specificities[prop.name]:
prop.is_overriden = True
self.display_received_live_css_data(result)
def display_received_live_css_data(self, data):
editor_name = data['editor_name']
sourceline = data['sourceline']
tags = data['tags']
if data is None or len(data['computed_css']) < 1:
if editor_name == self.current_name and (editor_name, sourceline, tags) == self.now_showing:
# Try again in a little while in case there was a transient
# error in the web view
self.start_update_timer()
return
self.clear()
return
self.now_showing = (editor_name, sourceline, tags)
data['html_name'] = editor_name
self.box.show_data(data)
self.refresh_needed = False
self.stack.setCurrentIndex(1)
def process_rule(self, rule, ancestor_specificity, maximum_specificities):
selector = rule['selector']
sheet_index = rule['sheet_index']
rule_address = rule['rule_address'] or ()
if selector is not None:
try:
specificity = [0] + list(parse(selector)[0].specificity())
except (AttributeError, TypeError, SelectorError):
specificity = [0, 0, 0, 0]
else: # style attribute
specificity = [1, 0, 0, 0]
specificity.extend((sheet_index, tuple(rule_address)))
properties = []
for prop in rule['properties']:
important = 1 if prop[-1] == 'important' else 0
p = Property(prop, [ancestor_specificity] + [important] + specificity)
properties.append(p)
if p.specificity > maximum_specificities.get(p.name, lowest_specificity):
maximum_specificities[p.name] = p.specificity
rule['properties'] = properties
href = rule['href']
if hasattr(href, 'startswith') and href.startswith(f'{FAKE_PROTOCOL}://{FAKE_HOST}'):
qurl = QUrl(href)
name = qurl.path()[1:]
if name:
rule['href'] = name
@property
def current_name(self):
return self.preview.current_name
@property
def is_visible(self):
return self.isVisible()
def showEvent(self, ev):
self.update_timer.start()
actions['auto-reload-preview'].setEnabled(True)
return QWidget.showEvent(self, ev)
def sync_to_editor(self):
self.update_data()
def update_data(self):
if not self.is_visible or self.preview_is_refreshing:
return
editor_name = self.current_name
ed = editors.get(editor_name, None)
if self.update_timer.isActive() or (ed is None and editor_name is not None):
return QTimer.singleShot(100, self.update_data)
if ed is not None:
sourceline, tags = ed.current_tag(for_position_sync=False)
if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags):
self.show_data(editor_name, sourceline, tags)
def start_update_timer(self):
if self.is_visible:
self.update_timer.start()
def stop_update_timer(self):
self.update_timer.stop()
def navigate_to_declaration(self, data, editor):
if data['type'] == 'inline':
sourceline, tags = data['sourceline_address']
editor.goto_sourceline(sourceline, tags, attribute='style')
elif data['type'] == 'sheet':
editor.goto_css_rule(data['rule_address'])
elif data['type'] == 'elem':
editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])