diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index e97e134b2d..34c7b1202b 100644 Binary files a/resources/compiled_coffeescript.zip and b/resources/compiled_coffeescript.zip differ diff --git a/src/calibre/ebooks/oeb/polish/preview.coffee b/src/calibre/ebooks/oeb/polish/preview.coffee index 95181b571d..f0796d94fa 100644 --- a/src/calibre/ebooks/oeb/polish/preview.coffee +++ b/src/calibre/ebooks/oeb/polish/preview.coffee @@ -189,7 +189,7 @@ process_rules = (node, cssRules, address, sheet, sheet_index, matching_selectors break properties = get_style_properties(rule.style, all_properties, node_style, is_ancestor) if properties.length > 0 - data = {'selector':st, 'type':type, 'href':href, 'properties':properties, 'is_ancestor':is_ancestor, 'rule_address':rule_address, 'sheet_index':sheet_index} + data = {'selector':st, 'type':type, 'href':href, 'properties':properties, 'rule_address':rule_address, 'sheet_index':sheet_index} ans.push(data) get_matched_css = (node, is_ancestor, all_properties) -> @@ -211,7 +211,7 @@ get_matched_css = (node, is_ancestor, all_properties) -> if node.getAttribute('style') properties = get_style_properties(node.style, all_properties, node_style, is_ancestor) if properties.length > 0 - data = {'selector':null, 'type':'inline', 'href':get_sourceline_address(node), 'properties':properties, 'is_ancestor':is_ancestor, 'rule_address':null, 'sheet_index':null} + data = {'selector':null, 'type':'inline', 'href':get_sourceline_address(node), 'properties':properties, 'rule_address':null, 'sheet_index':null} ans.push(data) return ans.reverse() @@ -326,7 +326,7 @@ class PreviewIntegration while target and target.ownerDocument css = get_matched_css(target, is_ancestor, all_properties) if css.length > 0 - ans['nodes'].push({'name':target.tagName?.toLowerCase(), 'css':css}) + ans['nodes'].push({'name':target.tagName?.toLowerCase(), 'css':css, 'is_ancestor':is_ancestor}) target = target.parentNode is_ancestor = true return JSON.stringify(ans) diff --git a/src/calibre/gui2/tweak_book/live_css.py b/src/calibre/gui2/tweak_book/live_css.py index affae3073a..bea8e0e750 100644 --- a/src/calibre/gui2/tweak_book/live_css.py +++ b/src/calibre/gui2/tweak_book/live_css.py @@ -6,12 +6,194 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import json +import json, math -from PyQt4.Qt import (QWidget, QTimer) +from PyQt4.Qt import ( + QWidget, QTimer, QStackedLayout, QLabel, QScrollArea, QVBoxLayout, + QPainter, Qt, QFontInfo, QPalette, QRect, QSize, QSizePolicy) from calibre.constants import iswindows -from calibre.gui2.tweak_book import editors, actions, current_container +from calibre.gui2.tweak_book import editors, actions, current_container, tprefs +from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color +from calibre.gui2.tweak_book.editor.text import default_font_family + +class Heading(QWidget): + + def __init__(self, text, expanded=True, parent=None): + QWidget.__init__(self, parent) + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) + self.setCursor(Qt.PointingHandCursor) + self.text = text + self.expanded = expanded + self.hovering = False + self.do_layout() + + def do_layout(self): + try: + f = self.parent().font() + except AttributeError: + return + f.setBold(True) + sz = QFontInfo(f).pointSize() + f.setPointSize(int(math.ceil(1.2 * sz))) + self.setFont(f) + + @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.AlternateBase) + if self.hovering: + bg = bg.lighter(115) + p.fillRect(ev.rect(), bg) + try: + p.drawText(ev.rect(), Qt.AlignLeft|Qt.AlignVCenter|Qt.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) + +class Cell(object): + + SIDE_MARGIN = 5 + FLAGS = Qt.AlignVCenter | Qt.TextSingleLine | Qt.TextIncludeTrailingSpaces + + def __init__(self, text, rect, right_align=False, color_role=QPalette.WindowText): + self.rect, self.text = rect, text + self.right_align = right_align + self.color_role = color_role + + def draw(self, painter, width, palette): + flags = self.FLAGS | (Qt.AlignRight if self.right_align else Qt.AlignLeft) + rect = QRect(self.rect) + if self.right_align: + rect.setRight(width - self.SIDE_MARGIN) + painter.setPen(palette.color(self.color_role)) + painter.drawText(rect, flags, self.text) + +class Declaration(QWidget): + + def __init__(self, html_name, data, is_first=False, parent=None): + QWidget.__init__(self, parent) + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) + self.data = data + self.is_first = is_first + self.html_name = html_name + self.do_layout() + + def do_layout(self): + fm = self.fontMetrics() + bounding_rect = lambda text: fm.boundingRect(0, 0, 10000, 10000, Cell.FLAGS, text) + line_spacing = 5 + 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.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 + + for (name, value, important) in self.data['properties']: + text = name + ':\xa0' + br1 = bounding_rect(text) + vtext = value + '\xa0' + ('!' if important else '') + important + br2 = bounding_rect(vtext) + self.rows.append([ + Cell(text, QRect(side_margin, ypos, br1.width(), br1.height()), color_role=QPalette.LinkVisited), + Cell(vtext, QRect(br1.right() + side_margin, ypos, br2.width(), br2.height())) + ]) + ypos += max(br1.height(), br2.height()) + line_spacing + + 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.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() + + +class Box(QWidget): + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.l = l = QVBoxLayout(self) + l.setAlignment(Qt.AlignTop) + self.setLayout(l) + self.widgets = [] + + def show_data(self, data): + for w in self.widgets: + self.layout().removeWidget(w) + w.deleteLater() + self.widgets = [] + for node in data['nodes']: + node_name = node['name'] + if node['is_ancestor']: + title = _('Inherited from %s') % node_name + else: + title = _('Matched CSS rules for %s') % node_name + h = Heading(title, parent=self) + 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) + self.widgets.append(d), self.layout().addWidget(d) + + h = Heading(_('Computed final style'), parent=self) + self.widgets.append(h), self.layout().addWidget(h) + keys = sorted(data['computed_css']) + declaration = {'properties':[[k, data['computed_css'][k], ''] for k in keys]} + d = Declaration(None, declaration, is_first=True, parent=self) + self.widgets.append(d), self.layout().addWidget(d) + + def relayout(self): + for w in self.widgets: + w.do_layout() + w.updateGeometry() class LiveCSS(QWidget): @@ -19,21 +201,61 @@ class LiveCSS(QWidget): QWidget.__init__(self, parent) self.preview = preview preview.refreshed.connect(self.update_data) + 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.stack = s = QStackedLayout(self) + self.setLayout(s) + + self.clear_label = la = QLabel('

' + _( + 'No style information found') + '

' + _( + 'Move the cursor inside a HTML tag to see what styles' + ' apply to that tag.')) + la.setWordWrap(True) + la.setAlignment(Qt.AlignTop | Qt.AlignLeft) + s.addWidget(la) + + self.box = box = Box(self) + self.scroll = sc = QScrollArea(self) + sc.setWidget(box) + sc.setWidgetResizable(True) + s.addWidget(sc) + + def apply_theme(self): + f = self.font() + f.setFamily(tprefs['editor_font_family'] or default_font_family()) + f.setPointSize(tprefs['editor_font_size']) + self.setFont(f) + theme = THEMES.get(tprefs['editor_theme'], None) + if theme is None: + theme = THEMES[default_theme()] + pal = self.palette() + pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) + pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) + pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) + pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) + self.setPalette(pal) + if hasattr(self, 'box'): + self.box.relayout() + self.update() + def clear(self): - pass # TODO: Implement this + self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) - if data is None: + if data is None or len(data['computed_css']) < 1: self.clear() return + data['html_name'] = editor_name + self.box.show_data(data) + self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index e882d78bf0..27602e3fae 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -261,6 +261,7 @@ class Main(MainWindow): area = getattr(Qt, '%sDockWidgetArea' % capitalize({'vertical':h, 'horizontal':v}[pref])) self.setCorner(getattr(Qt, '%s%sCorner' % tuple(map(capitalize, (v, h)))), area) self.preview.apply_settings() + self.live_css.apply_theme() def show_status_message(self, msg, timeout=5): self.status_bar.showMessage(msg, int(timeout*1000))