%PDF- %PDF-
Direktori : /proc/thread-self/root/usr/lib/calibre/calibre/gui2/tweak_book/editor/ |
Current File : //proc/thread-self/root/usr/lib/calibre/calibre/gui2/tweak_book/editor/themes.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' from collections import namedtuple from qt.core import ( QColor, QBrush, QFont, QApplication, QPalette, QComboBox, QPushButton, QIcon, QFormLayout, QLineEdit, QWidget, QScrollArea, QVBoxLayout, Qt, QHBoxLayout, pyqtSignal, QPixmap, QColorDialog, QDialog, QToolButton, QCheckBox, QSize, QLabel, QSplitter, QTextCharFormat, QDialogButtonBox) from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.editor import syntax_text_char_format from calibre.gui2.tweak_book.widgets import Dialog from polyglot.builtins import iteritems underline_styles = {'single', 'dash', 'dot', 'dash_dot', 'dash_dot_dot', 'wave', 'spell'} _default_theme = None def default_theme(): global _default_theme if _default_theme is None: isdark = QApplication.instance().palette().color(QPalette.ColorRole.WindowText).lightness() > 128 _default_theme = 'wombat-dark' if isdark else 'pyte-light' return _default_theme # The solarized themes {{{ SLDX = {'base03':'1c1c1c', 'base02':'262626', 'base01':'585858', 'base00':'626262', 'base0':'808080', 'base1':'8a8a8a', 'base2':'e4e4e4', 'base3':'ffffd7', 'yellow':'af8700', 'orange':'d75f00', 'red':'d70000', 'magenta':'af005f', 'violet':'5f5faf', 'blue':'0087ff', 'cyan':'00afaf', 'green':'5f8700'} # noqa SLD = {'base03':'002b36', 'base02':'073642', 'base01':'586e75', 'base00':'657b83', 'base0':'839496', 'base1':'93a1a1', 'base2':'eee8d5', 'base3':'fdf6e3', 'yellow':'b58900', 'orange':'cb4b16', 'red':'dc322f', 'magenta':'d33682', 'violet':'6c71c4', 'blue':'268bd2', 'cyan':'2aa198', 'green':'859900'} # noqa m = {'base%d'%n:'base%02d'%n for n in range(1, 4)} m.update({'base%02d'%n:'base%d'%n for n in range(1, 4)}) SLL = {m.get(k, k) : v for k, v in iteritems(SLD)} SLLX = {m.get(k, k) : v for k, v in iteritems(SLDX)} SOLARIZED = \ ''' CursorLine bg={base02} CursorColumn bg={base02} ColorColumn bg={base02} HighlightRegion bg={base00} MatchParen bg={base02} fg={magenta} Pmenu fg={base0} bg={base02} PmenuSel fg={base01} bg={base2} Cursor fg={base03} bg={base0} Normal fg={base0} bg={base02} LineNr fg={base01} bg={base02} LineNrC fg={magenta} Visual fg={base01} bg={base03} Comment fg={base01} italic Todo fg={magenta} bold String fg={cyan} Constant fg={cyan} Number fg={cyan} PreProc fg={orange} Identifier fg={blue} Function fg={blue} Type fg={yellow} Statement fg={green} bold Keyword fg={green} Special fg={red} SpecialCharacter bg={base02} Error us=wave uc={red} SpellError us=wave uc={orange} Tooltip fg=black bg=ffffed Link fg={blue} BadLink fg={cyan} us=wave uc={red} DiffDelete bg={base02} fg={red} DiffInsert bg={base02} fg={green} DiffReplace bg={base02} fg={blue} DiffReplaceReplace bg={base03} ''' # }}} THEMES = { 'wombat-dark': # {{{ ''' CursorLine bg={cursor_loc} CursorColumn bg={cursor_loc} ColorColumn bg={cursor_loc} HighlightRegion bg=3d3d3d MatchParen bg=444444 Pmenu fg=f6f3e8 bg=444444 PmenuSel fg=yellow bg={identifier} Tooltip fg=black bg=ffffed Cursor bg=656565 Normal fg=f6f3e8 bg=242424 LineNr fg=857b6f bg=000000 LineNrC fg=yellow Visual fg=black bg=888888 Comment fg={comment} Todo fg=8f8f8f String fg={string} Constant fg={constant} Number fg={constant} PreProc fg={constant} Identifier fg={identifier} Function fg={identifier} Type fg={identifier} Statement fg={keyword} Keyword fg={keyword} Special fg={special} Error us=wave uc=red SpellError us=wave uc=orange SpecialCharacter bg={cursor_loc} Link fg=cyan BadLink fg={string} us=wave uc=red DiffDelete bg=341414 fg=642424 DiffInsert bg=143414 fg=246424 DiffReplace bg=141434 fg=242464 DiffReplaceReplace bg=002050 '''.format( cursor_loc='323232', identifier='cae682', comment='99968b', string='95e454', keyword='8ac6f2', constant='e5786d', special='e7f6da'), # }}} 'pyte-light': # {{{ ''' CursorLine bg={cursor_loc} CursorColumn bg={cursor_loc} ColorColumn bg={cursor_loc} HighlightRegion bg=E3F988 MatchParen bg=cfcfcf Pmenu fg=white bg=808080 PmenuSel fg=white bg=808080 Tooltip fg=black bg=ffffed Cursor fg=black bg=b0b4b8 Normal fg=404850 bg=f0f0f0 LineNr fg=white bg=8090a0 LineNrC fg=yellow Visual fg=white bg=8090a0 Comment fg={comment} italic Todo fg={comment} italic bold String fg={string} Constant fg={constant} Number fg={constant} PreProc fg={constant} Identifier fg={identifier} Function fg={identifier} Type fg={identifier} Statement fg={keyword} Keyword fg={keyword} Special fg={special} italic SpecialCharacter bg={cursor_loc} Error us=wave uc=red SpellError us=wave uc=magenta Link fg=blue BadLink fg={string} us=wave uc=red DiffDelete bg=rgb(255,180,200) fg=rgb(200,80,110) DiffInsert bg=rgb(180,255,180) fg=rgb(80,210,80) DiffReplace bg=rgb(206,226,250) fg=rgb(90,130,180) DiffReplaceReplace bg=rgb(180,210,250) '''.format( cursor_loc='F8DE7E', identifier='7b5694', comment='a0b0c0', string='4070a0', keyword='007020', constant='a07040', special='70a0d0'), # }}} 'solarized-x-dark': SOLARIZED.format(**SLDX), 'solarized-dark': SOLARIZED.format(**SLD), 'solarized-light': SOLARIZED.format(**SLL), 'solarized-x-light': SOLARIZED.format(**SLLX), } def read_color(col): if QColor.isValidColor(col): return QBrush(QColor(col)) if col.startswith('rgb('): r, g, b = map(int, (x.strip() for x in col[4:-1].split(','))) return QBrush(QColor(r, g, b)) try: r, g, b = col[0:2], col[2:4], col[4:6] r, g, b = int(r, 16), int(g, 16), int(b, 16) return QBrush(QColor(r, g, b)) except Exception: pass Highlight = namedtuple('Highlight', 'fg bg bold italic underline underline_color') def read_theme(raw): ans = {} for line in raw.splitlines(): line = line.strip() if not line or line.startswith('#'): continue bold = italic = False fg = bg = name = underline = underline_color = None line = line.partition('#')[0] for i, token in enumerate(line.split()): if i == 0: name = token else: if token == 'bold': bold = True elif token == 'italic': italic = True elif '=' in token: prefix, val = token.partition('=')[0::2] if prefix == 'us': underline = val if val in underline_styles else None elif prefix == 'uc': underline_color = read_color(val) elif prefix == 'fg': fg = read_color(val) elif prefix == 'bg': bg = read_color(val) if name is not None: ans[name] = Highlight(fg, bg, bold, italic, underline, underline_color) return ans THEMES = {k:read_theme(raw) for k, raw in iteritems(THEMES)} def u(x): x = {'spell':'SpellCheck', 'dash_dot':'DashDot', 'dash_dot_dot':'DashDotDot'}.get(x, x.capitalize()) if 'Dot' in x: return x + 'Line' return x + 'Underline' underline_styles = {x:getattr(QTextCharFormat.UnderlineStyle, u(x)) for x in underline_styles} def to_highlight(data): data = data.copy() for c in ('fg', 'bg', 'underline_color'): data[c] = read_color(data[c]) if data.get(c, None) is not None else None return Highlight(**data) def read_custom_theme(data): dt = THEMES[default_theme()].copy() dt.update({k:to_highlight(v) for k, v in iteritems(data)}) return dt def get_theme(name): try: return THEMES[name] except KeyError: try: ans = tprefs['custom_themes'][name] except KeyError: return THEMES[default_theme()] else: return read_custom_theme(ans) def highlight_to_char_format(h): ans = syntax_text_char_format() if h.bold: ans.setFontWeight(QFont.Weight.Bold) if h.italic: ans.setFontItalic(True) if h.fg is not None: ans.setForeground(h.fg) if h.bg is not None: ans.setBackground(h.bg) if h.underline: ans.setUnderlineStyle(underline_styles[h.underline]) if h.underline_color is not None: ans.setUnderlineColor(h.underline_color.color()) return ans def theme_color(theme, name, attr): try: return getattr(theme[name], attr).color() except (KeyError, AttributeError): return getattr(THEMES[default_theme()][name], attr).color() def theme_format(theme, name): try: h = theme[name] except KeyError: h = THEMES[default_theme()][name] return highlight_to_char_format(h) def custom_theme_names(): return tuple(tprefs['custom_themes']) def builtin_theme_names(): return tuple(THEMES) def all_theme_names(): return builtin_theme_names() + custom_theme_names() # Custom theme creation/editing {{{ 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 str(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.GlobalColor.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.GlobalColor.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.GlobalColor.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.CheckState.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'] = str(self.underline.currentText()) or None self.changed.emit() # Help text {{{ HELP_TEXT = _('''\ <h2>Creating a custom theme</h2> <p id="attribute" lang="und">You can create a custom syntax highlighting theme, \ with your own colors and font styles. The most important types of highlighting \ rules are described below. Note that not every rule supports every kind of \ customization, for example, changing font or underline styles for the \ <code>Cursor</code> rule does not have any effect as that rule is used only for \ the color of the blinking cursor.</p> <p>As you make changes to your theme on the left, the changes will be reflected live in this panel.</p> <p xml:lang="und"> {} The most important rule. Sets the foreground and background colors for the \ editor as well as the style of "normal" text, that is, text that does not match any special syntax. {} Defines the colors for text selected by the mouse. {} Defines the color for the line containing the cursor. {} Defines the colors for the line numbers on the left. {} Defines the colors for matching tags in HTML and matching braces in CSS. {} Used for highlighting tags in HTML {} Used for highlighting attributes in HTML {} Tag names in HTML {} Namespace prefixes in XML and constants in CSS {} Non-breaking spaces/hyphens in HTML {} Syntax errors such as <this <> {} Misspelled words such as <span lang="en">thisword</span> {} Comments like <!-- this one --> </p> <style type="text/css"> /* Some CSS so you can see how the highlighting rules affect it */ p.someclass {{ font-family: serif; font-size: 12px; line-height: 1.2; }} </style> ''') # }}} 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) self.remove_button = b = QPushButton(QIcon(I('minus.png')), _('&Remove theme'), self) b.clicked.connect(self.remove_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) from calibre.gui2.tweak_book.editor.text import TextEdit self.preview = p = TextEdit(self, expected_geometry=(73, 50)) p.load_text(HELP_TEXT.format( *('<b>%s</b>' % x for x in ( 'Normal', 'Visual', 'CursorLine', 'LineNr', 'MatchParen', 'Function', 'Type', 'Statement', 'Constant', 'SpecialCharacter', 'Error', 'SpellError', 'Comment' )) )) p.setMaximumWidth(p.size_hint.width() + 5) s.setMinimumWidth(600) self.splitter = sp = QSplitter(self) l.addWidget(sp) sp.addWidget(s), sp.addWidget(p) self.bb.clear() self.bb.addButton(QDialogButtonBox.StandardButton.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) - set(THEMES[default_theme()]) missing = set(THEMES[default_theme()]) - set(data) for k in extra: data.pop(k) for k in missing: data[k] = dict(THEMES[default_theme()][k]._asdict()) for nk, nv in iteritems(data[k]): if isinstance(nv, QBrush): data[k][nk] = str(nv.color().name()) 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 = str(self.theme.currentText()) if not name: return 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) self.preview.apply_theme(read_custom_theme(data)) @property def theme_name(self): return str(self.theme.currentText()) def changed(self): name = self.theme_name data = self.update_theme(name) self.preview.apply_theme(read_custom_theme(data)) def create_new_theme(self): d = CreateNewTheme(self) if d.exec() == QDialog.DialogCode.Accepted: name = '*' + d.theme_name base = str(d.base.currentText()) theme = {} for key, val in iteritems(THEMES[base]): theme[key] = {k:col_to_string(v.color()) if isinstance(v, QBrush) else v for k, v in iteritems(val._asdict())} 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 remove_theme(self): name = self.theme_name if name: tprefs['custom_themes'].pop(name, None) tprefs['custom_themes'] = tprefs['custom_themes'] t = self.theme self.block_show = True t.clear(), t.addItems(sorted(custom_theme_names())) if t.count() > 0: t.setCurrentIndex(0) self.block_show = False self.show_theme() def sizeHint(self): g = self.screen().availableSize() return QSize(min(1500, g.width() - 25), 650) # }}} if __name__ == '__main__': from calibre.gui2 import Application app = Application([]) d = ThemeEditor() d.exec() del app