mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Display CSS report
This commit is contained in:
parent
abdcbcdd82
commit
a347091b4d
@ -122,6 +122,8 @@ def chars_data(container, book_locale):
|
|||||||
CSSRule = namedtuple('CSSRule', 'selector location')
|
CSSRule = namedtuple('CSSRule', 'selector location')
|
||||||
RuleLocation = namedtuple('RuleLocation', 'file_name line column')
|
RuleLocation = namedtuple('RuleLocation', 'file_name line column')
|
||||||
MatchLocation = namedtuple('MatchLocation', 'tag sourceline')
|
MatchLocation = namedtuple('MatchLocation', 'tag sourceline')
|
||||||
|
CSSEntry = namedtuple('CSSEntry', 'rule count matched_files sort_key')
|
||||||
|
CSSFileMatch = namedtuple('CSSFileMatch', 'file_name locations sort_key')
|
||||||
|
|
||||||
def css_data(container, book_locale):
|
def css_data(container, book_locale):
|
||||||
import tinycss
|
import tinycss
|
||||||
@ -215,9 +217,9 @@ def css_data(container, book_locale):
|
|||||||
|
|
||||||
ans = []
|
ans = []
|
||||||
for rule, loc_map in rule_map.iteritems():
|
for rule, loc_map in rule_map.iteritems():
|
||||||
la = [(name, locations) for name, locations in loc_map.iteritems() if locations]
|
la = tuple(CSSFileMatch(name, tuple(locations), numeric_sort_key(name)) for name, locations in loc_map.iteritems() if locations)
|
||||||
if la:
|
count = sum(len(fm.locations) for fm in la)
|
||||||
ans.append((rule, la))
|
ans.append(CSSEntry(rule, count, la, numeric_sort_key(rule)))
|
||||||
|
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
@ -19,10 +19,12 @@ from PyQt5.Qt import (
|
|||||||
QSize, QStackedLayout, QLabel, QVBoxLayout, Qt, QWidget, pyqtSignal,
|
QSize, QStackedLayout, QLabel, QVBoxLayout, Qt, QWidget, pyqtSignal,
|
||||||
QAbstractTableModel, QTableView, QSortFilterProxyModel, QIcon, QListWidget,
|
QAbstractTableModel, QTableView, QSortFilterProxyModel, QIcon, QListWidget,
|
||||||
QListWidgetItem, QLineEdit, QStackedWidget, QSplitter, QByteArray, QPixmap,
|
QListWidgetItem, QLineEdit, QStackedWidget, QSplitter, QByteArray, QPixmap,
|
||||||
QStyledItemDelegate, QModelIndex, QRect, QStyle, QPalette, QTimer, QMenu)
|
QStyledItemDelegate, QModelIndex, QRect, QStyle, QPalette, QTimer, QMenu,
|
||||||
|
QAbstractItemModel, QTreeView, QFont, QRadioButton, QHBoxLayout,
|
||||||
|
QFontDatabase, QComboBox)
|
||||||
|
|
||||||
from calibre import human_readable, fit_image
|
from calibre import human_readable, fit_image
|
||||||
from calibre.ebooks.oeb.polish.report import gather_data
|
from calibre.ebooks.oeb.polish.report import gather_data, CSSEntry, CSSFileMatch, MatchLocation
|
||||||
from calibre.gui2 import error_dialog, question_dialog, choose_save_file
|
from calibre.gui2 import error_dialog, question_dialog, choose_save_file
|
||||||
from calibre.gui2.tweak_book import current_container, tprefs, dictionaries
|
from calibre.gui2.tweak_book import current_container, tprefs, dictionaries
|
||||||
from calibre.gui2.tweak_book.widgets import Dialog
|
from calibre.gui2.tweak_book.widgets import Dialog
|
||||||
@ -32,6 +34,8 @@ from calibre.utils.localization import calibre_langcode_to_name, canonicalize_la
|
|||||||
|
|
||||||
# Utils {{{
|
# Utils {{{
|
||||||
|
|
||||||
|
ROOT = QModelIndex()
|
||||||
|
|
||||||
def read_state(name, default=None):
|
def read_state(name, default=None):
|
||||||
data = tprefs.get('reports-ui-state')
|
data = tprefs.get('reports-ui-state')
|
||||||
if data is None:
|
if data is None:
|
||||||
@ -326,7 +330,7 @@ class ImagesDelegate(QStyledItemDelegate):
|
|||||||
return QSize(max(width + m, ans.width()), height + m + self.MARGIN + ans.height())
|
return QSize(max(width + m, ans.width()), height + m + self.MARGIN + ans.height())
|
||||||
|
|
||||||
def paint(self, painter, option, index):
|
def paint(self, painter, option, index):
|
||||||
QStyledItemDelegate.paint(self, painter, option, QModelIndex())
|
QStyledItemDelegate.paint(self, painter, option, ROOT)
|
||||||
entry = index.data(Qt.UserRole)
|
entry = index.data(Qt.UserRole)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
return
|
return
|
||||||
@ -646,6 +650,196 @@ class CharsWidget(QWidget):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# CSS {{{
|
||||||
|
|
||||||
|
class CSSRulesModel(QAbstractItemModel):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QAbstractItemModel.__init__(self, parent)
|
||||||
|
self.rules = ()
|
||||||
|
self.num_size = 1
|
||||||
|
self.build_maps()
|
||||||
|
self.main_font = f = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
||||||
|
f.setBold(True), f.setPointSize(parent.font().pointSize() + 2)
|
||||||
|
self.italic_font = f = QFont(parent.font())
|
||||||
|
f.setItalic(True)
|
||||||
|
|
||||||
|
def build_maps(self):
|
||||||
|
self.parent_map = pm = {}
|
||||||
|
for i, entry in enumerate(self.rules):
|
||||||
|
container = entry.matched_files
|
||||||
|
pm[container] = (i, self.rules)
|
||||||
|
for i, child in enumerate(container):
|
||||||
|
gcontainer = child.locations
|
||||||
|
pm[gcontainer] = (i, container)
|
||||||
|
for i, gc in enumerate(gcontainer):
|
||||||
|
pm[gc] = (i, gcontainer)
|
||||||
|
|
||||||
|
def index(self, row, column, parent=ROOT):
|
||||||
|
container = self.to_container(self.index_to_entry(parent) or self.rules)
|
||||||
|
return self.createIndex(row, column, container) if -1 < row < len(container) else ROOT
|
||||||
|
|
||||||
|
def to_container(self, entry):
|
||||||
|
if isinstance(entry, CSSEntry):
|
||||||
|
return entry.matched_files
|
||||||
|
elif isinstance(entry, CSSFileMatch):
|
||||||
|
return entry.locations
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def index_to_entry(self, index):
|
||||||
|
if index.isValid():
|
||||||
|
try:
|
||||||
|
return index.internalPointer()[index.row()]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parent(self, index):
|
||||||
|
if not index.isValid():
|
||||||
|
return ROOT
|
||||||
|
parent = index.internalPointer()
|
||||||
|
if parent is self.rules or parent is None:
|
||||||
|
return ROOT
|
||||||
|
try:
|
||||||
|
pidx, grand_parent = self.parent_map[parent]
|
||||||
|
except KeyError:
|
||||||
|
return ROOT
|
||||||
|
return self.createIndex(pidx, 0, grand_parent)
|
||||||
|
|
||||||
|
def rowCount(self, parent=ROOT):
|
||||||
|
if not parent.isValid():
|
||||||
|
return len(self.rules)
|
||||||
|
entry = self.index_to_entry(parent)
|
||||||
|
if isinstance(entry, (CSSEntry, CSSFileMatch)):
|
||||||
|
return len(self.to_container(entry))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def columnCount(self, parent=ROOT):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def data(self, index, role=Qt.DisplayRole):
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
entry = self.index_to_entry(index)
|
||||||
|
if isinstance(entry, CSSEntry):
|
||||||
|
return '[%{}d] %s'.format(self.num_size) % (entry.count, entry.rule.selector)
|
||||||
|
elif isinstance(entry, CSSFileMatch):
|
||||||
|
return _('%s [%d elements]') % (entry.file_name, len(entry.locations))
|
||||||
|
elif isinstance(entry, MatchLocation):
|
||||||
|
return '%s @ %s' % (entry.tag, entry.sourceline)
|
||||||
|
elif role == Qt.UserRole:
|
||||||
|
return self.index_to_entry(index)
|
||||||
|
elif role == Qt.FontRole:
|
||||||
|
entry = self.index_to_entry(index)
|
||||||
|
if isinstance(entry, CSSEntry):
|
||||||
|
return self.main_font
|
||||||
|
elif isinstance(entry, CSSFileMatch):
|
||||||
|
return self.italic_font
|
||||||
|
|
||||||
|
def __call__(self, data):
|
||||||
|
self.beginResetModel()
|
||||||
|
self.rules = data['css']
|
||||||
|
try:
|
||||||
|
self.num_size = len(str(max(r.count for r in self.rules)))
|
||||||
|
except ValueError:
|
||||||
|
self.num_size = 1
|
||||||
|
self.build_maps()
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
class CSSProxyModel(QSortFilterProxyModel):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QSortFilterProxyModel.__init__(self, parent)
|
||||||
|
self._filter_text = None
|
||||||
|
self.sort_on_count = True
|
||||||
|
|
||||||
|
def filter_text(self, text):
|
||||||
|
self._filter_text = text
|
||||||
|
self.setFilterFixedString(text)
|
||||||
|
|
||||||
|
def filterAcceptsRow(self, row, parent):
|
||||||
|
if not self._filter_text:
|
||||||
|
return True
|
||||||
|
sm = self.sourceModel()
|
||||||
|
entry = sm.index_to_entry(sm.index(row, 0, parent))
|
||||||
|
if not isinstance(entry, CSSEntry):
|
||||||
|
return True
|
||||||
|
return primary_contains(self._filter_text, entry.rule.selector)
|
||||||
|
|
||||||
|
def lessThan(self, left, right):
|
||||||
|
sm = self.sourceModel()
|
||||||
|
left, right = sm.index_to_entry(left), sm.index_to_entry(right)
|
||||||
|
if isinstance(left, CSSEntry) and isinstance(right, CSSEntry):
|
||||||
|
if self.sort_on_count:
|
||||||
|
return left.count < right.count
|
||||||
|
return left.sort_key < right.sort_key
|
||||||
|
if isinstance(left, CSSFileMatch) and isinstance(right, CSSFileMatch):
|
||||||
|
if self.sort_on_count:
|
||||||
|
return len(left.locations) < len(right.locations)
|
||||||
|
return left.sort_key < right.sort_key
|
||||||
|
if isinstance(left, MatchLocation) and isinstance(right, MatchLocation):
|
||||||
|
return left.sourceline < right.sourceline
|
||||||
|
return False
|
||||||
|
|
||||||
|
class CSSWidget(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.l = l = QVBoxLayout(self)
|
||||||
|
self.h = h = QHBoxLayout()
|
||||||
|
|
||||||
|
self.filter_edit = e = QLineEdit(self)
|
||||||
|
l.addWidget(e)
|
||||||
|
e.setPlaceholderText(_('Filter'))
|
||||||
|
self.model = m = CSSRulesModel(self)
|
||||||
|
self.proxy = p = CSSProxyModel(self)
|
||||||
|
p.setSourceModel(m)
|
||||||
|
self.view = f = QTreeView(self)
|
||||||
|
f.setHeaderHidden(True)
|
||||||
|
f.setModel(p)
|
||||||
|
l.addWidget(f)
|
||||||
|
e.textChanged.connect(p.filter_text)
|
||||||
|
|
||||||
|
l.addLayout(h)
|
||||||
|
h.addWidget(QLabel(_('Sort by:')))
|
||||||
|
self.counts_button = b = QRadioButton(_('&Counts'), self)
|
||||||
|
b.setChecked(read_state('css-sort-on-counts', True))
|
||||||
|
h.addWidget(b)
|
||||||
|
self.name_button = b = QRadioButton(_('&Name'), self)
|
||||||
|
b.setChecked(not read_state('css-sort-on-counts', True))
|
||||||
|
h.addWidget(b)
|
||||||
|
b.toggled.connect(self.resort)
|
||||||
|
h.addStrut(20)
|
||||||
|
self._sort_order = o = QComboBox(self)
|
||||||
|
o.addItems([_('Ascending'), _('Descending')])
|
||||||
|
o.setCurrentIndex(0 if read_state('css-sort-ascending', True) else 1)
|
||||||
|
o.setEditable(False)
|
||||||
|
o.currentIndexChanged[int].connect(self.resort)
|
||||||
|
h.addWidget(o)
|
||||||
|
h.addStretch(10)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def sort_order(self):
|
||||||
|
def fget(self):
|
||||||
|
return [Qt.AscendingOrder, Qt.DescendingOrder][self._sort_order.currentIndex()]
|
||||||
|
def fset(self, val):
|
||||||
|
self._sort_order.setCurrentIndex({Qt.AscendingOrder:0}.get(val, 1))
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def __call__(self, data):
|
||||||
|
self.model(data)
|
||||||
|
self.filter_edit.clear()
|
||||||
|
self.resort()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
save_state('css-sort-on-counts', self.counts_button.isChecked())
|
||||||
|
save_state('css-sort-ascending', self.sort_order == Qt.AscendingOrder)
|
||||||
|
|
||||||
|
def resort(self, *args):
|
||||||
|
self.proxy.sort_on_count = self.counts_button.isChecked()
|
||||||
|
self.proxy.sort(-1, self.sort_order) # for some reason the proxy model does not resort without this
|
||||||
|
self.proxy.sort(0, self.sort_order)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
# Wrapper UI {{{
|
# Wrapper UI {{{
|
||||||
class ReportsWidget(QWidget):
|
class ReportsWidget(QWidget):
|
||||||
|
|
||||||
@ -680,6 +874,10 @@ class ReportsWidget(QWidget):
|
|||||||
s.addWidget(i)
|
s.addWidget(i)
|
||||||
QListWidgetItem(_('Images'), r)
|
QListWidgetItem(_('Images'), r)
|
||||||
|
|
||||||
|
self.css = c = CSSWidget(self)
|
||||||
|
s.addWidget(c)
|
||||||
|
QListWidgetItem(_('Style Rules'), r)
|
||||||
|
|
||||||
self.chars = c = CharsWidget(self)
|
self.chars = c = CharsWidget(self)
|
||||||
s.addWidget(c)
|
s.addWidget(c)
|
||||||
QListWidgetItem(_('Characters'), r)
|
QListWidgetItem(_('Characters'), r)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user