''' Code Input ========== .. versionadded:: 1.5.0 .. image:: images/codeinput.jpg .. note:: This widget requires ``pygments`` package to run. Install it with ``pip``. The :class:`CodeInput` provides a box of editable highlighted text like the one shown in the image. It supports all the features provided by the :class:`~kivy.uix.textinput` as well as code highlighting for `languages supported by pygments `_ along with `KivyLexer` for :mod:`kivy.lang` highlighting. Usage example ------------- To create a CodeInput with highlighting for `KV language`:: from kivy.uix.codeinput import CodeInput from kivy.extras.highlight import KivyLexer codeinput = CodeInput(lexer=KivyLexer()) To create a CodeInput with highlighting for `Cython`:: from kivy.uix.codeinput import CodeInput from pygments.lexers import CythonLexer codeinput = CodeInput(lexer=CythonLexer()) ''' __all__ = ('CodeInput', ) from pygments import highlight from pygments import lexers from pygments import styles from pygments.formatters import BBCodeFormatter from kivy.uix.textinput import TextInput from kivy.core.text.markup import MarkupLabel as Label from kivy.cache import Cache from kivy.properties import ObjectProperty, OptionProperty from kivy.utils import get_hex_from_color, get_color_from_hex from kivy.uix.behaviors import CodeNavigationBehavior Cache_get = Cache.get Cache_append = Cache.append # TODO: color chooser for keywords/strings/... class CodeInput(CodeNavigationBehavior, TextInput): '''CodeInput class, used for displaying highlighted code. ''' lexer = ObjectProperty(None) '''This holds the selected Lexer used by pygments to highlight the code. :attr:`lexer` is an :class:`~kivy.properties.ObjectProperty` and defaults to `PythonLexer`. ''' style_name = OptionProperty( 'default', options=list(styles.get_all_styles()) ) '''Name of the pygments style to use for formatting. :attr:`style_name` is an :class:`~kivy.properties.OptionProperty` and defaults to ``'default'``. ''' style = ObjectProperty(None) '''The pygments style object to use for formatting. When ``style_name`` is set, this will be changed to the corresponding style object. :attr:`style` is a :class:`~kivy.properties.ObjectProperty` and defaults to ``None`` ''' def __init__(self, **kwargs): stylename = kwargs.get('style_name', 'default') style = kwargs['style'] if 'style' in kwargs \ else styles.get_style_by_name(stylename) self.formatter = BBCodeFormatter(style=style) self.lexer = lexers.PythonLexer() self.text_color = '#000000' self._label_cached = Label() self.use_text_color = True super(CodeInput, self).__init__(**kwargs) self._line_options = kw = self._get_line_options() self._label_cached = Label(**kw) # use text_color as foreground color text_color = kwargs.get('foreground_color') if text_color: self.text_color = get_hex_from_color(text_color) # set foreground to white to allow text colors to show # use text_color as the default color in bbcodes self.use_text_color = False self.foreground_color = [1, 1, 1, .999] if not kwargs.get('background_color'): self.background_color = [.9, .92, .92, 1] def on_style_name(self, *args): self.style = styles.get_style_by_name(self.style_name) self.background_color = get_color_from_hex(self.style.background_color) self._trigger_refresh_text() def on_style(self, *args): self.formatter = BBCodeFormatter(style=self.style) self._trigger_update_graphics() def _create_line_label(self, text, hint=False): # Create a label from a text, using line options ntext = text.replace(u'\n', u'').replace(u'\t', u' ' * self.tab_width) if self.password and not hint: # Don't replace hint_text with * ntext = u'*' * len(ntext) ntext = self._get_bbcode(ntext) kw = self._get_line_options() cid = u'{}\0{}\0{}'.format(text, self.password, kw) texture = Cache_get('textinput.label', cid) if texture is None: # FIXME right now, we can't render very long line... # if we move on "VBO" version as fallback, we won't need to # do this. # try to find the maximum text we can handle label = Label(text=ntext, **kw) if text.find(u'\n') > 0: label.text = u'' else: label.text = ntext label.refresh() # ok, we found it. texture = label.texture Cache_append('textinput.label', cid, texture) label.text = '' return texture def _get_line_options(self): kw = super(CodeInput, self)._get_line_options() kw['markup'] = True kw['valign'] = 'top' kw['codeinput'] = repr(self.lexer) return kw def _get_text_width(self, text, tab_width, _label_cached): # Return the width of a text, according to the current line options. cid = u'{}\0{}\0{}'.format(text, self.password, self._get_line_options()) width = Cache_get('textinput.width', cid) if width is not None: return width lbl = self._create_line_label(text) width = lbl.width Cache_append('textinput.width', cid, width) return width def _get_bbcode(self, ntext): # get bbcoded text for python try: ntext[0] # replace brackets with special chars that aren't highlighted # by pygment. can't use &bl; ... cause & is highlighted ntext = ntext.replace(u'[', u'\x01').replace(u']', u'\x02') ntext = highlight(ntext, self.lexer, self.formatter) ntext = ntext.replace(u'\x01', u'&bl;').replace(u'\x02', u'&br;') # replace special chars with &bl; and &br; ntext = ''.join((u'[color=', str(self.text_color), u']', ntext, u'[/color]')) ntext = ntext.replace(u'\n', u'') # remove possible extra highlight options ntext = ntext.replace(u'[u]', '').replace(u'[/u]', '') return ntext except IndexError: return '' # overridden to prevent cursor position off screen def _cursor_offset(self): '''Get the cursor x offset on the current line ''' offset = 0 try: if self.cursor_col: offset = self._get_text_width( self._lines[self.cursor_row][:self.cursor_col]) return offset except: pass finally: return offset def on_lexer(self, instance, value): self._trigger_refresh_text() def on_foreground_color(self, instance, text_color): if not self.use_text_color: self.use_text_color = True return self.text_color = get_hex_from_color(text_color) self.use_text_color = False self.foreground_color = (1, 1, 1, .999) self._trigger_refresh_text() if __name__ == '__main__': from kivy.extras.highlight import KivyLexer from kivy.app import App class CodeInputTest(App): def build(self): return CodeInput(lexer=KivyLexer(), font_size=12, text=''' #:kivy 1.0 : canvas: Color: rgb: .5, .5, .5 Rectangle: pos: self.pos size: self.size''') CodeInputTest().run()