From 1b8697391bb9a11c773ec8b76dc0c029dd38414b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 27 May 2014 21:22:21 +0530 Subject: [PATCH] Start work on custom theme support for the editor --- src/calibre/gui2/tweak_book/__init__.py | 1 + src/calibre/gui2/tweak_book/editor/themes.py | 290 ++++++++++++++++++- 2 files changed, 289 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index 38db2bb41a..31deac68ac 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -47,6 +47,7 @@ d['saved_searches'] = [] d['insert_tag_mru'] = ['p', 'div', 'li', 'h1', 'h2', 'h3', 'h4', 'em', 'strong', 'td', 'tr'] d['spell_check_case_sensitive_sort'] = False d['inline_spell_check'] = True +d['custom_themes'] = {} del d diff --git a/src/calibre/gui2/tweak_book/editor/themes.py b/src/calibre/gui2/tweak_book/editor/themes.py index ec4d102875..abdc439c62 100644 --- a/src/calibre/gui2/tweak_book/editor/themes.py +++ b/src/calibre/gui2/tweak_book/editor/themes.py @@ -8,9 +8,16 @@ __copyright__ = '2013, Kovid Goyal ' from collections import namedtuple -from PyQt4.Qt import (QColor, QBrush, QFont, QApplication, QPalette) +from PyQt4.Qt import ( + QColor, QBrush, QFont, QApplication, QPalette, QComboBox, + QPushButton, QIcon, QFormLayout, QLineEdit, QWidget, QScrollArea, + QVBoxLayout, Qt, QHBoxLayout, pyqtSignal, QPixmap, QColorDialog, + QToolButton, QCheckBox, QSize, QLabel) +from calibre.gui2 import error_dialog +from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat +from calibre.gui2.tweak_book.widgets import Dialog underline_styles = {'single', 'dash', 'dot', 'dash_dot', 'dash_dot_dot', 'wave', 'spell'} @@ -226,11 +233,24 @@ def u(x): return x + 'Underline' underline_styles = {x:getattr(SyntaxTextCharFormat, u(x)) for x in underline_styles} +def to_highlight(data): + data = data.copy() + for c in ('fg', 'bg', 'uc'): + data[c] = read_color(data[c]) if c in data else None + return Highlight(**data) + def get_theme(name): try: return THEMES[name] except KeyError: - return THEMES[default_theme()] + try: + ans = tprefs['custom_themes'][name] + except KeyError: + return THEMES[default_theme()] + else: + dt = THEMES[default_theme()].copy() + dt.update({k:to_highlight(v) for k, v in ans.iteritems()}) + return dt def highlight_to_char_format(h): ans = SyntaxTextCharFormat() @@ -260,3 +280,269 @@ def theme_format(theme, name): except KeyError: h = THEMES[default_theme()][name] return highlight_to_char_format(h) + +def custom_theme_names(): + return tuple(tprefs['custom_themes'].iterkeys()) + +def builtin_theme_names(): + return tuple(THEMES.iterkeys()) + +def all_theme_names(): + return builtin_theme_names() + custom_theme_names() + +class CreateNewTheme(Dialog): + + def __init__(self, parent=None): + Dialog.__init__(self, _('Create custom theme'), 'custom-theme-create', parent=parent) + + def setup_ui(self): + self.l = l = QFormLayout(self) + self.setLayout(l) + + self._name = n = QLineEdit(self) + l.addRow(_('&Name of custom theme:'), n) + + self.base = b = QComboBox(self) + b.addItems(sorted(builtin_theme_names())) + l.addRow(_('&Builtin theme to base on:'), b) + idx = b.findText(tprefs['editor_theme'] or default_theme()) + if idx == -1: + idx = b.findText(default_theme()) + b.setCurrentIndex(idx) + + l.addRow(self.bb) + + @property + def theme_name(self): + return unicode(self._name.text()).strip() + + def accept(self): + if not self.theme_name: + return error_dialog(self, _('No name specified'), _( + 'You must specify a name for your theme'), show=True) + if '*' + self.theme_name in custom_theme_names(): + return error_dialog(self, _('Name already used'), _( + 'A custom theme with the name %s already exists') % self.theme_name, show=True) + return Dialog.accept(self) + +def col_to_string(color): + return '%02X%02X%02X' % color.getRgb()[:3] + +class ColorButton(QPushButton): + + changed = pyqtSignal() + + def __init__(self, data, name, text, parent): + QPushButton.__init__(self, text, parent) + self.ic = QPixmap(self.iconSize()) + color = data[name] + self.data, self.name = data, name + if color is not None: + self.current_color = read_color(color).color() + self.ic.fill(self.current_color) + else: + self.ic.fill(Qt.transparent) + self.current_color = color + self.update_tooltip() + self.setIcon(QIcon(self.ic)) + self.clicked.connect(self.choose_color) + + def clear(self): + self.current_color = None + self.update_tooltip() + self.ic.fill(Qt.transparent) + self.setIcon(QIcon(self.ic)) + self.data[self.name] = self.value + self.changed.emit() + + def choose_color(self): + col = QColorDialog.getColor(self.current_color or Qt.black, self, _('Choose color')) + if col.isValid(): + self.current_color = col + self.update_tooltip() + self.ic.fill(col) + self.setIcon(QIcon(self.ic)) + self.data[self.name] = self.value + self.changed.emit() + + def update_tooltip(self): + self.setToolTip(_('Red: {0} Green: {1} Blue: {2}').format(*self.current_color.getRgb()[:3]) if self.current_color else _('No color')) + + @property + def value(self): + if self.current_color is None: + return None + return col_to_string(self.current_color) + +class Bool(QCheckBox): + + changed = pyqtSignal() + + def __init__(self, data, key, text, parent): + QCheckBox.__init__(self, text, parent) + self.data, self.key = data, key + self.setChecked(data.get(key, False)) + self.stateChanged.connect(self._changed) + + def _changed(self, state): + self.data[self.key] = self.value + self.changed.emit() + + @property + def value(self): + return self.checkState() == Qt.Checked + +class Property(QWidget): + + changed = pyqtSignal() + + def __init__(self, name, data, parent=None): + QWidget.__init__(self, parent) + self.l = l = QHBoxLayout(self) + self.setLayout(l) + self.label = QLabel(name) + l.addWidget(self.label) + self.data = data + + def create_color_button(key, text): + b = ColorButton(data, key, text, self) + b.changed.connect(self.changed), l.addWidget(b) + bc = QToolButton(self) + bc.setIcon(QIcon(I('clear_left.png'))) + bc.setToolTip(_('Remove color')) + bc.clicked.connect(b.clear) + h = QHBoxLayout() + h.addWidget(b), h.addWidget(bc) + return h + + for k, text in (('fg', _('&Foreground')), ('bg', _('&Background'))): + h = create_color_button(k, text) + l.addLayout(h) + + for k, text in (('bold', _('B&old')), ('italic', _('&Italic'))): + w = Bool(data, k, text, self) + w.changed.connect(self.changed) + l.addWidget(w) + + self.underline = us = QComboBox(self) + us.addItems(sorted(tuple(underline_styles) + ('',))) + idx = us.findText(data.get('underline', '') or '') + us.setCurrentIndex(max(idx, 0)) + us.currentIndexChanged.connect(self.us_changed) + self.la = la = QLabel(_('&Underline:')) + la.setBuddy(us) + h = QHBoxLayout() + h.addWidget(la), h.addWidget(us), l.addLayout(h) + + h = create_color_button('underline_color', _('Color')) + l.addLayout(h) + l.addStretch(1) + + def us_changed(self): + self.data['underline'] = unicode(self.underline.currentText()) + self.changed.emit() + +class ThemeEditor(Dialog): + + def __init__(self, parent=None): + Dialog.__init__(self, _('Create/edit custom theme'), 'custom-theme-editor', parent=parent) + + def setup_ui(self): + self.block_show = False + self.properties = [] + self.l = l = QVBoxLayout(self) + self.setLayout(l) + h = QHBoxLayout() + l.addLayout(h) + self.la = la = QLabel(_('&Edit theme:')) + h.addWidget(la) + self.theme = t = QComboBox(self) + la.setBuddy(t) + t.addItems(sorted(custom_theme_names())) + t.setMinimumWidth(200) + if t.count() > 0: + t.setCurrentIndex(0) + t.currentIndexChanged[int].connect(self.show_theme) + h.addWidget(t) + + self.add_button = b = QPushButton(QIcon(I('plus.png')), _('Add &new theme'), self) + b.clicked.connect(self.create_new_theme) + h.addWidget(b) + h.addStretch(1) + + self.scroll = s = QScrollArea(self) + self.w = w = QWidget(self) + s.setWidget(w), s.setWidgetResizable(True) + + self.cl = cl = QVBoxLayout() + w.setLayout(cl) + l.addWidget(s) + + self.bb.clear() + self.bb.addButton(self.bb.Close) + l.addWidget(self.bb) + + if self.theme.count() > 0: + self.show_theme() + + def update_theme(self, name): + data = tprefs['custom_themes'][name] + extra = set(data.iterkeys()) - set(THEMES[default_theme()].iterkeys()) + missing = set(THEMES[default_theme()].iterkeys()) - set(data.iterkeys()) + for k in extra: + data.pop(k) + for k in missing: + data[k] = dict(THEMES[default_theme()][k]._asdict()) + if extra or missing: + tprefs['custom_themes'][name] = data + return data + + def show_theme(self): + if self.block_show: + return + for c in self.properties: + c.changed.disconnect() + self.cl.removeWidget(c) + c.setParent(None) + c.deleteLater() + self.properties = [] + name = unicode(self.theme.currentText()) + data = self.update_theme(name) + maxw = 0 + for k in sorted(data): + w = Property(k, data[k], parent=self) + w.changed.connect(self.changed) + self.properties.append(w) + maxw = max(maxw, w.label.sizeHint().width()) + self.cl.addWidget(w) + for p in self.properties: + p.label.setMinimumWidth(maxw), p.label.setMaximumWidth(maxw) + + def changed(self): + pass + + def create_new_theme(self): + d = CreateNewTheme(self) + if d.exec_() == d.Accepted: + name = '*' + d.theme_name + base = unicode(d.base.currentText()) + theme = {} + for key, val in THEMES[base].iteritems(): + theme[key] = {k:col_to_string(v.color()) if isinstance(v, QBrush) else v for k, v in val._asdict().iteritems()} + tprefs['custom_themes'][name] = theme + tprefs['custom_themes'] = tprefs['custom_themes'] + t = self.theme + self.block_show = True + t.clear(), t.addItems(sorted(custom_theme_names())) + t.setCurrentIndex(t.findText(name)) + self.block_show = False + self.show_theme() + + def sizeHint(self): + return QSize(1000, 650) + +if __name__ == '__main__': + app = QApplication([]) + d = ThemeEditor() + d.exec_() + del app