import logging import re import math from pathlib import Path import pygments from gi.repository import Gtk from gi.repository import Gdk from gi.repository.Pango import FontDescription from gi.repository.Pango import Style from gi.repository.Pango import SCALE from gajim.common import app from gajim.plugins.plugins_i18n import _ from gajim.plugins.helpers import get_builder from syntax_highlight.gtkformatter import GTKFormatter from syntax_highlight.types import LineBreakOptions from syntax_highlight.types import CodeMarkerOptions from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID log = logging.getLogger('gajim.p.syntax_highlight') PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)', PLUGIN_INTERNAL_NONE_LEXER_ID) class SyntaxHighlighterPluginConfig(Gtk.ApplicationWindow): def __init__(self, plugin, transient): Gtk.ApplicationWindow.__init__(self) self.set_application(app.app) self.set_show_menubar(False) self.set_title(_('Syntax Highlighter Configuration')) self.set_transient_for(transient) self.set_default_size(400, 500) self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_modal(True) self.set_destroy_with_parent(True) ui_path = Path(__file__).parent self._ui = get_builder(ui_path.resolve() / 'config_dialog.ui') self.add(self._ui.main_box) self.show_all() self._ui.preview_textview.get_buffer().connect( 'insert-text', self._on_preview_text_inserted) self._ui.connect_signals(self) self._lexer_liststore = Gtk.ListStore(str) self._ui.default_lexer_combobox.set_model(self._lexer_liststore) self._style_liststore = Gtk.ListStore(str) self._ui.style_combobox.set_model(self._style_liststore) self._plugin = plugin self._lexers = plugin.highlighter_config.get_lexer_list() self._styles = plugin.highlighter_config.get_styles_list() self._provider = None self._add_css_provider() self._initialize() def _initialize(self): default_lexer = self._plugin.highlighter_config.get_default_lexer_name() for i, lexer in enumerate(self._lexers): self._lexer_liststore.append([lexer[0]]) if lexer[1] == default_lexer: self._ui.default_lexer_combobox.set_active(i) for i, style in enumerate(self._styles): self._style_liststore.append([style]) if style == self._plugin.config['style']: self._ui.style_combobox.set_active(i) self._ui.line_break_combobox.set_active( self._plugin.config['line_break'].value) self._ui.code_marker_combobox.set_active( self._plugin.config['code_marker']) self._ui.font_button.set_font(self._plugin.config['font']) bg_override_enabled = self._plugin.config['bgcolor_override'] self._ui.bg_color_checkbutton.set_active(bg_override_enabled) self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled) color = Gdk.RGBA() if color.parse(self._plugin.config['bgcolor']): self._ui.bg_color_colorbutton.set_rgba(color) self._update_preview() def _lexer_changed(self, widget): self._plugin.highlighter_config.set_default_lexer( self._lexers[widget.get_active()][1]) self._update_preview() def _line_break_changed(self, widget): self._plugin.config['line_break'] = LineBreakOptions( widget.get_active()) self._update_preview() def _code_marker_changed(self, widget): self._plugin.config['code_marker'] = CodeMarkerOptions( widget.get_active()) def _bg_color_enabled(self, widget): override_color = widget.get_active() self._plugin.config['bgcolor_override'] = override_color self._ui.bg_color_colorbutton.set_sensitive(override_color) self._update_preview() def _bg_color_changed(self, widget): color = widget.get_rgba() self._plugin.config['bgcolor'] = color.to_string() self._update_preview() def _style_changed(self, widget): style = self._styles[widget.get_active()] if style is not None and style != '': self._plugin.config['style'] = style self._update_preview() def _font_changed(self, widget): font = widget.get_font() if font is not None and font != '': self._plugin.config['font'] = font self._update_preview() def _update_preview(self): self._format_preview_text() def _on_preview_text_inserted(self, _buf, _iterator, text, length, *_args): if (length == 1 and re.match(r'\s', text)) or length > 1: self._format_preview_text() def _add_css_provider(self): self._context = self._ui.preview_textview.get_style_context() self._provider = Gtk.CssProvider() self._context.add_provider( self._provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) self._context.add_class('syntax-preview') def _format_preview_text(self): buf = self._ui.preview_textview.get_buffer() start_iter = buf.get_start_iter() start_mark = buf.create_mark(None, start_iter, True) buf.remove_all_tags(start_iter, buf.get_end_iter()) formatter = GTKFormatter( style=self._plugin.config['style'], start_mark=start_mark) code = start_iter.get_text(buf.get_end_iter()) lexer = self._plugin.highlighter_config.get_default_lexer() if lexer != PLUGIN_INTERNAL_NONE_LEXER_ID: tokens = pygments.lex(code, lexer) pygments.format(tokens, formatter, buf) buf.delete_mark(start_mark) css = self._get_css() self._provider.load_from_data(bytes(css.encode())) def _get_css(self): # Build CSS from Pango.FontDescription description = FontDescription.from_string(self._plugin.config['font']) size = description.get_size() / SCALE style = self._get_string_from_pango_style(description.get_style()) weight = self._pango_to_css_weight(int(description.get_weight())) family = description.get_family() font = '%spt %s' % (size, family) if self._plugin.config['bgcolor_override']: color = self._plugin.config['bgcolor'] else: color = '@theme_base_color' css = ''' .syntax-preview { font: %s; font-weight: %s; font-style: %s; } .syntax-preview > text { background-color: %s; } ''' % (font, weight, style, color) return css @staticmethod def _pango_to_css_weight(number): # Pango allows for weight values between 100 and 1000 # CSS allows only full hundred numbers like 100, 200 .. number = int(number) if number < 100: return 100 if number > 900: return 900 return int(math.ceil(number / 100.0)) * 100 @staticmethod def _get_string_from_pango_style(style: Style) -> str: if style == Style.NORMAL: return 'normal' if style == Style.ITALIC: return 'italic' # Style.OBLIQUE: return 'oblique'