Repository URL to install this package:
Version:
7.2.1 ▾
|
# (C) Copyright 2004-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines a source code editor for the wxPython user interface toolkit,
useful for tools such as debuggers.
"""
import wx
import wx.stc as stc
from traits.api import Str, List, Int, Event, Bool, TraitError, observe
from traits.trait_base import SequenceTypes
from pyface.api import PythonEditor
from pyface.wx.python_stc import faces
from .editor import Editor
from .constants import OKColor, ErrorColor
# Marker line constants:
# Marks a marked line
MARK_MARKER = 0
# Marks a line matching the current search
SEARCH_MARKER = 1
# Marks the currently selected line
SELECTED_MARKER = 2
# -------------------------------------------------------------------------
# 'SourceEditor' class:
# -------------------------------------------------------------------------
class SourceEditor(Editor):
""" Editor for source code, which displays a Pyface PythonEditor.
"""
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
#: The code editor is scrollable. This value overrides the default.
scrollable = True
#: Is the editor read only?
readonly = Bool(False)
#: The currently selected line
selected_line = Int()
#: The currently selected text
selected_text = Str()
#: The list of line numbers to mark
mark_lines = List(Int)
#: The current line number
line = Event()
#: The current column
column = Event()
#: calltip clicked event
calltip_clicked = Event()
#: The STC lexer use
lexer = Int()
#: The lines to be dimmed
dim_lines = List(Int)
dim_color = Str()
_dim_style_number = Int(16) # 0-15 are reserved for the python lexer
#: The lines to have squiggles drawn under them
squiggle_lines = List(Int)
squiggle_color = Str()
def init(self, parent):
""" Finishes initializing the editor by creating the underlying toolkit
widget.
"""
factory = self.factory
self._editor = editor = PythonEditor(
parent, show_line_numbers=factory.show_line_numbers
)
self.control = control = editor.control
# There are a number of events which aren't well documented that look
# to be useful in future implmentations, below are a subset of the
# events that look interesting:
# EVT_STC_AUTOCOMP_SELECTION
# EVT_STC_HOTSPOT_CLICK
# EVT_STC_HOTSPOT_DCLICK
# EVT_STC_DOUBLECLICK
# EVT_STC_MARGINCLICK
control.SetSize(wx.Size(300, 124))
# Clear out the goofy hotkeys for zooming text
control.CmdKeyClear(ord("B"), stc.STC_SCMOD_CTRL)
control.CmdKeyClear(ord("N"), stc.STC_SCMOD_CTRL)
# Set up the events
control.Bind(wx.EVT_KILL_FOCUS, self.wx_update_object)
control.Bind(stc.EVT_STC_CALLTIP_CLICK, self._calltip_clicked)
if factory.auto_scroll and (factory.selected_line != ""):
control.Bind(wx.EVT_SIZE, self._update_selected_line)
if factory.auto_set:
editor.on_trait_change(
self.update_object, "changed", dispatch="ui"
)
if factory.key_bindings is not None:
editor.on_trait_change(
self.key_pressed, "key_pressed", dispatch="ui"
)
if self.readonly:
control.SetReadOnly(True)
# Set up the lexer
control.SetLexer(stc.STC_LEX_CONTAINER)
control.Bind(stc.EVT_STC_STYLENEEDED, self._style_needed)
try:
self.lexer = getattr(stc, "STC_LEX_" + self.factory.lexer.upper())
except AttributeError:
self.lexer = stc.STC_LEX_NULL
# Define the markers we use:
control.MarkerDefine(
MARK_MARKER,
stc.STC_MARK_BACKGROUND,
background=wx.Colour(factory.mark_color_),
)
control.MarkerDefine(
SEARCH_MARKER,
stc.STC_MARK_BACKGROUND,
background=wx.Colour(factory.search_color_),
)
control.MarkerDefine(
SELECTED_MARKER,
stc.STC_MARK_BACKGROUND,
background=wx.Colour(factory.selected_color_),
)
# Make sure the editor has been initialized:
self.update_editor()
# Set up any event listeners:
self.sync_value(factory.mark_lines, "mark_lines", "from", is_list=True)
self.sync_value(factory.selected_line, "selected_line", "from")
self.sync_value(factory.selected_text, "selected_text", "to")
self.sync_value(factory.line, "line")
self.sync_value(factory.column, "column")
self.sync_value(factory.calltip_clicked, "calltip_clicked")
self.sync_value(factory.dim_lines, "dim_lines", "from", is_list=True)
if self.factory.dim_color == "":
self.dim_color = "dark grey"
else:
self.sync_value(factory.dim_color, "dim_color", "from")
self.sync_value(
factory.squiggle_lines, "squiggle_lines", "from", is_list=True
)
if factory.squiggle_color == "":
self.squiggle_color = "red"
else:
self.sync_value(factory.squiggle_color, "squiggle_color", "from")
# Check if we need to monitor the line or column position being
# changed:
if (
(factory.line != "")
or (factory.column != "")
or (factory.selected_text != "")
):
control.Bind(stc.EVT_STC_UPDATEUI, self._position_changed)
self.set_tooltip()
def wx_update_object(self, event):
""" Handles the user entering input data in the edit control.
"""
self.update_object()
event.Skip()
def update_object(self):
""" Handles the user entering input data in the edit control.
"""
if not self._locked:
try:
value = self.control.GetText()
if isinstance(self.value, SequenceTypes):
value = value.split()
self.value = value
self.control.SetBackgroundColour(OKColor)
self.control.Refresh()
except TraitError as excp:
pass
def update_editor(self):
""" Updates the editor when the object trait changes externally to the
editor.
"""
self._locked = True
new_value = self.value
if isinstance(new_value, SequenceTypes):
new_value = "\n".join([line.rstrip() for line in new_value])
control = self.control
if control.GetText() != new_value:
readonly = control.GetReadOnly()
control.SetReadOnly(False)
l1 = control.GetFirstVisibleLine()
pos = control.GetCurrentPos()
control.SetText(new_value)
control.GotoPos(pos)
control.ScrollToLine(l1)
control.SetReadOnly(readonly)
self._mark_lines_changed()
self._selected_line_changed()
self._style_document()
self._locked = False
def _calltip_clicked(self, event):
self.calltip_clicked = True
def _mark_lines_changed(self):
""" Handles the set of marked lines being changed.
"""
lines = self.mark_lines
control = self.control
lc = control.GetLineCount()
control.MarkerDeleteAll(MARK_MARKER)
for line in lines:
if 0 < line <= lc:
control.MarkerAdd(line - 1, MARK_MARKER)
control.Refresh()
def _mark_lines_items_changed(self):
self._mark_lines_changed()
def _selected_line_changed(self):
""" Handles a change in which line is currently selected.
"""
line = self.selected_line
control = self.control
line = max(1, min(control.GetLineCount(), line)) - 1
control.MarkerDeleteAll(SELECTED_MARKER)
control.MarkerAdd(line, SELECTED_MARKER)
control.GotoLine(line)
if self.factory.auto_scroll:
control.ScrollToLine(line - (control.LinesOnScreen() // 2))
control.Refresh()
def _line_changed(self, line):
if not self._locked:
self.control.GotoLine(line - 1)
def _column_changed(self, column):
if not self._locked:
control = self.control
line = control.LineFromPosition(control.GetCurrentPos())
control.GotoPos(control.PositionFromLine(line) + column - 1)
def _position_changed(self, event):
""" Handles the cursor position being changed.
"""
control = self.control
pos = control.GetCurrentPos()
line = control.LineFromPosition(pos)
self._locked = True
self.line = line + 1
self.column = pos - control.PositionFromLine(line) + 1
self._locked = False
self.selected_text = control.GetSelectedText()
def key_pressed(self, event):
""" Handles a key being pressed within the editor.
"""
self.factory.key_bindings.do(
event.event, self.ui.handler, self.ui.info
)
def _dim_color_changed(self):
self.control.StyleSetForeground(self._dim_style_number, self.dim_color)
self.control.StyleSetFaceName(self._dim_style_number, "courier new")
self.control.StyleSetSize(self._dim_style_number, faces["size"])
self.control.Refresh()
def _squiggle_color_changed(self):
self.control.IndicatorSetStyle(2, stc.STC_INDIC_SQUIGGLE)
self.control.IndicatorSetForeground(2, self.squiggle_color)
self.control.Refresh()
@observe("dim_lines, squiggle_lines")
def _style_document(self, event=None):
""" Force the STC to fire a STC_STYLENEEDED event for the entire
document.
"""
self.control.ClearDocumentStyle()
self.control.Colourise(0, -1)
self.control.Refresh()
def _style_needed(self, event):
""" Handles an STC request for styling for some area.
"""
position = self.control.GetEndStyled()
start_line = self.control.LineFromPosition(position)
end = event.GetPosition()
end_line = self.control.LineFromPosition(end)
# Fixes a strange a bug with the STC widget where creating a new line
# after a dimmed line causes it to mysteriously lose its styling
if start_line in self.dim_lines:
start_line -= 1
# Trying to Colourise only the lines that we want does not seem to work
# so we do the whole area and then override the styling on certain
# lines
if self.lexer != stc.STC_LEX_NULL:
self.control.SetLexer(self.lexer)
self.control.Colourise(position, end)
self.control.SetLexer(stc.STC_LEX_CONTAINER)
for line in range(start_line, end_line + 1):
# We don't use LineLength here because it includes newline
# characters. Styling these leads to strange behavior.
position = self.control.PositionFromLine(line)
style_length = self.control.GetLineEndPosition(line) - position
if line + 1 in self.dim_lines:
# Set styling mask to only style text bits, not indicator bits
self.control.StartStyling(position)
self.control.SetStyling(style_length, self._dim_style_number)
elif self.lexer == stc.STC_LEX_NULL:
self.control.StartStyling(position)
self.control.SetStyling(style_length, stc.STC_STYLE_DEFAULT)
if line + 1 in self.squiggle_lines:
self.control.StartStyling(position)
self.control.SetStyling(style_length, stc.STC_INDIC2_MASK)
else:
self.control.StartStyling(position)
self.control.SetStyling(style_length, stc.STC_STYLE_DEFAULT)
def error(self, excp):
""" Handles an error that occurs while setting the object's trait value.
"""
self.control.SetBackgroundColour(ErrorColor)
self.control.Refresh()
def dispose(self):
""" Disposes of the contents of an editor.
"""
if self.control is not None:
if self.factory.auto_set:
self._editor.on_trait_change(
self.update_object, "changed", remove=True
)
if self.factory.key_bindings is not None:
self._editor.on_trait_change(
self.key_pressed, "key_pressed", remove=True
)
self.control.Unbind(wx.EVT_KILL_FOCUS)
self.control.Unbind(stc.EVT_STC_CALLTIP_CLICK)
self.control.Unbind(stc.EVT_STC_STYLENEEDED)
if (
(self.factory.line != "")
or (self.factory.column != "")
or (self.factory.selected_text != "")
):
self.control.Unbind(stc.EVT_STC_UPDATEUI)
super().dispose()
# -- UI preference save/restore interface ---------------------------------
def restore_prefs(self, prefs):
""" Restores any saved user preference information associated with the
editor.
"""
if self.factory.key_bindings is not None:
key_bindings = prefs.get("key_bindings")
if key_bindings is not None:
self.factory.key_bindings.merge(key_bindings)
def save_prefs(self):
""" Returns any user preference information associated with the editor.
"""
return {"key_bindings": self.factory.key_bindings}
# Define the simple, custom, text and readonly editors, which will be accessed
# by the editor factory for code editors.
CustomEditor = SimpleEditor = TextEditor = SourceEditor
class ReadonlyEditor(SourceEditor):
#: Set the value of the readonly trait.
readonly = True