[syntax_highlight] Simplify plugin code
Fix deprecations warnings in config
This commit is contained in:
@@ -1,123 +0,0 @@
|
|||||||
# Syntax Highlighting Plugin for Gajim
|
|
||||||
|
|
||||||
[Gajim](https://gajim.org) Plugin that highlights source code blocks in the chat window.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
The recommended way of installing this plugin is to use Gajim's Plugin Installer.
|
|
||||||
|
|
||||||
For more information and instruction on how to install plugins manually, please
|
|
||||||
refer to the [Gajim Plugin Wiki site](https://dev.gajim.org/gajim/gajim-plugins/wikis/home#how-to-install-plugins).
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
This plugin uses markdown-style syntax to identify which parts of a message
|
|
||||||
should be formatted as code in the chat window.
|
|
||||||
|
|
||||||
```
|
|
||||||
Inline source code will be highlighted when placed in between `two single
|
|
||||||
back-ticks`.
|
|
||||||
```
|
|
||||||
|
|
||||||
The language used to highlight the syntax of inline code is selected as the
|
|
||||||
default language in the plugin settings.
|
|
||||||
|
|
||||||
|
|
||||||
Multi-line code blocks are started by three back-ticks followed by a newline.
|
|
||||||
Optionally, a language can be specified directly after the opening back-ticks and
|
|
||||||
before the line break:
|
|
||||||
````
|
|
||||||
```language
|
|
||||||
Note, that the last line of a code block may only contain the closing back-ticks,
|
|
||||||
i.e. there must be a newline here.
|
|
||||||
```
|
|
||||||
````
|
|
||||||
|
|
||||||
In case no language is specified with the opening tag or the specified language
|
|
||||||
could not be identified, the default language configured in the settings is
|
|
||||||
used.
|
|
||||||
|
|
||||||
You can test it by copying and sending the following text to one of your
|
|
||||||
contacts:
|
|
||||||
````
|
|
||||||
```python
|
|
||||||
def test():
|
|
||||||
print("Hello, world!")
|
|
||||||
```
|
|
||||||
````
|
|
||||||
(**Note:** your contact will not receive highlighted text unless she is also
|
|
||||||
using the plugin.)
|
|
||||||
|
|
||||||
|
|
||||||
## Relation to XEP-0393 - 'Message Styling'
|
|
||||||
|
|
||||||
https://xmpp.org/extensions/xep-0393.html#pre-block
|
|
||||||
|
|
||||||
In [XEP-0393](https://xmpp.org/extensions/xep-0393.html),
|
|
||||||
the back-tick based syntax is defined as markup for preformatted
|
|
||||||
text blocks, respectively inline preformatted text.
|
|
||||||
Formatting of such text blocks with mono-spaced fonts is recommended by the XEP.
|
|
||||||
|
|
||||||
By using the same syntax as defined in XEP-0393 XMPP clients with only XEP-0393
|
|
||||||
support but without syntax highlighting can at least present their users blocks
|
|
||||||
of preformatted text.
|
|
||||||
|
|
||||||
Since text in between the back-tick markers is not further formatted by this
|
|
||||||
plugin, it can be considered "preformatted".
|
|
||||||
Hence, this plugin is compatible to the formatting options defined by XEP-0393,
|
|
||||||
[section 5.1.2, "Preformatted Text"](https://xmpp.org/extensions/xep-0393.html#pre-block)
|
|
||||||
and [section 5.2.5, "Preformatted Span"](https://xmpp.org/extensions/xep-0393.html#mono).
|
|
||||||
|
|
||||||
Nevertheless, syntax highlighting for source code is not part of XEP but
|
|
||||||
rather a non-standard extension introduced with this plugin.
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The configuration can be found via 'Gajim' > 'Plugins', then select the
|
|
||||||
'Source Code Syntax Highlight' Plugin and click the gears symbol.
|
|
||||||
The configuration options let you specify many details how code is formatted,
|
|
||||||
including default language, style, font settings, background color and formatting
|
|
||||||
of the code markers.
|
|
||||||
|
|
||||||
In the configuration window, the current settings are displayed in an
|
|
||||||
interactive preview panel. This allows you to directly check how code would
|
|
||||||
look like in the message
|
|
||||||
window.
|
|
||||||
|
|
||||||
## Report Bugs and Feature Requests
|
|
||||||
|
|
||||||
For bug reports, please report them to the [Gajim Plugin Issue tracker](https://dev.gajim.org/gajim/gajim-plugins/issues/new?issue[FlorianMuenchbach]=&issue[description]=Gajim%20Version%3A%20%0APlugin%20Version%3A%0AOperating%20System%3A&issue[title]=[syntax_highlight]).
|
|
||||||
|
|
||||||
Please make sure that the issue you create contains `[syntax_highlight]` in the
|
|
||||||
title and information such as Gajim version, Plugin version, Operating system,
|
|
||||||
etc.
|
|
||||||
|
|
||||||
## Debug
|
|
||||||
|
|
||||||
The plugin adds its own logger. It can be used to set a specific debug level
|
|
||||||
for this plugin and/or filter log messages.
|
|
||||||
|
|
||||||
Run
|
|
||||||
```
|
|
||||||
gajim --loglevel gajim.p.syntax_highlight=DEBUG
|
|
||||||
```
|
|
||||||
in a terminal to display the debug messages.
|
|
||||||
|
|
||||||
|
|
||||||
## Known Issues / ToDo
|
|
||||||
|
|
||||||
* ~~Gajim crashes when correcting a message containing highlighted code.~~
|
|
||||||
(fixed in version 1.1.0)
|
|
||||||
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
Since I had no experience in writing Plugins for Gajim, I used the
|
|
||||||
[Latex Plugin](https://dev.gajim.org/gajim/gajim-plugins/wikis/LatexPlugin)
|
|
||||||
written by Yves Fischer and Yann Leboulanger as an example and copied a big
|
|
||||||
portion of initial code. Therefore, credits go to the authors of the Latex
|
|
||||||
Plugin for providing an example.
|
|
||||||
|
|
||||||
The syntax highlighting itself is done by [pygments](http://pygments.org/).
|
|
||||||
@@ -8,12 +8,22 @@ from syntax_highlight.gtkformatter import GTKFormatter
|
|||||||
from syntax_highlight.types import MatchType
|
from syntax_highlight.types import MatchType
|
||||||
from syntax_highlight.types import LineBreakOptions
|
from syntax_highlight.types import LineBreakOptions
|
||||||
from syntax_highlight.types import CodeMarkerOptions
|
from syntax_highlight.types import CodeMarkerOptions
|
||||||
|
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
|
||||||
|
|
||||||
log = logging.getLogger('gajim.p.syntax_highlight')
|
log = logging.getLogger('gajim.p.syntax_highlight')
|
||||||
|
|
||||||
|
|
||||||
class ChatSyntaxHighlighter:
|
class ChatSyntaxHighlighter:
|
||||||
def hide_code_markup(self, buf, start, end):
|
def __init__(self, plugin_config, highlighter_config, textview):
|
||||||
|
self.textview = textview
|
||||||
|
self._plugin_config = plugin_config
|
||||||
|
self._highlighter_config = highlighter_config
|
||||||
|
|
||||||
|
def update_config(self, plugin_config):
|
||||||
|
self._plugin_config = plugin_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hide_code_markup(buf, start, end):
|
||||||
tag = buf.get_tag_table().lookup('hide_code_markup')
|
tag = buf.get_tag_table().lookup('hide_code_markup')
|
||||||
if tag is None:
|
if tag is None:
|
||||||
tag = Gtk.TextTag.new('hide_code_markup')
|
tag = Gtk.TextTag.new('hide_code_markup')
|
||||||
@@ -22,17 +32,16 @@ class ChatSyntaxHighlighter:
|
|||||||
|
|
||||||
buf.apply_tag_by_name('hide_code_markup', start, end)
|
buf.apply_tag_by_name('hide_code_markup', start, end)
|
||||||
|
|
||||||
def check_line_break(self, is_multiline):
|
def _check_line_break(self, is_multiline):
|
||||||
line_break = self.config.get_line_break_action()
|
line_break = self._plugin_config['line_break'].value
|
||||||
|
|
||||||
return (line_break == LineBreakOptions.ALWAYS) \
|
return (line_break == LineBreakOptions.ALWAYS) \
|
||||||
or (is_multiline and line_break == LineBreakOptions.MULTILINE)
|
or (is_multiline and line_break == LineBreakOptions.MULTILINE)
|
||||||
|
|
||||||
def format_code(self, buf, s_tag, s_code, e_tag, e_code, language):
|
def _format_code(self, buf, s_tag, s_code, e_tag, e_code, language):
|
||||||
style = self.config.get_style_name()
|
style = self._plugin_config['style']
|
||||||
if self.config.get_code_marker_setting() == CodeMarkerOptions.HIDE:
|
if self._plugin_config['code_marker'] == CodeMarkerOptions.HIDE:
|
||||||
self.hide_code_markup(buf, s_tag, s_code)
|
self._hide_code_markup(buf, s_tag, s_code)
|
||||||
self.hide_code_markup(buf, e_code, e_tag)
|
self._hide_code_markup(buf, e_code, e_tag)
|
||||||
else:
|
else:
|
||||||
comment_tag = GTKFormatter.create_tag_for_token(
|
comment_tag = GTKFormatter.create_tag_for_token(
|
||||||
pygments.token.Comment,
|
pygments.token.Comment,
|
||||||
@@ -49,24 +58,25 @@ class ChatSyntaxHighlighter:
|
|||||||
lexer = None
|
lexer = None
|
||||||
|
|
||||||
if language is None:
|
if language is None:
|
||||||
lexer = self.config.get_default_lexer()
|
lexer = self._highlighter_config.get_default_lexer()
|
||||||
log.info('No Language specified. '
|
log.info('No Language specified. '
|
||||||
'Falling back to default lexer: %s.',
|
'Falling back to default lexer: %s.',
|
||||||
self.config.get_default_lexer_name())
|
self._highlighter_config.get_default_lexer_name())
|
||||||
else:
|
else:
|
||||||
log.debug('Using lexer for %s.', str(language))
|
log.debug('Using lexer for %s.', str(language))
|
||||||
lexer = self.config.get_lexer_with_fallback(language)
|
lexer = self._highlighter_config.get_lexer_with_fallback(language)
|
||||||
|
|
||||||
if lexer is None:
|
if lexer is None:
|
||||||
iterator = buf.get_iter_at_mark(start_mark)
|
iterator = buf.get_iter_at_mark(start_mark)
|
||||||
buf.insert(iterator, '\n')
|
buf.insert(iterator, '\n')
|
||||||
elif not self.config.is_internal_none_lexer(lexer):
|
elif lexer != PLUGIN_INTERNAL_NONE_LEXER_ID:
|
||||||
tokens = pygments.lex(code, lexer)
|
tokens = pygments.lex(code, lexer)
|
||||||
|
|
||||||
formatter = GTKFormatter(style=style, start_mark=start_mark)
|
formatter = GTKFormatter(style=style, start_mark=start_mark)
|
||||||
pygments.format(tokens, formatter, buf)
|
pygments.format(tokens, formatter, buf)
|
||||||
|
|
||||||
def find_multiline_matches(self, text):
|
@staticmethod
|
||||||
|
def _find_multiline_matches(text):
|
||||||
start = None
|
start = None
|
||||||
matches = []
|
matches = []
|
||||||
# Less strict, allow prefixed whitespaces:
|
# Less strict, allow prefixed whitespaces:
|
||||||
@@ -84,7 +94,8 @@ class ChatSyntaxHighlighter:
|
|||||||
continue
|
continue
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
def find_inline_matches(self, text):
|
@staticmethod
|
||||||
|
def _find_inline_matches(text):
|
||||||
"""
|
"""
|
||||||
Inline code is highlighted if the start marker is precedded by a start
|
Inline code is highlighted if the start marker is precedded by a start
|
||||||
of line, a whitespace character or either of the other span markers
|
of line, a whitespace character or either of the other span markers
|
||||||
@@ -95,18 +106,19 @@ class ChatSyntaxHighlighter:
|
|||||||
re.finditer(r'(?:^|\s|\*|~|_)(`((?!`).+?)`)(?:\s|\*|~|_|$)',
|
re.finditer(r'(?:^|\s|\*|~|_)(`((?!`).+?)`)(?:\s|\*|~|_|$)',
|
||||||
text)]
|
text)]
|
||||||
|
|
||||||
def merge_match_groups(self, real_text, inline_matches, multiline_matches):
|
@staticmethod
|
||||||
|
def _merge_match_groups(real_text, inline_matches, multiline_matches):
|
||||||
it_inline = iter(inline_matches)
|
it_inline = iter(inline_matches)
|
||||||
it_multi = iter(multiline_matches)
|
it_multi = iter(multiline_matches)
|
||||||
length = len(real_text)
|
length = len(real_text)
|
||||||
|
|
||||||
# Just to get cleaner code below...
|
# Just to get cleaner code below...
|
||||||
def get_next(iterator):
|
def _get_next(iterator):
|
||||||
return next(iterator, (length, length, ''))
|
return next(iterator, (length, length, ''))
|
||||||
|
|
||||||
# In order to simplify the process, we use the 'length' here.
|
# In order to simplify the process, we use the 'length' here.
|
||||||
cur_inline = get_next(it_inline)
|
cur_inline = _get_next(it_inline)
|
||||||
cur_multi = get_next(it_multi)
|
cur_multi = _get_next(it_multi)
|
||||||
|
|
||||||
pos = 0
|
pos = 0
|
||||||
|
|
||||||
@@ -142,18 +154,18 @@ class ChatSyntaxHighlighter:
|
|||||||
# Also, forward the other one, if regions overlap or we took over...
|
# Also, forward the other one, if regions overlap or we took over...
|
||||||
if selected[2] == MatchType.INLINE:
|
if selected[2] == MatchType.INLINE:
|
||||||
if cur_multi[0] < cur_inline[1]:
|
if cur_multi[0] < cur_inline[1]:
|
||||||
cur_multi = get_next(it_multi)
|
cur_multi = _get_next(it_multi)
|
||||||
cur_inline = get_next(it_inline)
|
cur_inline = _get_next(it_inline)
|
||||||
elif selected[2] == MatchType.MULTILINE:
|
elif selected[2] == MatchType.MULTILINE:
|
||||||
if cur_inline[0] < cur_multi[1]:
|
if cur_inline[0] < cur_multi[1]:
|
||||||
cur_inline = get_next(it_inline)
|
cur_inline = _get_next(it_inline)
|
||||||
cur_multi = get_next(it_multi)
|
cur_multi = _get_next(it_multi)
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def process_text(self, real_text, other_tags, _graphics, iter_,
|
def process_text(self, real_text, other_tags, _graphics, iter_,
|
||||||
_additional):
|
_additional):
|
||||||
def fix_newline(char, marker_len_no_newline, force=False):
|
def _fix_newline(char, marker_len_no_newline, force=False):
|
||||||
fixed = (marker_len_no_newline, '')
|
fixed = (marker_len_no_newline, '')
|
||||||
if char == '\n':
|
if char == '\n':
|
||||||
fixed = (marker_len_no_newline + 1, '')
|
fixed = (marker_len_no_newline + 1, '')
|
||||||
@@ -164,8 +176,8 @@ class ChatSyntaxHighlighter:
|
|||||||
buf = self.textview.tv.get_buffer()
|
buf = self.textview.tv.get_buffer()
|
||||||
|
|
||||||
# First, try to find inline or multiline code snippets
|
# First, try to find inline or multiline code snippets
|
||||||
inline_matches = self.find_inline_matches(real_text)
|
inline_matches = self._find_inline_matches(real_text)
|
||||||
multiline_matches = self.find_multiline_matches(real_text)
|
multiline_matches = self._find_multiline_matches(real_text)
|
||||||
|
|
||||||
if not inline_matches and not multiline_matches:
|
if not inline_matches and not multiline_matches:
|
||||||
log.debug('Stopping early, since there is no code block in it...')
|
log.debug('Stopping early, since there is no code block in it...')
|
||||||
@@ -177,10 +189,10 @@ class ChatSyntaxHighlighter:
|
|||||||
start_mark = buf.create_mark('SHP_start', iterator, True)
|
start_mark = buf.create_mark('SHP_start', iterator, True)
|
||||||
end_mark = buf.create_mark('SHP_end', iterator, False)
|
end_mark = buf.create_mark('SHP_end', iterator, False)
|
||||||
|
|
||||||
insert_newline_for_multiline = self.check_line_break(True)
|
insert_newline_for_multiline = self._check_line_break(True)
|
||||||
insert_newline_for_inline = self.check_line_break(False)
|
insert_newline_for_inline = self._check_line_break(False)
|
||||||
|
|
||||||
split_text = self.merge_match_groups(
|
split_text = self._merge_match_groups(
|
||||||
real_text, inline_matches, multiline_matches)
|
real_text, inline_matches, multiline_matches)
|
||||||
|
|
||||||
buf.begin_user_action()
|
buf.begin_user_action()
|
||||||
@@ -204,20 +216,20 @@ class ChatSyntaxHighlighter:
|
|||||||
language_len = 0 if language is None else len(language)
|
language_len = 0 if language is None else len(language)
|
||||||
|
|
||||||
# We account the language word width for the front marker
|
# We account the language word width for the front marker
|
||||||
front = fix_newline(
|
front = _fix_newline(
|
||||||
text_to_insert[0],
|
text_to_insert[0],
|
||||||
3 + language_len,
|
3 + language_len,
|
||||||
insert_newline_for_multiline)
|
insert_newline_for_multiline)
|
||||||
back = fix_newline(
|
back = _fix_newline(
|
||||||
text_to_insert[-1],
|
text_to_insert[-1],
|
||||||
3,
|
3,
|
||||||
insert_newline_for_multiline and not end_of_message)
|
insert_newline_for_multiline and not end_of_message)
|
||||||
else:
|
else:
|
||||||
front = fix_newline(
|
front = _fix_newline(
|
||||||
text_to_insert[0],
|
text_to_insert[0],
|
||||||
1,
|
1,
|
||||||
insert_newline_for_inline)
|
insert_newline_for_inline)
|
||||||
back = fix_newline(
|
back = _fix_newline(
|
||||||
text_to_insert[-1],
|
text_to_insert[-1],
|
||||||
1,
|
1,
|
||||||
insert_newline_for_inline and not end_of_message)
|
insert_newline_for_inline and not end_of_message)
|
||||||
@@ -226,8 +238,9 @@ class ChatSyntaxHighlighter:
|
|||||||
text_to_insert = ''.join([front[1], text_to_insert, back[1]])
|
text_to_insert = ''.join([front[1], text_to_insert, back[1]])
|
||||||
|
|
||||||
# Insertion invalidates iterator, let's use our start mark...
|
# Insertion invalidates iterator, let's use our start mark...
|
||||||
self.insert_and_format_code(buf, text_to_insert, language,
|
self._insert_and_format_code(
|
||||||
marker_widths, start_mark, end_mark, other_tags)
|
buf, text_to_insert, language, marker_widths, start_mark,
|
||||||
|
end_mark, other_tags)
|
||||||
|
|
||||||
iterator = buf.get_iter_at_mark(end_mark)
|
iterator = buf.get_iter_at_mark(end_mark)
|
||||||
# The current end of the buffer's contents is the start for the
|
# The current end of the buffer's contents is the start for the
|
||||||
@@ -244,14 +257,13 @@ class ChatSyntaxHighlighter:
|
|||||||
# print_special_text method is resetting the plugin_modified variable...
|
# print_special_text method is resetting the plugin_modified variable...
|
||||||
self.textview.plugin_modified = True
|
self.textview.plugin_modified = True
|
||||||
|
|
||||||
def insert_and_format_code(self, buf, insert_text, language, marker,
|
def _insert_and_format_code(self, buf, insert_text, language, marker,
|
||||||
start_mark, end_mark, other_tags=None):
|
start_mark, end_mark, other_tags=None):
|
||||||
|
|
||||||
start_iter = buf.get_iter_at_mark(start_mark)
|
start_iter = buf.get_iter_at_mark(start_mark)
|
||||||
|
|
||||||
if other_tags:
|
if other_tags:
|
||||||
buf.insert_with_tags_by_name(start_iter, insert_text,
|
buf.insert_with_tags_by_name(start_iter, insert_text, *other_tags)
|
||||||
*other_tags)
|
|
||||||
else:
|
else:
|
||||||
buf.insert(start_iter, insert_text)
|
buf.insert(start_iter, insert_text)
|
||||||
|
|
||||||
@@ -264,20 +276,16 @@ class ChatSyntaxHighlighter:
|
|||||||
|
|
||||||
log.debug('full text between tags: %s.', tag_start.get_text(tag_end))
|
log.debug('full text between tags: %s.', tag_start.get_text(tag_end))
|
||||||
|
|
||||||
self.format_code(buf, tag_start, s_code, tag_end, e_code, language)
|
self._format_code(buf, tag_start, s_code, tag_end, e_code, language)
|
||||||
|
|
||||||
self.textview.plugin_modified = True
|
self.textview.plugin_modified = True
|
||||||
|
|
||||||
# Set general code block format
|
# Set general code block format
|
||||||
tag = Gtk.TextTag.new()
|
tag = Gtk.TextTag.new()
|
||||||
if self.config.is_bgcolor_override_enabled():
|
bg_color = self._plugin_config['bgcolor']
|
||||||
tag.set_property('background', self.config.get_bgcolor())
|
if self._plugin_config['bgcolor_override']:
|
||||||
tag.set_property('paragraph-background', self.config.get_bgcolor())
|
tag.set_property('background', bg_color)
|
||||||
tag.set_property('font', self.config.get_font())
|
tag.set_property('paragraph-background', bg_color)
|
||||||
|
tag.set_property('font', self._plugin_config['font'])
|
||||||
buf.get_tag_table().add(tag)
|
buf.get_tag_table().add(tag)
|
||||||
buf.apply_tag(tag, tag_start, tag_end)
|
buf.apply_tag(tag, tag_start, tag_end)
|
||||||
|
|
||||||
def __init__(self, config, textview):
|
|
||||||
self.last_end_mark = None
|
|
||||||
self.config = config
|
|
||||||
self.textview = textview
|
|
||||||
|
|||||||
206
syntax_highlight/config_dialog.py
Normal file
206
syntax_highlight/config_dialog.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
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'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generated with glade 3.22.1 -->
|
<!-- Generated with glade 3.36.0 -->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.20"/>
|
<requires lib="gtk+" version="3.22"/>
|
||||||
<object class="GtkTextBuffer"/>
|
<object class="GtkTextBuffer"/>
|
||||||
<object class="GtkListStore" id="code_marker_selection">
|
<object class="GtkListStore" id="code_marker_selection">
|
||||||
<columns>
|
<columns>
|
||||||
@@ -309,7 +309,4 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkTextBuffer" id="textbuffer1">
|
|
||||||
<property name="text">Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.</property>
|
|
||||||
</object>
|
|
||||||
</interface>
|
</interface>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class GTKFormatter(Formatter):
|
|||||||
|
|
||||||
def format(self, tokensource, outfile):
|
def format(self, tokensource, outfile):
|
||||||
if not isinstance(outfile, Gtk.TextBuffer) or outfile is None:
|
if not isinstance(outfile, Gtk.TextBuffer) or outfile is None:
|
||||||
log.warn("Did not get a buffer to format...")
|
log.warning('Did not get a buffer to format...')
|
||||||
return
|
return
|
||||||
buf = outfile
|
buf = outfile
|
||||||
|
|
||||||
|
|||||||
97
syntax_highlight/highlighter_config.py
Normal file
97
syntax_highlight/highlighter_config.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
from pygments.lexers import get_all_lexers
|
||||||
|
from pygments.styles import get_all_styles
|
||||||
|
from pygments.util import ClassNotFound
|
||||||
|
|
||||||
|
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 HighlighterConfig:
|
||||||
|
def __init__(self, plugin_config):
|
||||||
|
self._plugin_config = plugin_config
|
||||||
|
|
||||||
|
self._lexer_list = self._create_lexer_list()
|
||||||
|
self._style_list = []
|
||||||
|
for style in get_all_styles():
|
||||||
|
self._style_list.append(style)
|
||||||
|
self._style_list.sort()
|
||||||
|
|
||||||
|
self._default_lexer = None
|
||||||
|
self.set_default_lexer(self._plugin_config['default_lexer'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_lexer_list():
|
||||||
|
# The list we create here contains the plain text name and the lexer's
|
||||||
|
# id string
|
||||||
|
lexers = []
|
||||||
|
|
||||||
|
# Iteration over get_all_lexers() seems to be broken somehow
|
||||||
|
# Workaround
|
||||||
|
all_lexers = get_all_lexers()
|
||||||
|
for lexer in all_lexers:
|
||||||
|
# We don't want to add lexers that we cant identify by name later
|
||||||
|
if lexer[1] is not None and lexer[1]:
|
||||||
|
lexers.append((lexer[0], lexer[1][0]))
|
||||||
|
lexers.sort()
|
||||||
|
|
||||||
|
# Insert our internal 'none' type at top of the list
|
||||||
|
lexers.insert(0, PLUGIN_INTERNAL_NONE_LEXER)
|
||||||
|
return lexers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_lexer_by_name(name):
|
||||||
|
lexer = None
|
||||||
|
try:
|
||||||
|
lexer = get_lexer_by_name(name)
|
||||||
|
except ClassNotFound:
|
||||||
|
pass
|
||||||
|
return lexer
|
||||||
|
|
||||||
|
def get_lexer_with_fallback(self, language):
|
||||||
|
lexer = self.get_lexer_by_name(language)
|
||||||
|
if lexer is None:
|
||||||
|
log.info('Falling back to default lexer for %s.',
|
||||||
|
self.get_default_lexer_name())
|
||||||
|
lexer = self._default_lexer[1]
|
||||||
|
return lexer
|
||||||
|
|
||||||
|
def set_default_lexer(self, name):
|
||||||
|
if name != PLUGIN_INTERNAL_NONE_LEXER_ID:
|
||||||
|
lexer = get_lexer_by_name(name)
|
||||||
|
|
||||||
|
if lexer is None and self._default_lexer is None:
|
||||||
|
log.error('Failed to get default lexer by name.'
|
||||||
|
'Falling back to simply using the first lexer '
|
||||||
|
'in the list.')
|
||||||
|
lexer = self._lexer_list[0]
|
||||||
|
name = lexer[0]
|
||||||
|
self._default_lexer = (name, lexer)
|
||||||
|
if lexer is None and self._default_lexer is not None:
|
||||||
|
log.info('Failed to get default lexer by name, keeping '
|
||||||
|
'previous setting (lexer = %s).',
|
||||||
|
self._default_lexer[0])
|
||||||
|
name = self._default_lexer[0]
|
||||||
|
else:
|
||||||
|
self._default_lexer = (name, lexer)
|
||||||
|
else:
|
||||||
|
self._default_lexer = PLUGIN_INTERNAL_NONE_LEXER
|
||||||
|
|
||||||
|
self._plugin_config['default_lexer'] = name
|
||||||
|
|
||||||
|
def get_default_lexer(self):
|
||||||
|
return self._default_lexer[1]
|
||||||
|
|
||||||
|
def get_default_lexer_name(self):
|
||||||
|
return self._default_lexer[0]
|
||||||
|
|
||||||
|
def get_lexer_list(self):
|
||||||
|
return self._lexer_list
|
||||||
|
|
||||||
|
def get_styles_list(self):
|
||||||
|
return self._style_list
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from gi.repository import Gdk
|
|
||||||
|
|
||||||
from pygments.lexers import get_lexer_by_name
|
|
||||||
from pygments.lexers import get_all_lexers
|
|
||||||
from pygments.styles import get_all_styles
|
|
||||||
from pygments.util import ClassNotFound
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
class SyntaxHighlighterConfig:
|
|
||||||
PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)',
|
|
||||||
PLUGIN_INTERNAL_NONE_LEXER_ID)
|
|
||||||
|
|
||||||
def _create_lexer_list(self):
|
|
||||||
# The list we create here contains the plain text name and the lexer's
|
|
||||||
# id string
|
|
||||||
lexers = []
|
|
||||||
|
|
||||||
# Iteration over get_all_lexers() seems to be broken somehow
|
|
||||||
# Workaround
|
|
||||||
all_lexers = get_all_lexers()
|
|
||||||
for lexer in all_lexers:
|
|
||||||
# We don't want to add lexers that we cant identify by name later
|
|
||||||
if lexer[1] is not None and lexer[1]:
|
|
||||||
lexers.append((lexer[0], lexer[1][0]))
|
|
||||||
lexers.sort()
|
|
||||||
|
|
||||||
# Insert our internal 'none' type at top of the list
|
|
||||||
lexers.insert(0, self.PLUGIN_INTERNAL_NONE_LEXER)
|
|
||||||
return lexers
|
|
||||||
|
|
||||||
def is_internal_none_lexer(self, lexer):
|
|
||||||
return lexer == PLUGIN_INTERNAL_NONE_LEXER_ID
|
|
||||||
|
|
||||||
def get_internal_none_lexer(self):
|
|
||||||
return self.PLUGIN_INTERNAL_NONE_LEXER
|
|
||||||
|
|
||||||
def get_lexer_by_name(self, name):
|
|
||||||
lexer = None
|
|
||||||
try:
|
|
||||||
lexer = get_lexer_by_name(name)
|
|
||||||
except ClassNotFound:
|
|
||||||
pass
|
|
||||||
return lexer
|
|
||||||
|
|
||||||
def get_lexer_with_fallback(self, language):
|
|
||||||
lexer = self.get_lexer_by_name(language)
|
|
||||||
if lexer is None:
|
|
||||||
log.info('Falling back to default lexer for %s.',
|
|
||||||
self.get_default_lexer_name())
|
|
||||||
lexer = self.default_lexer[1]
|
|
||||||
return lexer
|
|
||||||
|
|
||||||
def set_font(self, font):
|
|
||||||
if font is not None and font != '':
|
|
||||||
self.config['font'] = font
|
|
||||||
|
|
||||||
def set_style(self, style):
|
|
||||||
if style is not None and style != '':
|
|
||||||
self.config['style'] = style
|
|
||||||
|
|
||||||
def set_line_break_action(self, option):
|
|
||||||
if isinstance(option, int):
|
|
||||||
option = LineBreakOptions(option)
|
|
||||||
self.config['line_break'] = option
|
|
||||||
|
|
||||||
def set_default_lexer(self, name):
|
|
||||||
if not self.is_internal_none_lexer(name):
|
|
||||||
lexer = get_lexer_by_name(name)
|
|
||||||
|
|
||||||
if lexer is None and self.default_lexer is None:
|
|
||||||
log.error('Failed to get default lexer by name.'
|
|
||||||
'Falling back to simply using the first lexer '
|
|
||||||
'in the list.')
|
|
||||||
lexer = self.lexer_list[0]
|
|
||||||
name = lexer[0]
|
|
||||||
self.default_lexer = (name, lexer)
|
|
||||||
if lexer is None and self.default_lexer is not None:
|
|
||||||
log.info('Failed to get default lexer by name, keeping '
|
|
||||||
'previous setting (lexer = %s).',
|
|
||||||
self.default_lexer[0])
|
|
||||||
name = self.default_lexer[0]
|
|
||||||
else:
|
|
||||||
self.default_lexer = (name, lexer)
|
|
||||||
else:
|
|
||||||
self.default_lexer = self.PLUGIN_INTERNAL_NONE_LEXER
|
|
||||||
|
|
||||||
self.config['default_lexer'] = name
|
|
||||||
|
|
||||||
def set_bgcolor_override_enabled(self, state):
|
|
||||||
self.config['bgcolor_override'] = state
|
|
||||||
|
|
||||||
def set_bgcolor(self, color):
|
|
||||||
if isinstance(color, Gdk.RGBA):
|
|
||||||
color = color.to_string()
|
|
||||||
self.config['bgcolor'] = color
|
|
||||||
|
|
||||||
def set_code_marker_setting(self, option):
|
|
||||||
if isinstance(option, int):
|
|
||||||
option = CodeMarkerOptions(option)
|
|
||||||
self.config['code_marker'] = option
|
|
||||||
|
|
||||||
def set_pygments_path(self, path):
|
|
||||||
self.config['pygments_path'] = path
|
|
||||||
|
|
||||||
def get_default_lexer(self):
|
|
||||||
return self.default_lexer[1]
|
|
||||||
|
|
||||||
def get_default_lexer_name(self):
|
|
||||||
return self.default_lexer[0]
|
|
||||||
|
|
||||||
def get_lexer_list(self):
|
|
||||||
return self.lexer_list
|
|
||||||
|
|
||||||
def get_line_break_action(self):
|
|
||||||
# Return int only
|
|
||||||
if isinstance(self.config['line_break'], int):
|
|
||||||
# In case of legacy settings, convert.
|
|
||||||
action = self.config['line_break']
|
|
||||||
self.set_line_break_action(action)
|
|
||||||
else:
|
|
||||||
action = self.config['line_break'].value
|
|
||||||
|
|
||||||
return action
|
|
||||||
|
|
||||||
def get_pygments_path(self):
|
|
||||||
return self.config['pygments_path']
|
|
||||||
|
|
||||||
def get_font(self):
|
|
||||||
return self.config['font']
|
|
||||||
|
|
||||||
def get_style_name(self):
|
|
||||||
return self.config['style']
|
|
||||||
|
|
||||||
def is_bgcolor_override_enabled(self):
|
|
||||||
return self.config['bgcolor_override']
|
|
||||||
|
|
||||||
def get_bgcolor(self):
|
|
||||||
return self.config['bgcolor']
|
|
||||||
|
|
||||||
def get_code_marker_setting(self):
|
|
||||||
return self.config['code_marker']
|
|
||||||
|
|
||||||
def get_styles_list(self):
|
|
||||||
return self.style_list
|
|
||||||
|
|
||||||
def init_pygments(self):
|
|
||||||
"""
|
|
||||||
Initialize all config variables that depend directly on pygments being
|
|
||||||
available.
|
|
||||||
"""
|
|
||||||
self.lexer_list = self._create_lexer_list()
|
|
||||||
self.style_list = [s for s in get_all_styles()]
|
|
||||||
self.style_list.sort()
|
|
||||||
self.set_default_lexer(self.config['default_lexer'])
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.lexer_list = []
|
|
||||||
self.style_list = []
|
|
||||||
self.config = config
|
|
||||||
self.default_lexer = None
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import re
|
|
||||||
import pygments
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from gi.repository import Gdk
|
|
||||||
from gi.repository.Pango import FontDescription
|
|
||||||
|
|
||||||
from gajim.plugins.gui import GajimPluginConfigDialog
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class SyntaxHighlighterPluginConfiguration(GajimPluginConfigDialog):
|
|
||||||
def init(self):
|
|
||||||
path = self.plugin.local_file_path('config_dialog.ui')
|
|
||||||
self._ui = get_builder(path)
|
|
||||||
box = self.get_content_area()
|
|
||||||
box.pack_start(self._ui.main_box, True, True, 0)
|
|
||||||
|
|
||||||
self._ui.set_translation_domain('gajim_plugins')
|
|
||||||
|
|
||||||
self.liststore = Gtk.ListStore(str)
|
|
||||||
self._ui.default_lexer_combobox.set_model(self.liststore)
|
|
||||||
|
|
||||||
self.style_liststore = Gtk.ListStore(str)
|
|
||||||
self._ui.style_combobox.set_model(self.style_liststore)
|
|
||||||
|
|
||||||
self._ui.preview_textview.get_buffer().connect(
|
|
||||||
'insert-text', self._on_preview_text_inserted)
|
|
||||||
|
|
||||||
self._ui.connect_signals(self)
|
|
||||||
|
|
||||||
self.default_lexer_id = 0
|
|
||||||
self.style_id = 0
|
|
||||||
|
|
||||||
def set_config(self, config):
|
|
||||||
self.config = config
|
|
||||||
self.lexers = self.config.get_lexer_list()
|
|
||||||
self.styles = self.config.get_styles_list()
|
|
||||||
default_lexer = self.config.get_default_lexer_name()
|
|
||||||
default_style = self.config.get_style_name()
|
|
||||||
|
|
||||||
for i, lexer in enumerate(self.lexers):
|
|
||||||
self.liststore.append([lexer[0]])
|
|
||||||
if lexer[1] == default_lexer:
|
|
||||||
self.default_lexer_id = i
|
|
||||||
|
|
||||||
for i, style in enumerate(self.styles):
|
|
||||||
self.style_liststore.append([style])
|
|
||||||
if style == default_style:
|
|
||||||
self.style_id = i
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
def _lexer_changed(self, _widget):
|
|
||||||
new = self._ui.default_lexer_combobox.get_active()
|
|
||||||
if new != self.default_lexer_id:
|
|
||||||
self.default_lexer_id = new
|
|
||||||
self.config.set_default_lexer(self.lexers[self.default_lexer_id][1])
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
def _line_break_changed(self, _widget):
|
|
||||||
new = LineBreakOptions(self._ui.line_break_combobox.get_active())
|
|
||||||
if new != self.config.get_line_break_action():
|
|
||||||
self.config.set_line_break_action(new)
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
def _code_marker_changed(self, _widget):
|
|
||||||
new = CodeMarkerOptions(self._ui.code_marker_combobox.get_active())
|
|
||||||
if new != self.config.get_code_marker_setting():
|
|
||||||
self.config.set_code_marker_setting(new)
|
|
||||||
|
|
||||||
def _bg_color_enabled(self, _widget):
|
|
||||||
new = self._ui.bg_color_checkbutton.get_active()
|
|
||||||
if new != self.config.is_bgcolor_override_enabled():
|
|
||||||
bg_override_enabled = new
|
|
||||||
self.config.set_bgcolor_override_enabled(bg_override_enabled)
|
|
||||||
self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled)
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
def _bg_color_changed(self, _widget):
|
|
||||||
new = self._ui.bg_color_colorbutton.get_rgba()
|
|
||||||
if new != self.config.get_bgcolor():
|
|
||||||
self.config.set_bgcolor(new)
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
def _style_changed(self, _widget):
|
|
||||||
new = self._ui.style_combobox.get_active()
|
|
||||||
if new != self.style_id:
|
|
||||||
self.style_id = new
|
|
||||||
self.config.set_style(self.styles[self.style_id])
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
def _font_changed(self, _widget):
|
|
||||||
new = self._ui.font_button.get_font()
|
|
||||||
if new != self.config.get_font():
|
|
||||||
self.config.set_font(new)
|
|
||||||
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 _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.config.get_style_name(), start_mark=start_mark)
|
|
||||||
|
|
||||||
code = start_iter.get_text(buf.get_end_iter())
|
|
||||||
lexer = self.config.get_default_lexer()
|
|
||||||
if not self.config.is_internal_none_lexer(lexer):
|
|
||||||
tokens = pygments.lex(code, lexer)
|
|
||||||
pygments.format(tokens, formatter, buf)
|
|
||||||
|
|
||||||
buf.delete_mark(start_mark)
|
|
||||||
|
|
||||||
self._ui.preview_textview.override_font(
|
|
||||||
FontDescription.from_string(self.config.get_font()))
|
|
||||||
|
|
||||||
color = Gdk.RGBA()
|
|
||||||
if color.parse(self.config.get_bgcolor()):
|
|
||||||
self._ui.preview_textview.override_background_color(
|
|
||||||
Gtk.StateFlags.NORMAL, color)
|
|
||||||
|
|
||||||
def on_run(self):
|
|
||||||
self._ui.default_lexer_combobox.set_active(self.default_lexer_id)
|
|
||||||
self._ui.line_break_combobox.set_active(
|
|
||||||
self.config.get_line_break_action())
|
|
||||||
self._ui.code_marker_combobox.set_active(
|
|
||||||
self.config.get_code_marker_setting())
|
|
||||||
self._ui.style_combobox.set_active(self.style_id)
|
|
||||||
|
|
||||||
self._ui.font_button.set_font(self.config.get_font())
|
|
||||||
|
|
||||||
bg_override_enabled = self.config.is_bgcolor_override_enabled()
|
|
||||||
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.config.get_bgcolor()):
|
|
||||||
self._ui.bg_color_colorbutton.set_rgba(color)
|
|
||||||
@@ -1,111 +1,91 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
from functools import partial
|
||||||
|
|
||||||
from gajim.plugins import GajimPlugin
|
from gajim.plugins import GajimPlugin
|
||||||
|
from gajim.plugins.plugins_i18n import _
|
||||||
|
|
||||||
from syntax_highlight.types import LineBreakOptions
|
from syntax_highlight.types import LineBreakOptions
|
||||||
from syntax_highlight.types import CodeMarkerOptions
|
from syntax_highlight.types import CodeMarkerOptions
|
||||||
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
|
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
|
||||||
if sys.version_info >= (3, 4):
|
|
||||||
from importlib.util import find_spec as find_module
|
|
||||||
else:
|
|
||||||
from importlib import find_loader as find_module
|
|
||||||
|
|
||||||
PYGMENTS_MISSING = 'You are missing Python-Pygments.'
|
|
||||||
|
|
||||||
log = logging.getLogger('gajim.p.syntax_highlight')
|
log = logging.getLogger('gajim.p.syntax_highlight')
|
||||||
|
|
||||||
|
HAS_PYGMENTS = False
|
||||||
def try_loading_pygments():
|
|
||||||
success = find_module('pygments') is not None
|
|
||||||
if success:
|
|
||||||
try:
|
try:
|
||||||
from syntax_highlight.chat_syntax_highlighter import \
|
from syntax_highlight.chat_syntax_highlighter import ChatSyntaxHighlighter
|
||||||
ChatSyntaxHighlighter
|
from syntax_highlight.config_dialog import SyntaxHighlighterPluginConfig
|
||||||
from syntax_highlight.plugin_config_dialog import \
|
from syntax_highlight.highlighter_config import HighlighterConfig
|
||||||
SyntaxHighlighterPluginConfiguration
|
HAS_PYGMENTS = True
|
||||||
from syntax_highlight.plugin_config import SyntaxHighlighterConfig
|
|
||||||
global SyntaxHighlighterPluginConfiguration
|
|
||||||
global ChatSyntaxHighlighter
|
|
||||||
global SyntaxHighlighterConfig
|
|
||||||
success = True
|
|
||||||
log.debug("pygments loaded.")
|
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
log.error("Import Error: %s.", exception)
|
log.error('Could not load pygments: %s', exception)
|
||||||
success = False
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
|
|
||||||
class SyntaxHighlighterPlugin(GajimPlugin):
|
class SyntaxHighlighterPlugin(GajimPlugin):
|
||||||
def on_connect_with_chat_control(self, chat_control):
|
|
||||||
account = chat_control.contact.account.name
|
|
||||||
jid = chat_control.contact.jid
|
|
||||||
if account not in self.ccontrol:
|
|
||||||
self.ccontrol[account] = {}
|
|
||||||
self.ccontrol[account][jid] = ChatSyntaxHighlighter(
|
|
||||||
self.conf, chat_control.conv_textview)
|
|
||||||
|
|
||||||
def on_disconnect_from_chat_control(self, chat_control):
|
|
||||||
account = chat_control.contact.account.name
|
|
||||||
jid = chat_control.contact.jid
|
|
||||||
del self.ccontrol[account][jid]
|
|
||||||
|
|
||||||
def on_print_real_text(self, text_view, real_text, other_tags, graphics,
|
|
||||||
iterator, additional):
|
|
||||||
account = text_view.account
|
|
||||||
for jid in self.ccontrol[account]:
|
|
||||||
if self.ccontrol[account][jid].textview != text_view:
|
|
||||||
continue
|
|
||||||
self.ccontrol[account][jid].process_text(
|
|
||||||
real_text, other_tags, graphics, iterator, additional)
|
|
||||||
return
|
|
||||||
|
|
||||||
def try_init(self):
|
|
||||||
"""
|
|
||||||
Separating this part of the initialization from the init() method
|
|
||||||
allows repeating this step again, without reloading the plugin,
|
|
||||||
i.e. restarting Gajim for instance.
|
|
||||||
Doing so allows resolving the dependency issues without restart :)
|
|
||||||
"""
|
|
||||||
pygments_loaded = try_loading_pygments()
|
|
||||||
if not pygments_loaded:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.activatable = True
|
|
||||||
self.available_text = None
|
|
||||||
self.config_dialog = SyntaxHighlighterPluginConfiguration(self)
|
|
||||||
|
|
||||||
self.conf = SyntaxHighlighterConfig(self.config)
|
|
||||||
# The following initialization requires pygments to be available.
|
|
||||||
self.conf.init_pygments()
|
|
||||||
|
|
||||||
self.config_dialog = SyntaxHighlighterPluginConfiguration(self)
|
|
||||||
self.config_dialog.set_config(self.conf)
|
|
||||||
|
|
||||||
self.gui_extension_points = {
|
|
||||||
'chat_control_base': (
|
|
||||||
self.on_connect_with_chat_control,
|
|
||||||
self.on_disconnect_from_chat_control),
|
|
||||||
'print_real_text': (self.on_print_real_text, None), }
|
|
||||||
return True
|
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
self.ccontrol = {}
|
self.description = _(
|
||||||
|
'Source code syntax highlighting in the chat window.\n\n'
|
||||||
|
'Markdown-style syntax is supported, i.e. text inbetween '
|
||||||
|
'`single backticks` is rendered as inline code.\n'
|
||||||
|
'```language\n'
|
||||||
|
'selection is possible in multi-line code snippets inbetween '
|
||||||
|
'triple-backticks\n'
|
||||||
|
'Note the newlines in this case…\n'
|
||||||
|
'```\n\n'
|
||||||
|
'Changed settings will take effect after re-opening the message '
|
||||||
|
'tab/window.')
|
||||||
|
|
||||||
self.config_default_values = {
|
self.config_default_values = {
|
||||||
'default_lexer': (PLUGIN_INTERNAL_NONE_LEXER_ID, ''),
|
'default_lexer': (PLUGIN_INTERNAL_NONE_LEXER_ID, ''),
|
||||||
'line_break': (LineBreakOptions.MULTILINE, ''),
|
'line_break': (LineBreakOptions.MULTILINE, ''),
|
||||||
'style': ('default', ''),
|
'style': ('default', ''),
|
||||||
'font': ('Monospace 10', ''),
|
'font': ('Monospace 10', ''),
|
||||||
'bgcolor': ('#ccc', ''),
|
'bgcolor': ('rgb(200, 200, 200)', ''),
|
||||||
'bgcolor_override': (True, ''),
|
'bgcolor_override': (True, ''),
|
||||||
'code_marker': (CodeMarkerOptions.AS_COMMENT, ''),
|
'code_marker': (CodeMarkerOptions.AS_COMMENT, ''),
|
||||||
'pygments_path': (None, ''), }
|
}
|
||||||
|
|
||||||
is_initialized = self.try_init()
|
self.gui_extension_points = {
|
||||||
|
'chat_control_base': (
|
||||||
|
self._connect_chat_control,
|
||||||
|
self._disconnect_chat_control),
|
||||||
|
'print_real_text': (self._on_print_real_text, None)
|
||||||
|
}
|
||||||
|
|
||||||
if not is_initialized:
|
if not HAS_PYGMENTS:
|
||||||
self.activatable = False
|
self.activatable = False
|
||||||
self.available_text = PYGMENTS_MISSING
|
self.available_text = _('You are missing python-pygments.')
|
||||||
self.config_dialog = None
|
self.config_dialog = None
|
||||||
|
|
||||||
|
self._migrate_settings()
|
||||||
|
self._highlighters = {}
|
||||||
|
self.config_dialog = partial(SyntaxHighlighterPluginConfig, self)
|
||||||
|
self.highlighter_config = HighlighterConfig(self.config)
|
||||||
|
|
||||||
|
def _migrate_settings(self):
|
||||||
|
line_break = self.config['line_break']
|
||||||
|
if isinstance(line_break, int):
|
||||||
|
self.config['line_break'] = LineBreakOptions(line_break)
|
||||||
|
|
||||||
|
def _connect_chat_control(self, chat_control):
|
||||||
|
highlighter = ChatSyntaxHighlighter(
|
||||||
|
self.config, self.highlighter_config, chat_control.conv_textview)
|
||||||
|
self._highlighters[chat_control.control_id] = highlighter
|
||||||
|
|
||||||
|
def _disconnect_chat_control(self, chat_control):
|
||||||
|
highlighter = self._highlighters.get(chat_control.control_id)
|
||||||
|
if highlighter is not None:
|
||||||
|
del highlighter
|
||||||
|
self._highlighters.pop(chat_control.control_id, None)
|
||||||
|
|
||||||
|
def _on_print_real_text(self, text_view, real_text, other_tags, graphics,
|
||||||
|
iterator, additional):
|
||||||
|
for highlighter in self._highlighters.values():
|
||||||
|
if highlighter.textview != text_view:
|
||||||
|
continue
|
||||||
|
highlighter.process_text(
|
||||||
|
real_text, other_tags, graphics, iterator, additional)
|
||||||
|
return
|
||||||
|
|
||||||
|
def update_highlighters(self):
|
||||||
|
for highlighter in self._highlighters.values():
|
||||||
|
highlighter.update_config(self.config)
|
||||||
|
|||||||
Reference in New Issue
Block a user