Show More
@@ -0,0 +1,156 b'' | |||||
|
1 | # System library imports | |||
|
2 | from PyQt4 import QtCore, QtGui | |||
|
3 | ||||
|
4 | ||||
|
5 | class CallTipWidget(QtGui.QLabel): | |||
|
6 | """ Shows call tips by parsing the current text of Q[Plain]TextEdit. | |||
|
7 | """ | |||
|
8 | ||||
|
9 | #-------------------------------------------------------------------------- | |||
|
10 | # 'QWidget' interface | |||
|
11 | #-------------------------------------------------------------------------- | |||
|
12 | ||||
|
13 | def __init__(self, parent): | |||
|
14 | """ Create a call tip manager that is attached to the specified Qt | |||
|
15 | text edit widget. | |||
|
16 | """ | |||
|
17 | assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | |||
|
18 | QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip) | |||
|
19 | ||||
|
20 | self.setFont(parent.document().defaultFont()) | |||
|
21 | self.setForegroundRole(QtGui.QPalette.ToolTipText) | |||
|
22 | self.setBackgroundRole(QtGui.QPalette.ToolTipBase) | |||
|
23 | self.setPalette(QtGui.QToolTip.palette()) | |||
|
24 | ||||
|
25 | self.setAlignment(QtCore.Qt.AlignLeft) | |||
|
26 | self.setIndent(1) | |||
|
27 | self.setFrameStyle(QtGui.QFrame.NoFrame) | |||
|
28 | self.setMargin(1 + self.style().pixelMetric( | |||
|
29 | QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self)) | |||
|
30 | self.setWindowOpacity(self.style().styleHint( | |||
|
31 | QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0) | |||
|
32 | ||||
|
33 | def hideEvent(self, event): | |||
|
34 | """ Reimplemented to disconnect the cursor movement handler. | |||
|
35 | """ | |||
|
36 | QtGui.QListWidget.hideEvent(self, event) | |||
|
37 | self.parent().cursorPositionChanged.disconnect(self._update_tip) | |||
|
38 | ||||
|
39 | def paintEvent(self, event): | |||
|
40 | """ Reimplemented to paint the background panel. | |||
|
41 | """ | |||
|
42 | painter = QtGui.QStylePainter(self) | |||
|
43 | option = QtGui.QStyleOptionFrame() | |||
|
44 | option.init(self) | |||
|
45 | painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option) | |||
|
46 | painter.end() | |||
|
47 | ||||
|
48 | QtGui.QLabel.paintEvent(self, event) | |||
|
49 | ||||
|
50 | def showEvent(self, event): | |||
|
51 | """ Reimplemented to connect the cursor movement handler. | |||
|
52 | """ | |||
|
53 | QtGui.QListWidget.showEvent(self, event) | |||
|
54 | self.parent().cursorPositionChanged.connect(self._update_tip) | |||
|
55 | ||||
|
56 | #-------------------------------------------------------------------------- | |||
|
57 | # 'CallTipWidget' interface | |||
|
58 | #-------------------------------------------------------------------------- | |||
|
59 | ||||
|
60 | def show_tip(self, tip): | |||
|
61 | """ Attempts to show the specified tip at the current cursor location. | |||
|
62 | """ | |||
|
63 | text_edit = self.parent() | |||
|
64 | document = text_edit.document() | |||
|
65 | cursor = text_edit.textCursor() | |||
|
66 | search_pos = cursor.position() - 1 | |||
|
67 | self._start_position, _ = self._find_parenthesis(search_pos, | |||
|
68 | forward=False) | |||
|
69 | if self._start_position == -1: | |||
|
70 | return False | |||
|
71 | ||||
|
72 | point = text_edit.cursorRect(cursor).bottomRight() | |||
|
73 | point = text_edit.mapToGlobal(point) | |||
|
74 | self.move(point) | |||
|
75 | self.setText(tip) | |||
|
76 | if self.isVisible(): | |||
|
77 | self.resize(self.sizeHint()) | |||
|
78 | else: | |||
|
79 | self.show() | |||
|
80 | return True | |||
|
81 | ||||
|
82 | #-------------------------------------------------------------------------- | |||
|
83 | # Protected interface | |||
|
84 | #-------------------------------------------------------------------------- | |||
|
85 | ||||
|
86 | def _find_parenthesis(self, position, forward=True): | |||
|
87 | """ If 'forward' is True (resp. False), proceed forwards | |||
|
88 | (resp. backwards) through the line that contains 'position' until an | |||
|
89 | unmatched closing (resp. opening) parenthesis is found. Returns a | |||
|
90 | tuple containing the position of this parenthesis (or -1 if it is | |||
|
91 | not found) and the number commas (at depth 0) found along the way. | |||
|
92 | """ | |||
|
93 | commas = depth = 0 | |||
|
94 | document = self.parent().document() | |||
|
95 | qchar = document.characterAt(position) | |||
|
96 | while (position > 0 and qchar.isPrint() and | |||
|
97 | # Need to check explicitly for line/paragraph separators: | |||
|
98 | qchar.unicode() not in (0x2028, 0x2029)): | |||
|
99 | char = qchar.toAscii() | |||
|
100 | if char == ',' and depth == 0: | |||
|
101 | commas += 1 | |||
|
102 | elif char == ')': | |||
|
103 | if forward and depth == 0: | |||
|
104 | break | |||
|
105 | depth += 1 | |||
|
106 | elif char == '(': | |||
|
107 | if not forward and depth == 0: | |||
|
108 | break | |||
|
109 | depth -= 1 | |||
|
110 | position += 1 if forward else -1 | |||
|
111 | qchar = document.characterAt(position) | |||
|
112 | else: | |||
|
113 | position = -1 | |||
|
114 | return position, commas | |||
|
115 | ||||
|
116 | def _highlight_tip(self, tip, current_argument): | |||
|
117 | """ Highlight the current argument (arguments start at 0), ending at the | |||
|
118 | next comma or unmatched closing parenthesis. | |||
|
119 | ||||
|
120 | FIXME: This is an unreliable way to do things and it isn't being | |||
|
121 | used right now. Instead, we should use inspect.getargspec | |||
|
122 | metadata for this purpose. | |||
|
123 | """ | |||
|
124 | start = tip.find('(') | |||
|
125 | if start != -1: | |||
|
126 | for i in xrange(current_argument): | |||
|
127 | start = tip.find(',', start) | |||
|
128 | if start != -1: | |||
|
129 | end = start + 1 | |||
|
130 | while end < len(tip): | |||
|
131 | char = tip[end] | |||
|
132 | depth = 0 | |||
|
133 | if (char == ',' and depth == 0): | |||
|
134 | break | |||
|
135 | elif char == '(': | |||
|
136 | depth += 1 | |||
|
137 | elif char == ')': | |||
|
138 | if depth == 0: | |||
|
139 | break | |||
|
140 | depth -= 1 | |||
|
141 | end += 1 | |||
|
142 | tip = tip[:start+1] + '<font color="blue">' + \ | |||
|
143 | tip[start+1:end] + '</font>' + tip[end:] | |||
|
144 | tip = tip.replace('\n', '<br/>') | |||
|
145 | return tip | |||
|
146 | ||||
|
147 | def _update_tip(self): | |||
|
148 | """ Updates the tip based on user cursor movement. | |||
|
149 | """ | |||
|
150 | cursor = self.parent().textCursor() | |||
|
151 | if cursor.position() <= self._start_position: | |||
|
152 | self.hide() | |||
|
153 | else: | |||
|
154 | position, commas = self._find_parenthesis(self._start_position + 1) | |||
|
155 | if position != -1: | |||
|
156 | self.hide() |
@@ -0,0 +1,57 b'' | |||||
|
1 | # System library imports | |||
|
2 | from pygments.token import Token, is_token_subtype | |||
|
3 | ||||
|
4 | ||||
|
5 | class CompletionLexer(object): | |||
|
6 | """ Uses Pygments and some auxillary information to lex code snippets for | |||
|
7 | symbol contexts. | |||
|
8 | """ | |||
|
9 | ||||
|
10 | # Maps Lexer names to a list of possible name separators | |||
|
11 | separator_map = { 'C' : [ '.', '->' ], | |||
|
12 | 'C++' : [ '.', '->', '::' ], | |||
|
13 | 'Python' : [ '.' ] } | |||
|
14 | ||||
|
15 | def __init__(self, lexer): | |||
|
16 | self.lexer = lexer | |||
|
17 | ||||
|
18 | def get_context(self, string): | |||
|
19 | """ Assuming the cursor is at the end of the specified string, get the | |||
|
20 | context (a list of names) for the symbol at cursor position. | |||
|
21 | """ | |||
|
22 | context = [] | |||
|
23 | reversed_tokens = list(self._lexer.get_tokens(string)) | |||
|
24 | reversed_tokens.reverse() | |||
|
25 | ||||
|
26 | # Pygments often tacks on a newline when none is specified in the input | |||
|
27 | if reversed_tokens and reversed_tokens[0][1].endswith('\n') and \ | |||
|
28 | not string.endswith('\n'): | |||
|
29 | reversed_tokens.pop(0) | |||
|
30 | ||||
|
31 | current_op = unicode() | |||
|
32 | for token, text in reversed_tokens: | |||
|
33 | if is_token_subtype(token, Token.Name) and \ | |||
|
34 | (not context or current_op in self._name_separators): | |||
|
35 | if not context and current_op in self._name_separators: | |||
|
36 | context.insert(0, unicode()) | |||
|
37 | context.insert(0, text) | |||
|
38 | current_op = unicode() | |||
|
39 | elif token is Token.Operator or token is Token.Punctuation: | |||
|
40 | current_op = text + current_op | |||
|
41 | else: | |||
|
42 | break | |||
|
43 | ||||
|
44 | return context | |||
|
45 | ||||
|
46 | def get_lexer(self, lexer): | |||
|
47 | return self._lexer | |||
|
48 | ||||
|
49 | def set_lexer(self, lexer, name_separators=None): | |||
|
50 | self._lexer = lexer | |||
|
51 | if name_separators is None: | |||
|
52 | self._name_separators = self.separator_map.get(lexer.name, ['.']) | |||
|
53 | else: | |||
|
54 | self._name_separators = list(name_separators) | |||
|
55 | ||||
|
56 | lexer = property(get_lexer, set_lexer) | |||
|
57 |
@@ -0,0 +1,121 b'' | |||||
|
1 | # System library imports | |||
|
2 | from PyQt4 import QtCore, QtGui | |||
|
3 | ||||
|
4 | ||||
|
5 | class CompletionWidget(QtGui.QListWidget): | |||
|
6 | """ A widget for GUI tab completion. | |||
|
7 | """ | |||
|
8 | ||||
|
9 | #-------------------------------------------------------------------------- | |||
|
10 | # 'QWidget' interface | |||
|
11 | #-------------------------------------------------------------------------- | |||
|
12 | ||||
|
13 | def __init__(self, parent): | |||
|
14 | """ Create a completion widget that is attached to the specified Qt | |||
|
15 | text edit widget. | |||
|
16 | """ | |||
|
17 | assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | |||
|
18 | QtGui.QListWidget.__init__(self, parent) | |||
|
19 | ||||
|
20 | self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint) | |||
|
21 | self.setAttribute(QtCore.Qt.WA_StaticContents) | |||
|
22 | ||||
|
23 | # Ensure that parent keeps focus when widget is displayed. | |||
|
24 | self.setFocusProxy(parent) | |||
|
25 | ||||
|
26 | self.setFrameShadow(QtGui.QFrame.Plain) | |||
|
27 | self.setFrameShape(QtGui.QFrame.StyledPanel) | |||
|
28 | ||||
|
29 | self.itemActivated.connect(self._complete_current) | |||
|
30 | ||||
|
31 | def hideEvent(self, event): | |||
|
32 | """ Reimplemented to disconnect the cursor movement handler. | |||
|
33 | """ | |||
|
34 | QtGui.QListWidget.hideEvent(self, event) | |||
|
35 | self.parent().cursorPositionChanged.disconnect(self._update_current) | |||
|
36 | ||||
|
37 | def keyPressEvent(self, event): | |||
|
38 | """ Reimplemented to update the list. | |||
|
39 | """ | |||
|
40 | key, text = event.key(), event.text() | |||
|
41 | ||||
|
42 | if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, | |||
|
43 | QtCore.Qt.Key_Tab): | |||
|
44 | self._complete_current() | |||
|
45 | event.accept() | |||
|
46 | ||||
|
47 | elif key == QtCore.Qt.Key_Escape: | |||
|
48 | self.hide() | |||
|
49 | event.accept() | |||
|
50 | ||||
|
51 | elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, | |||
|
52 | QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown, | |||
|
53 | QtCore.Qt.Key_Home, QtCore.Qt.Key_End): | |||
|
54 | QtGui.QListWidget.keyPressEvent(self, event) | |||
|
55 | event.accept() | |||
|
56 | ||||
|
57 | else: | |||
|
58 | event.ignore() | |||
|
59 | ||||
|
60 | def showEvent(self, event): | |||
|
61 | """ Reimplemented to connect the cursor movement handler. | |||
|
62 | """ | |||
|
63 | QtGui.QListWidget.showEvent(self, event) | |||
|
64 | self.parent().cursorPositionChanged.connect(self._update_current) | |||
|
65 | ||||
|
66 | #-------------------------------------------------------------------------- | |||
|
67 | # 'CompletionWidget' interface | |||
|
68 | #-------------------------------------------------------------------------- | |||
|
69 | ||||
|
70 | def show_items(self, cursor, items): | |||
|
71 | """ Shows the completion widget with 'items' at the position specified | |||
|
72 | by 'cursor'. | |||
|
73 | """ | |||
|
74 | text_edit = self.parent() | |||
|
75 | point = text_edit.cursorRect(cursor).bottomRight() | |||
|
76 | point = text_edit.mapToGlobal(point) | |||
|
77 | screen_rect = QtGui.QApplication.desktop().availableGeometry(self) | |||
|
78 | if screen_rect.size().height() - point.y() - self.height() < 0: | |||
|
79 | point = text_edit.mapToGlobal(text_edit.cursorRect().topRight()) | |||
|
80 | point.setY(point.y() - self.height()) | |||
|
81 | self.move(point) | |||
|
82 | ||||
|
83 | self._start_position = cursor.position() | |||
|
84 | self.clear() | |||
|
85 | self.addItems(items) | |||
|
86 | self.setCurrentRow(0) | |||
|
87 | self.show() | |||
|
88 | ||||
|
89 | #-------------------------------------------------------------------------- | |||
|
90 | # Protected interface | |||
|
91 | #-------------------------------------------------------------------------- | |||
|
92 | ||||
|
93 | def _complete_current(self): | |||
|
94 | """ Perform the completion with the currently selected item. | |||
|
95 | """ | |||
|
96 | self._current_text_cursor().insertText(self.currentItem().text()) | |||
|
97 | self.hide() | |||
|
98 | ||||
|
99 | def _current_text_cursor(self): | |||
|
100 | """ Returns a cursor with text between the start position and the | |||
|
101 | current position selected. | |||
|
102 | """ | |||
|
103 | cursor = self.parent().textCursor() | |||
|
104 | if cursor.position() >= self._start_position: | |||
|
105 | cursor.setPosition(self._start_position, | |||
|
106 | QtGui.QTextCursor.KeepAnchor) | |||
|
107 | return cursor | |||
|
108 | ||||
|
109 | def _update_current(self): | |||
|
110 | """ Updates the current item based on the current text. | |||
|
111 | """ | |||
|
112 | prefix = self._current_text_cursor().selectedText() | |||
|
113 | if prefix: | |||
|
114 | items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith | | |||
|
115 | QtCore.Qt.MatchCaseSensitive)) | |||
|
116 | if items: | |||
|
117 | self.setCurrentItem(items[0]) | |||
|
118 | else: | |||
|
119 | self.hide() | |||
|
120 | else: | |||
|
121 | self.hide() |
This diff has been collapsed as it changes many lines, (650 lines changed) Show them Hide them | |||||
@@ -0,0 +1,650 b'' | |||||
|
1 | # Standard library imports | |||
|
2 | import re | |||
|
3 | ||||
|
4 | # System library imports | |||
|
5 | from PyQt4 import QtCore, QtGui | |||
|
6 | ||||
|
7 | # Local imports | |||
|
8 | from completion_widget import CompletionWidget | |||
|
9 | ||||
|
10 | ||||
|
11 | class AnsiCodeProcessor(object): | |||
|
12 | """ Translates ANSI escape codes into readable attributes. | |||
|
13 | """ | |||
|
14 | ||||
|
15 | def __init__(self): | |||
|
16 | self.ansi_colors = ( # Normal, Bright/Light | |||
|
17 | ('#000000', '#7f7f7f'), # 0: black | |||
|
18 | ('#cd0000', '#ff0000'), # 1: red | |||
|
19 | ('#00cd00', '#00ff00'), # 2: green | |||
|
20 | ('#cdcd00', '#ffff00'), # 3: yellow | |||
|
21 | ('#0000ee', '#0000ff'), # 4: blue | |||
|
22 | ('#cd00cd', '#ff00ff'), # 5: magenta | |||
|
23 | ('#00cdcd', '#00ffff'), # 6: cyan | |||
|
24 | ('#e5e5e5', '#ffffff')) # 7: white | |||
|
25 | self.reset() | |||
|
26 | ||||
|
27 | def set_code(self, code): | |||
|
28 | """ Set attributes based on code. | |||
|
29 | """ | |||
|
30 | if code == 0: | |||
|
31 | self.reset() | |||
|
32 | elif code == 1: | |||
|
33 | self.intensity = 1 | |||
|
34 | self.bold = True | |||
|
35 | elif code == 3: | |||
|
36 | self.italic = True | |||
|
37 | elif code == 4: | |||
|
38 | self.underline = True | |||
|
39 | elif code == 22: | |||
|
40 | self.intensity = 0 | |||
|
41 | self.bold = False | |||
|
42 | elif code == 23: | |||
|
43 | self.italic = False | |||
|
44 | elif code == 24: | |||
|
45 | self.underline = False | |||
|
46 | elif code >= 30 and code <= 37: | |||
|
47 | self.foreground_color = code - 30 | |||
|
48 | elif code == 39: | |||
|
49 | self.foreground_color = None | |||
|
50 | elif code >= 40 and code <= 47: | |||
|
51 | self.background_color = code - 40 | |||
|
52 | elif code == 49: | |||
|
53 | self.background_color = None | |||
|
54 | ||||
|
55 | def reset(self): | |||
|
56 | """ Reset attributs to their default values. | |||
|
57 | """ | |||
|
58 | self.intensity = 0 | |||
|
59 | self.italic = False | |||
|
60 | self.bold = False | |||
|
61 | self.underline = False | |||
|
62 | self.foreground_color = None | |||
|
63 | self.background_color = None | |||
|
64 | ||||
|
65 | ||||
|
66 | class QtAnsiCodeProcessor(AnsiCodeProcessor): | |||
|
67 | """ Translates ANSI escape codes into QTextCharFormats. | |||
|
68 | """ | |||
|
69 | ||||
|
70 | def get_format(self): | |||
|
71 | """ Returns a QTextCharFormat that encodes the current style attributes. | |||
|
72 | """ | |||
|
73 | format = QtGui.QTextCharFormat() | |||
|
74 | ||||
|
75 | # Set foreground color | |||
|
76 | if self.foreground_color is not None: | |||
|
77 | color = self.ansi_colors[self.foreground_color][self.intensity] | |||
|
78 | format.setForeground(QtGui.QColor(color)) | |||
|
79 | ||||
|
80 | # Set background color | |||
|
81 | if self.background_color is not None: | |||
|
82 | color = self.ansi_colors[self.background_color][self.intensity] | |||
|
83 | format.setBackground(QtGui.QColor(color)) | |||
|
84 | ||||
|
85 | # Set font weight/style options | |||
|
86 | if self.bold: | |||
|
87 | format.setFontWeight(QtGui.QFont.Bold) | |||
|
88 | else: | |||
|
89 | format.setFontWeight(QtGui.QFont.Normal) | |||
|
90 | format.setFontItalic(self.italic) | |||
|
91 | format.setFontUnderline(self.underline) | |||
|
92 | ||||
|
93 | return format | |||
|
94 | ||||
|
95 | ||||
|
96 | class ConsoleWidget(QtGui.QPlainTextEdit): | |||
|
97 | """ Base class for console-type widgets. This class is mainly concerned with | |||
|
98 | dealing with the prompt, keeping the cursor inside the editing line, and | |||
|
99 | handling ANSI escape sequences. | |||
|
100 | """ | |||
|
101 | ||||
|
102 | # Regex to match ANSI escape sequences | |||
|
103 | _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?') | |||
|
104 | ||||
|
105 | # When ctrl is pressed, map certain keys to other keys (without the ctrl): | |||
|
106 | _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, | |||
|
107 | QtCore.Qt.Key_F : QtCore.Qt.Key_Right, | |||
|
108 | QtCore.Qt.Key_A : QtCore.Qt.Key_Home, | |||
|
109 | QtCore.Qt.Key_E : QtCore.Qt.Key_End, | |||
|
110 | QtCore.Qt.Key_P : QtCore.Qt.Key_Up, | |||
|
111 | QtCore.Qt.Key_N : QtCore.Qt.Key_Down, | |||
|
112 | QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, } | |||
|
113 | ||||
|
114 | #--------------------------------------------------------------------------- | |||
|
115 | # 'QWidget' interface | |||
|
116 | #--------------------------------------------------------------------------- | |||
|
117 | ||||
|
118 | def __init__(self, parent=None): | |||
|
119 | QtGui.QPlainTextEdit.__init__(self, parent) | |||
|
120 | ||||
|
121 | # Initialize public and protected variables | |||
|
122 | self.ansi_codes = True | |||
|
123 | self.continuation_prompt = '> ' | |||
|
124 | self.gui_completion = True | |||
|
125 | self._ansi_processor = QtAnsiCodeProcessor() | |||
|
126 | self._completion_widget = CompletionWidget(self) | |||
|
127 | self._executing = False | |||
|
128 | self._prompt = '' | |||
|
129 | self._prompt_pos = 0 | |||
|
130 | self._reading = False | |||
|
131 | ||||
|
132 | # Configure some basic QPlainTextEdit settings | |||
|
133 | self.setLineWrapMode(QtGui.QPlainTextEdit.WidgetWidth) | |||
|
134 | self.setMaximumBlockCount(500) # Limit text buffer size | |||
|
135 | self.setUndoRedoEnabled(False) | |||
|
136 | ||||
|
137 | # Set a monospaced font | |||
|
138 | point_size = QtGui.QApplication.font().pointSize() | |||
|
139 | font = QtGui.QFont('Monospace', point_size) | |||
|
140 | font.setStyleHint(QtGui.QFont.TypeWriter) | |||
|
141 | self._completion_widget.setFont(font) | |||
|
142 | self.document().setDefaultFont(font) | |||
|
143 | ||||
|
144 | # Define a custom context menu | |||
|
145 | self._context_menu = QtGui.QMenu(self) | |||
|
146 | ||||
|
147 | copy_action = QtGui.QAction('Copy', self) | |||
|
148 | copy_action.triggered.connect(self.copy) | |||
|
149 | self.copyAvailable.connect(copy_action.setEnabled) | |||
|
150 | self._context_menu.addAction(copy_action) | |||
|
151 | ||||
|
152 | self._paste_action = QtGui.QAction('Paste', self) | |||
|
153 | self._paste_action.triggered.connect(self.paste) | |||
|
154 | self._context_menu.addAction(self._paste_action) | |||
|
155 | self._context_menu.addSeparator() | |||
|
156 | ||||
|
157 | select_all_action = QtGui.QAction('Select All', self) | |||
|
158 | select_all_action.triggered.connect(self.selectAll) | |||
|
159 | self._context_menu.addAction(select_all_action) | |||
|
160 | ||||
|
161 | def contextMenuEvent(self, event): | |||
|
162 | """ Reimplemented to create a menu without destructive actions like | |||
|
163 | 'Cut' and 'Delete'. | |||
|
164 | """ | |||
|
165 | clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty() | |||
|
166 | self._paste_action.setEnabled(not clipboard_empty) | |||
|
167 | ||||
|
168 | self._context_menu.exec_(event.globalPos()) | |||
|
169 | ||||
|
170 | def keyPressEvent(self, event): | |||
|
171 | """ Reimplemented to create a console-like interface. | |||
|
172 | """ | |||
|
173 | intercepted = False | |||
|
174 | cursor = self.textCursor() | |||
|
175 | position = cursor.position() | |||
|
176 | key = event.key() | |||
|
177 | ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier | |||
|
178 | alt_down = event.modifiers() & QtCore.Qt.AltModifier | |||
|
179 | shift_down = event.modifiers() & QtCore.Qt.ShiftModifier | |||
|
180 | ||||
|
181 | if ctrl_down: | |||
|
182 | if key in self._ctrl_down_remap: | |||
|
183 | ctrl_down = False | |||
|
184 | key = self._ctrl_down_remap[key] | |||
|
185 | event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key, | |||
|
186 | QtCore.Qt.NoModifier) | |||
|
187 | ||||
|
188 | elif key == QtCore.Qt.Key_K: | |||
|
189 | if self._in_buffer(position): | |||
|
190 | cursor.movePosition(QtGui.QTextCursor.EndOfLine, | |||
|
191 | QtGui.QTextCursor.KeepAnchor) | |||
|
192 | cursor.removeSelectedText() | |||
|
193 | intercepted = True | |||
|
194 | ||||
|
195 | elif key == QtCore.Qt.Key_Y: | |||
|
196 | self.paste() | |||
|
197 | intercepted = True | |||
|
198 | ||||
|
199 | elif alt_down: | |||
|
200 | if key == QtCore.Qt.Key_B: | |||
|
201 | self.setTextCursor(self._get_word_start_cursor(position)) | |||
|
202 | intercepted = True | |||
|
203 | ||||
|
204 | elif key == QtCore.Qt.Key_F: | |||
|
205 | self.setTextCursor(self._get_word_end_cursor(position)) | |||
|
206 | intercepted = True | |||
|
207 | ||||
|
208 | elif key == QtCore.Qt.Key_Backspace: | |||
|
209 | cursor = self._get_word_start_cursor(position) | |||
|
210 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) | |||
|
211 | cursor.removeSelectedText() | |||
|
212 | intercepted = True | |||
|
213 | ||||
|
214 | elif key == QtCore.Qt.Key_D: | |||
|
215 | cursor = self._get_word_end_cursor(position) | |||
|
216 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) | |||
|
217 | cursor.removeSelectedText() | |||
|
218 | intercepted = True | |||
|
219 | ||||
|
220 | if self._completion_widget.isVisible(): | |||
|
221 | self._completion_widget.keyPressEvent(event) | |||
|
222 | intercepted = event.isAccepted() | |||
|
223 | ||||
|
224 | else: | |||
|
225 | if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): | |||
|
226 | if self._reading: | |||
|
227 | self._reading = False | |||
|
228 | elif not self._executing: | |||
|
229 | self._executing = True | |||
|
230 | self.execute(interactive=True) | |||
|
231 | intercepted = True | |||
|
232 | ||||
|
233 | elif key == QtCore.Qt.Key_Up: | |||
|
234 | if self._reading or not self._up_pressed(): | |||
|
235 | intercepted = True | |||
|
236 | else: | |||
|
237 | prompt_line = self._get_prompt_cursor().blockNumber() | |||
|
238 | intercepted = cursor.blockNumber() <= prompt_line | |||
|
239 | ||||
|
240 | elif key == QtCore.Qt.Key_Down: | |||
|
241 | if self._reading or not self._down_pressed(): | |||
|
242 | intercepted = True | |||
|
243 | else: | |||
|
244 | end_line = self._get_end_cursor().blockNumber() | |||
|
245 | intercepted = cursor.blockNumber() == end_line | |||
|
246 | ||||
|
247 | elif key == QtCore.Qt.Key_Tab: | |||
|
248 | if self._reading: | |||
|
249 | intercepted = False | |||
|
250 | else: | |||
|
251 | intercepted = not self._tab_pressed() | |||
|
252 | ||||
|
253 | elif key == QtCore.Qt.Key_Left: | |||
|
254 | intercepted = not self._in_buffer(position - 1) | |||
|
255 | ||||
|
256 | elif key == QtCore.Qt.Key_Home: | |||
|
257 | cursor.movePosition(QtGui.QTextCursor.StartOfLine) | |||
|
258 | start_pos = cursor.position() | |||
|
259 | start_line = cursor.blockNumber() | |||
|
260 | if start_line == self._get_prompt_cursor().blockNumber(): | |||
|
261 | start_pos += len(self._prompt) | |||
|
262 | else: | |||
|
263 | start_pos += len(self.continuation_prompt) | |||
|
264 | if shift_down and self._in_buffer(position): | |||
|
265 | self._set_selection(position, start_pos) | |||
|
266 | else: | |||
|
267 | self._set_position(start_pos) | |||
|
268 | intercepted = True | |||
|
269 | ||||
|
270 | elif key == QtCore.Qt.Key_Backspace and not alt_down: | |||
|
271 | ||||
|
272 | # Line deletion (remove continuation prompt) | |||
|
273 | len_prompt = len(self.continuation_prompt) | |||
|
274 | if cursor.columnNumber() == len_prompt and \ | |||
|
275 | position != self._prompt_pos: | |||
|
276 | cursor.setPosition(position - len_prompt, | |||
|
277 | QtGui.QTextCursor.KeepAnchor) | |||
|
278 | cursor.removeSelectedText() | |||
|
279 | ||||
|
280 | # Regular backwards deletion | |||
|
281 | else: | |||
|
282 | anchor = cursor.anchor() | |||
|
283 | if anchor == position: | |||
|
284 | intercepted = not self._in_buffer(position - 1) | |||
|
285 | else: | |||
|
286 | intercepted = not self._in_buffer(min(anchor, position)) | |||
|
287 | ||||
|
288 | elif key == QtCore.Qt.Key_Delete: | |||
|
289 | anchor = cursor.anchor() | |||
|
290 | intercepted = not self._in_buffer(min(anchor, position)) | |||
|
291 | ||||
|
292 | # Don't move cursor if control is down to allow copy-paste using | |||
|
293 | # the keyboard in any part of the buffer | |||
|
294 | if not ctrl_down: | |||
|
295 | self._keep_cursor_in_buffer() | |||
|
296 | ||||
|
297 | if not intercepted: | |||
|
298 | QtGui.QPlainTextEdit.keyPressEvent(self, event) | |||
|
299 | ||||
|
300 | #-------------------------------------------------------------------------- | |||
|
301 | # 'QPlainTextEdit' interface | |||
|
302 | #-------------------------------------------------------------------------- | |||
|
303 | ||||
|
304 | def appendPlainText(self, text): | |||
|
305 | """ Reimplemented to not append text as a new paragraph, which doesn't | |||
|
306 | make sense for a console widget. Also, if enabled, handle ANSI | |||
|
307 | codes. | |||
|
308 | """ | |||
|
309 | cursor = self.textCursor() | |||
|
310 | cursor.movePosition(QtGui.QTextCursor.End) | |||
|
311 | ||||
|
312 | if self.ansi_codes: | |||
|
313 | format = QtGui.QTextCharFormat() | |||
|
314 | previous_end = 0 | |||
|
315 | for match in self._ansi_pattern.finditer(text): | |||
|
316 | cursor.insertText(text[previous_end:match.start()], format) | |||
|
317 | previous_end = match.end() | |||
|
318 | for code in match.group(1).split(';'): | |||
|
319 | self._ansi_processor.set_code(int(code)) | |||
|
320 | format = self._ansi_processor.get_format() | |||
|
321 | cursor.insertText(text[previous_end:], format) | |||
|
322 | else: | |||
|
323 | cursor.insertText(text) | |||
|
324 | ||||
|
325 | def paste(self): | |||
|
326 | """ Reimplemented to ensure that text is pasted in the editing region. | |||
|
327 | """ | |||
|
328 | self._keep_cursor_in_buffer() | |||
|
329 | QtGui.QPlainTextEdit.paste(self) | |||
|
330 | ||||
|
331 | #--------------------------------------------------------------------------- | |||
|
332 | # 'ConsoleWidget' public interface | |||
|
333 | #--------------------------------------------------------------------------- | |||
|
334 | ||||
|
335 | def execute(self, interactive=False): | |||
|
336 | """ Execute the text in the input buffer. Returns whether the input | |||
|
337 | buffer was completely processed and a new prompt created. | |||
|
338 | """ | |||
|
339 | self.appendPlainText('\n') | |||
|
340 | self._prompt_finished() | |||
|
341 | return self._execute(interactive=interactive) | |||
|
342 | ||||
|
343 | def _get_input_buffer(self): | |||
|
344 | cursor = self._get_end_cursor() | |||
|
345 | cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) | |||
|
346 | ||||
|
347 | # Use QTextDocumentFragment intermediate object because it strips | |||
|
348 | # out the Unicode line break characters that Qt insists on inserting. | |||
|
349 | input_buffer = str(cursor.selection().toPlainText()) | |||
|
350 | ||||
|
351 | # Strip out continuation prompts | |||
|
352 | return input_buffer.replace('\n' + self.continuation_prompt, '\n') | |||
|
353 | ||||
|
354 | def _set_input_buffer(self, string): | |||
|
355 | # Add continuation prompts where necessary | |||
|
356 | lines = string.splitlines() | |||
|
357 | for i in xrange(1, len(lines)): | |||
|
358 | lines[i] = self.continuation_prompt + lines[i] | |||
|
359 | string = '\n'.join(lines) | |||
|
360 | ||||
|
361 | # Replace buffer with new text | |||
|
362 | cursor = self._get_end_cursor() | |||
|
363 | cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) | |||
|
364 | cursor.insertText(string) | |||
|
365 | self.moveCursor(QtGui.QTextCursor.End) | |||
|
366 | ||||
|
367 | input_buffer = property(_get_input_buffer, _set_input_buffer) | |||
|
368 | ||||
|
369 | def _get_input_buffer_cursor_line(self): | |||
|
370 | cursor = self.textCursor() | |||
|
371 | if cursor.position() >= self._prompt_pos: | |||
|
372 | text = str(cursor.block().text()) | |||
|
373 | if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): | |||
|
374 | return text[len(self._prompt):] | |||
|
375 | else: | |||
|
376 | return text[len(self.continuation_prompt):] | |||
|
377 | else: | |||
|
378 | return None | |||
|
379 | ||||
|
380 | input_buffer_cursor_line = property(_get_input_buffer_cursor_line) | |||
|
381 | ||||
|
382 | #--------------------------------------------------------------------------- | |||
|
383 | # 'ConsoleWidget' abstract interface | |||
|
384 | #--------------------------------------------------------------------------- | |||
|
385 | ||||
|
386 | def _execute(self, interactive): | |||
|
387 | """ Called to execute the input buffer. When triggered by an the enter | |||
|
388 | key press, 'interactive' is True; otherwise, it is False. Returns | |||
|
389 | whether the input buffer was completely processed and a new prompt | |||
|
390 | created. | |||
|
391 | """ | |||
|
392 | raise NotImplementedError | |||
|
393 | ||||
|
394 | def _prompt_started_hook(self): | |||
|
395 | """ Called immediately after a new prompt is displayed. | |||
|
396 | """ | |||
|
397 | pass | |||
|
398 | ||||
|
399 | def _prompt_finished_hook(self): | |||
|
400 | """ Called immediately after a prompt is finished, i.e. when some input | |||
|
401 | will be processed and a new prompt displayed. | |||
|
402 | """ | |||
|
403 | pass | |||
|
404 | ||||
|
405 | def _up_pressed(self): | |||
|
406 | """ Called when the up key is pressed. Returns whether to continue | |||
|
407 | processing the event. | |||
|
408 | """ | |||
|
409 | return True | |||
|
410 | ||||
|
411 | def _down_pressed(self): | |||
|
412 | """ Called when the down key is pressed. Returns whether to continue | |||
|
413 | processing the event. | |||
|
414 | """ | |||
|
415 | return True | |||
|
416 | ||||
|
417 | def _tab_pressed(self): | |||
|
418 | """ Called when the tab key is pressed. Returns whether to continue | |||
|
419 | processing the event. | |||
|
420 | """ | |||
|
421 | return False | |||
|
422 | ||||
|
423 | #-------------------------------------------------------------------------- | |||
|
424 | # 'ConsoleWidget' protected interface | |||
|
425 | #-------------------------------------------------------------------------- | |||
|
426 | ||||
|
427 | def _complete_with_items(self, cursor, items): | |||
|
428 | """ Performs completion with 'items' at the specified cursor location. | |||
|
429 | """ | |||
|
430 | if len(items) == 1: | |||
|
431 | cursor.setPosition(self.textCursor().position(), | |||
|
432 | QtGui.QTextCursor.KeepAnchor) | |||
|
433 | cursor.insertText(items[0]) | |||
|
434 | elif len(items) > 1: | |||
|
435 | if self.gui_completion: | |||
|
436 | self._completion_widget.show_items(cursor, items) | |||
|
437 | else: | |||
|
438 | text = '\n'.join(items) + '\n' | |||
|
439 | self._write_text_keeping_prompt(text) | |||
|
440 | ||||
|
441 | def _get_end_cursor(self): | |||
|
442 | """ Convenience method that returns a cursor for the last character. | |||
|
443 | """ | |||
|
444 | cursor = self.textCursor() | |||
|
445 | cursor.movePosition(QtGui.QTextCursor.End) | |||
|
446 | return cursor | |||
|
447 | ||||
|
448 | def _get_prompt_cursor(self): | |||
|
449 | """ Convenience method that returns a cursor for the prompt position. | |||
|
450 | """ | |||
|
451 | cursor = self.textCursor() | |||
|
452 | cursor.setPosition(self._prompt_pos) | |||
|
453 | return cursor | |||
|
454 | ||||
|
455 | def _get_selection_cursor(self, start, end): | |||
|
456 | """ Convenience method that returns a cursor with text selected between | |||
|
457 | the positions 'start' and 'end'. | |||
|
458 | """ | |||
|
459 | cursor = self.textCursor() | |||
|
460 | cursor.setPosition(start) | |||
|
461 | cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor) | |||
|
462 | return cursor | |||
|
463 | ||||
|
464 | def _get_word_start_cursor(self, position): | |||
|
465 | """ Find the start of the word to the left the given position. If a | |||
|
466 | sequence of non-word characters precedes the first word, skip over | |||
|
467 | them. (This emulates the behavior of bash, emacs, etc.) | |||
|
468 | """ | |||
|
469 | document = self.document() | |||
|
470 | position -= 1 | |||
|
471 | while self._in_buffer(position) and \ | |||
|
472 | not document.characterAt(position).isLetterOrNumber(): | |||
|
473 | position -= 1 | |||
|
474 | while self._in_buffer(position) and \ | |||
|
475 | document.characterAt(position).isLetterOrNumber(): | |||
|
476 | position -= 1 | |||
|
477 | cursor = self.textCursor() | |||
|
478 | cursor.setPosition(position + 1) | |||
|
479 | return cursor | |||
|
480 | ||||
|
481 | def _get_word_end_cursor(self, position): | |||
|
482 | """ Find the end of the word to the right the given position. If a | |||
|
483 | sequence of non-word characters precedes the first word, skip over | |||
|
484 | them. (This emulates the behavior of bash, emacs, etc.) | |||
|
485 | """ | |||
|
486 | document = self.document() | |||
|
487 | end = self._get_end_cursor().position() | |||
|
488 | while position < end and \ | |||
|
489 | not document.characterAt(position).isLetterOrNumber(): | |||
|
490 | position += 1 | |||
|
491 | while position < end and \ | |||
|
492 | document.characterAt(position).isLetterOrNumber(): | |||
|
493 | position += 1 | |||
|
494 | cursor = self.textCursor() | |||
|
495 | cursor.setPosition(position) | |||
|
496 | return cursor | |||
|
497 | ||||
|
498 | def _prompt_started(self): | |||
|
499 | """ Called immediately after a new prompt is displayed. | |||
|
500 | """ | |||
|
501 | self.moveCursor(QtGui.QTextCursor.End) | |||
|
502 | self.centerCursor() | |||
|
503 | self.setReadOnly(False) | |||
|
504 | self._executing = False | |||
|
505 | self._prompt_started_hook() | |||
|
506 | ||||
|
507 | def _prompt_finished(self): | |||
|
508 | """ Called immediately after a prompt is finished, i.e. when some input | |||
|
509 | will be processed and a new prompt displayed. | |||
|
510 | """ | |||
|
511 | self.setReadOnly(True) | |||
|
512 | self._prompt_finished_hook() | |||
|
513 | ||||
|
514 | def _set_position(self, position): | |||
|
515 | """ Convenience method to set the position of the cursor. | |||
|
516 | """ | |||
|
517 | cursor = self.textCursor() | |||
|
518 | cursor.setPosition(position) | |||
|
519 | self.setTextCursor(cursor) | |||
|
520 | ||||
|
521 | def _set_selection(self, start, end): | |||
|
522 | """ Convenience method to set the current selected text. | |||
|
523 | """ | |||
|
524 | self.setTextCursor(self._get_selection_cursor(start, end)) | |||
|
525 | ||||
|
526 | def _show_prompt(self, prompt): | |||
|
527 | """ Writes a new prompt at the end of the buffer. | |||
|
528 | """ | |||
|
529 | self.appendPlainText('\n' + prompt) | |||
|
530 | self._prompt = prompt | |||
|
531 | self._prompt_pos = self._get_end_cursor().position() | |||
|
532 | self._prompt_started() | |||
|
533 | ||||
|
534 | def _show_continuation_prompt(self): | |||
|
535 | """ Writes a new continuation prompt at the end of the buffer. | |||
|
536 | """ | |||
|
537 | self.appendPlainText(self.continuation_prompt) | |||
|
538 | self._prompt_started() | |||
|
539 | ||||
|
540 | def _write_text_keeping_prompt(self, text): | |||
|
541 | """ Writes 'text' after the current prompt, then restores the old prompt | |||
|
542 | with its old input buffer. | |||
|
543 | """ | |||
|
544 | input_buffer = self.input_buffer | |||
|
545 | self.appendPlainText('\n') | |||
|
546 | self._prompt_finished() | |||
|
547 | ||||
|
548 | self.appendPlainText(text) | |||
|
549 | self._show_prompt(self._prompt) | |||
|
550 | self.input_buffer = input_buffer | |||
|
551 | ||||
|
552 | def _in_buffer(self, position): | |||
|
553 | """ Returns whether the given position is inside the editing region. | |||
|
554 | """ | |||
|
555 | return position >= self._prompt_pos | |||
|
556 | ||||
|
557 | def _keep_cursor_in_buffer(self): | |||
|
558 | """ Ensures that the cursor is inside the editing region. Returns | |||
|
559 | whether the cursor was moved. | |||
|
560 | """ | |||
|
561 | cursor = self.textCursor() | |||
|
562 | if cursor.position() < self._prompt_pos: | |||
|
563 | cursor.movePosition(QtGui.QTextCursor.End) | |||
|
564 | self.setTextCursor(cursor) | |||
|
565 | return True | |||
|
566 | else: | |||
|
567 | return False | |||
|
568 | ||||
|
569 | ||||
|
570 | class HistoryConsoleWidget(ConsoleWidget): | |||
|
571 | """ A ConsoleWidget that keeps a history of the commands that have been | |||
|
572 | executed. | |||
|
573 | """ | |||
|
574 | ||||
|
575 | #--------------------------------------------------------------------------- | |||
|
576 | # 'QWidget' interface | |||
|
577 | #--------------------------------------------------------------------------- | |||
|
578 | ||||
|
579 | def __init__(self, parent=None): | |||
|
580 | super(HistoryConsoleWidget, self).__init__(parent) | |||
|
581 | ||||
|
582 | self._history = [] | |||
|
583 | self._history_index = 0 | |||
|
584 | ||||
|
585 | #--------------------------------------------------------------------------- | |||
|
586 | # 'ConsoleWidget' public interface | |||
|
587 | #--------------------------------------------------------------------------- | |||
|
588 | ||||
|
589 | def execute(self, interactive=False): | |||
|
590 | """ Reimplemented to the store history. | |||
|
591 | """ | |||
|
592 | stripped = self.input_buffer.rstrip() | |||
|
593 | executed = super(HistoryConsoleWidget, self).execute(interactive) | |||
|
594 | if executed: | |||
|
595 | self._history.append(stripped) | |||
|
596 | self._history_index = len(self._history) | |||
|
597 | return executed | |||
|
598 | ||||
|
599 | #--------------------------------------------------------------------------- | |||
|
600 | # 'ConsoleWidget' abstract interface | |||
|
601 | #--------------------------------------------------------------------------- | |||
|
602 | ||||
|
603 | def _up_pressed(self): | |||
|
604 | """ Called when the up key is pressed. Returns whether to continue | |||
|
605 | processing the event. | |||
|
606 | """ | |||
|
607 | prompt_cursor = self._get_prompt_cursor() | |||
|
608 | if self.textCursor().blockNumber() == prompt_cursor.blockNumber(): | |||
|
609 | self.history_previous() | |||
|
610 | ||||
|
611 | # Go to the first line of prompt for seemless history scrolling. | |||
|
612 | cursor = self._get_prompt_cursor() | |||
|
613 | cursor.movePosition(QtGui.QTextCursor.EndOfLine) | |||
|
614 | self.setTextCursor(cursor) | |||
|
615 | ||||
|
616 | return False | |||
|
617 | return True | |||
|
618 | ||||
|
619 | def _down_pressed(self): | |||
|
620 | """ Called when the down key is pressed. Returns whether to continue | |||
|
621 | processing the event. | |||
|
622 | """ | |||
|
623 | end_cursor = self._get_end_cursor() | |||
|
624 | if self.textCursor().blockNumber() == end_cursor.blockNumber(): | |||
|
625 | self.history_next() | |||
|
626 | return False | |||
|
627 | return True | |||
|
628 | ||||
|
629 | #--------------------------------------------------------------------------- | |||
|
630 | # 'HistoryConsoleWidget' interface | |||
|
631 | #--------------------------------------------------------------------------- | |||
|
632 | ||||
|
633 | def history_previous(self): | |||
|
634 | """ If possible, set the input buffer to the previous item in the | |||
|
635 | history. | |||
|
636 | """ | |||
|
637 | if self._history_index > 0: | |||
|
638 | self._history_index -= 1 | |||
|
639 | self.input_buffer = self._history[self._history_index] | |||
|
640 | ||||
|
641 | def history_next(self): | |||
|
642 | """ Set the input buffer to the next item in the history, or a blank | |||
|
643 | line if there is no subsequent item. | |||
|
644 | """ | |||
|
645 | if self._history_index < len(self._history): | |||
|
646 | self._history_index += 1 | |||
|
647 | if self._history_index < len(self._history): | |||
|
648 | self.input_buffer = self._history[self._history_index] | |||
|
649 | else: | |||
|
650 | self.input_buffer = '' |
@@ -0,0 +1,383 b'' | |||||
|
1 | # Standard library imports | |||
|
2 | from codeop import CommandCompiler | |||
|
3 | from threading import Thread | |||
|
4 | import time | |||
|
5 | import types | |||
|
6 | ||||
|
7 | # System library imports | |||
|
8 | from IPython.zmq.session import Message, Session | |||
|
9 | from pygments.lexers import PythonLexer | |||
|
10 | from PyQt4 import QtCore, QtGui | |||
|
11 | import zmq | |||
|
12 | ||||
|
13 | # ETS imports | |||
|
14 | from enthought.pyface.ui.qt4.code_editor.pygments_highlighter import \ | |||
|
15 | PygmentsHighlighter | |||
|
16 | ||||
|
17 | # Local imports | |||
|
18 | from call_tip_widget import CallTipWidget | |||
|
19 | from completion_lexer import CompletionLexer | |||
|
20 | from console_widget import HistoryConsoleWidget | |||
|
21 | ||||
|
22 | ||||
|
23 | class FrontendReplyThread(Thread, QtCore.QObject): | |||
|
24 | """ A Thread that receives a reply from the kernel for the frontend. | |||
|
25 | """ | |||
|
26 | ||||
|
27 | finished = QtCore.pyqtSignal() | |||
|
28 | output_received = QtCore.pyqtSignal(Message) | |||
|
29 | reply_received = QtCore.pyqtSignal(Message) | |||
|
30 | ||||
|
31 | def __init__(self, parent): | |||
|
32 | """ Create a FrontendReplyThread for the specified frontend. | |||
|
33 | """ | |||
|
34 | assert isinstance(parent, FrontendWidget) | |||
|
35 | QtCore.QObject.__init__(self, parent) | |||
|
36 | Thread.__init__(self) | |||
|
37 | ||||
|
38 | self.sleep_time = 0.05 | |||
|
39 | ||||
|
40 | def run(self): | |||
|
41 | """ The starting point for the thread. | |||
|
42 | """ | |||
|
43 | frontend = self.parent() | |||
|
44 | while True: | |||
|
45 | rep = frontend._recv_reply() | |||
|
46 | if rep is not None: | |||
|
47 | self._recv_output() | |||
|
48 | self.reply_received.emit(rep) | |||
|
49 | break | |||
|
50 | ||||
|
51 | self._recv_output() | |||
|
52 | time.sleep(self.sleep_time) | |||
|
53 | ||||
|
54 | self.finished.emit() | |||
|
55 | ||||
|
56 | def _recv_output(self): | |||
|
57 | """ Send any output to the frontend. | |||
|
58 | """ | |||
|
59 | frontend = self.parent() | |||
|
60 | omsgs = frontend._recv_output() | |||
|
61 | for omsg in omsgs: | |||
|
62 | self.output_received.emit(omsg) | |||
|
63 | ||||
|
64 | ||||
|
65 | class FrontendHighlighter(PygmentsHighlighter): | |||
|
66 | """ A Python PygmentsHighlighter that can be turned on and off and which | |||
|
67 | knows about continuation prompts. | |||
|
68 | """ | |||
|
69 | ||||
|
70 | def __init__(self, frontend): | |||
|
71 | PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer()) | |||
|
72 | self._current_offset = 0 | |||
|
73 | self._frontend = frontend | |||
|
74 | self.highlighting_on = False | |||
|
75 | ||||
|
76 | def highlightBlock(self, qstring): | |||
|
77 | """ Highlight a block of text. Reimplemented to highlight selectively. | |||
|
78 | """ | |||
|
79 | if self.highlighting_on: | |||
|
80 | for prompt in (self._frontend._prompt, | |||
|
81 | self._frontend.continuation_prompt): | |||
|
82 | if qstring.startsWith(prompt): | |||
|
83 | qstring.remove(0, len(prompt)) | |||
|
84 | self._current_offset = len(prompt) | |||
|
85 | break | |||
|
86 | PygmentsHighlighter.highlightBlock(self, qstring) | |||
|
87 | ||||
|
88 | def setFormat(self, start, count, format): | |||
|
89 | """ Reimplemented to avoid highlighting continuation prompts. | |||
|
90 | """ | |||
|
91 | start += self._current_offset | |||
|
92 | PygmentsHighlighter.setFormat(self, start, count, format) | |||
|
93 | ||||
|
94 | ||||
|
95 | class FrontendWidget(HistoryConsoleWidget): | |||
|
96 | """ A Qt frontend for an IPython kernel. | |||
|
97 | """ | |||
|
98 | ||||
|
99 | # Emitted when an 'execute_reply' is received from the kernel. | |||
|
100 | executed = QtCore.pyqtSignal(Message) | |||
|
101 | ||||
|
102 | #--------------------------------------------------------------------------- | |||
|
103 | # 'QWidget' interface | |||
|
104 | #--------------------------------------------------------------------------- | |||
|
105 | ||||
|
106 | def __init__(self, parent=None, session=None, request_socket=None, | |||
|
107 | sub_socket=None): | |||
|
108 | super(FrontendWidget, self).__init__(parent) | |||
|
109 | self.continuation_prompt = '... ' | |||
|
110 | ||||
|
111 | self._call_tip_widget = CallTipWidget(self) | |||
|
112 | self._compile = CommandCompiler() | |||
|
113 | self._completion_lexer = CompletionLexer(PythonLexer()) | |||
|
114 | self._highlighter = FrontendHighlighter(self) | |||
|
115 | ||||
|
116 | self.session = Session() if session is None else session | |||
|
117 | self.request_socket = request_socket | |||
|
118 | self.sub_socket = sub_socket | |||
|
119 | ||||
|
120 | self.document().contentsChange.connect(self._document_contents_change) | |||
|
121 | ||||
|
122 | self._kernel_connected() # XXX | |||
|
123 | ||||
|
124 | def focusOutEvent(self, event): | |||
|
125 | """ Reimplemented to hide calltips. | |||
|
126 | """ | |||
|
127 | self._call_tip_widget.hide() | |||
|
128 | return super(FrontendWidget, self).focusOutEvent(event) | |||
|
129 | ||||
|
130 | def keyPressEvent(self, event): | |||
|
131 | """ Reimplemented to hide calltips. | |||
|
132 | """ | |||
|
133 | if event.key() == QtCore.Qt.Key_Escape: | |||
|
134 | self._call_tip_widget.hide() | |||
|
135 | return super(FrontendWidget, self).keyPressEvent(event) | |||
|
136 | ||||
|
137 | #--------------------------------------------------------------------------- | |||
|
138 | # 'ConsoleWidget' abstract interface | |||
|
139 | #--------------------------------------------------------------------------- | |||
|
140 | ||||
|
141 | def _execute(self, interactive): | |||
|
142 | """ Called to execute the input buffer. When triggered by an the enter | |||
|
143 | key press, 'interactive' is True; otherwise, it is False. Returns | |||
|
144 | whether the input buffer was completely processed and a new prompt | |||
|
145 | created. | |||
|
146 | """ | |||
|
147 | return self.execute_source(self.input_buffer, interactive=interactive) | |||
|
148 | ||||
|
149 | def _prompt_started_hook(self): | |||
|
150 | """ Called immediately after a new prompt is displayed. | |||
|
151 | """ | |||
|
152 | self._highlighter.highlighting_on = True | |||
|
153 | ||||
|
154 | def _prompt_finished_hook(self): | |||
|
155 | """ Called immediately after a prompt is finished, i.e. when some input | |||
|
156 | will be processed and a new prompt displayed. | |||
|
157 | """ | |||
|
158 | self._highlighter.highlighting_on = False | |||
|
159 | ||||
|
160 | def _tab_pressed(self): | |||
|
161 | """ Called when the tab key is pressed. Returns whether to continue | |||
|
162 | processing the event. | |||
|
163 | """ | |||
|
164 | self._keep_cursor_in_buffer() | |||
|
165 | cursor = self.textCursor() | |||
|
166 | if not self._complete(): | |||
|
167 | cursor.insertText(' ') | |||
|
168 | return False | |||
|
169 | ||||
|
170 | #--------------------------------------------------------------------------- | |||
|
171 | # 'FrontendWidget' interface | |||
|
172 | #--------------------------------------------------------------------------- | |||
|
173 | ||||
|
174 | def execute_source(self, source, hidden=False, interactive=False): | |||
|
175 | """ Execute a string containing Python code. If 'hidden', no output is | |||
|
176 | shown. Returns whether the source executed (i.e., returns True only | |||
|
177 | if no more input is necessary). | |||
|
178 | """ | |||
|
179 | try: | |||
|
180 | code = self._compile(source, symbol='single') | |||
|
181 | except (OverflowError, SyntaxError, ValueError): | |||
|
182 | # Just let IPython deal with the syntax error. | |||
|
183 | code = Exception | |||
|
184 | ||||
|
185 | # Only execute interactive multiline input if it ends with a blank line | |||
|
186 | lines = source.splitlines() | |||
|
187 | if interactive and len(lines) > 1 and lines[-1].strip() != '': | |||
|
188 | code = None | |||
|
189 | ||||
|
190 | executed = code is not None | |||
|
191 | if executed: | |||
|
192 | msg = self.session.send(self.request_socket, 'execute_request', | |||
|
193 | dict(code=source)) | |||
|
194 | thread = FrontendReplyThread(self) | |||
|
195 | if not hidden: | |||
|
196 | thread.output_received.connect(self._handle_output) | |||
|
197 | thread.reply_received.connect(self._handle_reply) | |||
|
198 | thread.finished.connect(thread.deleteLater) | |||
|
199 | thread.start() | |||
|
200 | else: | |||
|
201 | space = 0 | |||
|
202 | for char in lines[-1]: | |||
|
203 | if char == '\t': | |||
|
204 | space += 4 | |||
|
205 | elif char == ' ': | |||
|
206 | space += 1 | |||
|
207 | else: | |||
|
208 | break | |||
|
209 | if source.endswith(':') or source.endswith(':\n'): | |||
|
210 | space += 4 | |||
|
211 | self._show_continuation_prompt() | |||
|
212 | self.appendPlainText(' ' * space) | |||
|
213 | ||||
|
214 | return executed | |||
|
215 | ||||
|
216 | def execute_file(self, path, hidden=False): | |||
|
217 | """ Attempts to execute file with 'path'. If 'hidden', no output is | |||
|
218 | shown. | |||
|
219 | """ | |||
|
220 | self.execute_source('run %s' % path, hidden=hidden) | |||
|
221 | ||||
|
222 | #--------------------------------------------------------------------------- | |||
|
223 | # 'FrontendWidget' protected interface | |||
|
224 | #--------------------------------------------------------------------------- | |||
|
225 | ||||
|
226 | def _call_tip(self): | |||
|
227 | """ Shows a call tip, if appropriate, at the current cursor location. | |||
|
228 | """ | |||
|
229 | # Decide if it makes sense to show a call tip | |||
|
230 | cursor = self.textCursor() | |||
|
231 | cursor.movePosition(QtGui.QTextCursor.Left) | |||
|
232 | document = self.document() | |||
|
233 | if document.characterAt(cursor.position()).toAscii() != '(': | |||
|
234 | return False | |||
|
235 | context = self._get_context(cursor) | |||
|
236 | if not context: | |||
|
237 | return False | |||
|
238 | ||||
|
239 | # Send the metadata request to the kernel | |||
|
240 | text = '.'.join(context) | |||
|
241 | msg = self.session.send(self.request_socket, 'metadata_request', | |||
|
242 | dict(context=text)) | |||
|
243 | ||||
|
244 | # Give the kernel some time to respond | |||
|
245 | rep = self._recv_reply_now('metadata_reply') | |||
|
246 | doc = rep.content.docstring if rep else '' | |||
|
247 | ||||
|
248 | # Show the call tip | |||
|
249 | if doc: | |||
|
250 | self._call_tip_widget.show_tip(doc) | |||
|
251 | return True | |||
|
252 | ||||
|
253 | def _complete(self): | |||
|
254 | """ Performs completion at the current cursor location. | |||
|
255 | """ | |||
|
256 | # Decide if it makes sense to do completion | |||
|
257 | context = self._get_context() | |||
|
258 | if not context: | |||
|
259 | return False | |||
|
260 | ||||
|
261 | # Send the completion request to the kernel | |||
|
262 | text = '.'.join(context) | |||
|
263 | line = self.input_buffer_cursor_line | |||
|
264 | msg = self.session.send(self.request_socket, 'complete_request', | |||
|
265 | dict(text=text, line=line)) | |||
|
266 | ||||
|
267 | # Give the kernel some time to respond | |||
|
268 | rep = self._recv_reply_now('complete_reply') | |||
|
269 | matches = rep.content.matches if rep else [] | |||
|
270 | ||||
|
271 | # Show the completion at the correct location | |||
|
272 | cursor = self.textCursor() | |||
|
273 | cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) | |||
|
274 | self._complete_with_items(cursor, matches) | |||
|
275 | return True | |||
|
276 | ||||
|
277 | def _kernel_connected(self): | |||
|
278 | """ Called when the frontend is connected to a kernel. | |||
|
279 | """ | |||
|
280 | self._show_prompt('>>> ') | |||
|
281 | ||||
|
282 | def _get_context(self, cursor=None): | |||
|
283 | """ Gets the context at the current cursor location. | |||
|
284 | """ | |||
|
285 | if cursor is None: | |||
|
286 | cursor = self.textCursor() | |||
|
287 | cursor.movePosition(QtGui.QTextCursor.StartOfLine, | |||
|
288 | QtGui.QTextCursor.KeepAnchor) | |||
|
289 | text = unicode(cursor.selectedText()) | |||
|
290 | return self._completion_lexer.get_context(text) | |||
|
291 | ||||
|
292 | #------ Signal handlers ---------------------------------------------------- | |||
|
293 | ||||
|
294 | def _document_contents_change(self, position, removed, added): | |||
|
295 | """ Called whenever the document's content changes. Display a calltip | |||
|
296 | if appropriate. | |||
|
297 | """ | |||
|
298 | # Calculate where the cursor should be *after* the change: | |||
|
299 | position += added | |||
|
300 | ||||
|
301 | document = self.document() | |||
|
302 | if position == self.textCursor().position(): | |||
|
303 | self._call_tip() | |||
|
304 | ||||
|
305 | def _handle_output(self, omsg): | |||
|
306 | handler = getattr(self, '_handle_%s' % omsg.msg_type, None) | |||
|
307 | if handler is not None: | |||
|
308 | handler(omsg) | |||
|
309 | ||||
|
310 | def _handle_pyout(self, omsg): | |||
|
311 | if omsg.parent_header.session == self.session.session: | |||
|
312 | self.appendPlainText(omsg.content.data + '\n') | |||
|
313 | ||||
|
314 | def _handle_stream(self, omsg): | |||
|
315 | self.appendPlainText(omsg.content.data) | |||
|
316 | ||||
|
317 | def _handle_reply(self, rep): | |||
|
318 | if rep is not None: | |||
|
319 | if rep.msg_type == 'execute_reply': | |||
|
320 | if rep.content.status == 'error': | |||
|
321 | self.appendPlainText(rep.content.traceback[-1]) | |||
|
322 | elif rep.content.status == 'aborted': | |||
|
323 | text = "ERROR: ABORTED\n" | |||
|
324 | ab = self.messages[rep.parent_header.msg_id].content | |||
|
325 | if 'code' in ab: | |||
|
326 | text += ab.code | |||
|
327 | else: | |||
|
328 | text += ab | |||
|
329 | self.appendPlainText(text) | |||
|
330 | self._show_prompt('>>> ') | |||
|
331 | self.executed.emit(rep) | |||
|
332 | ||||
|
333 | #------ Communication methods ---------------------------------------------- | |||
|
334 | ||||
|
335 | def _recv_output(self): | |||
|
336 | omsgs = [] | |||
|
337 | while True: | |||
|
338 | omsg = self.session.recv(self.sub_socket) | |||
|
339 | if omsg is None: | |||
|
340 | break | |||
|
341 | else: | |||
|
342 | omsgs.append(omsg) | |||
|
343 | return omsgs | |||
|
344 | ||||
|
345 | def _recv_reply(self): | |||
|
346 | return self.session.recv(self.request_socket) | |||
|
347 | ||||
|
348 | def _recv_reply_now(self, msg_type): | |||
|
349 | for i in xrange(5): | |||
|
350 | rep = self._recv_reply() | |||
|
351 | if rep is not None and rep.msg_type == msg_type: | |||
|
352 | return rep | |||
|
353 | time.sleep(0.1) | |||
|
354 | return None | |||
|
355 | ||||
|
356 | ||||
|
357 | if __name__ == '__main__': | |||
|
358 | import sys | |||
|
359 | ||||
|
360 | # Defaults | |||
|
361 | ip = '127.0.0.1' | |||
|
362 | port_base = 5555 | |||
|
363 | connection = ('tcp://%s' % ip) + ':%i' | |||
|
364 | req_conn = connection % port_base | |||
|
365 | sub_conn = connection % (port_base+1) | |||
|
366 | ||||
|
367 | # Create initial sockets | |||
|
368 | c = zmq.Context() | |||
|
369 | request_socket = c.socket(zmq.XREQ) | |||
|
370 | request_socket.connect(req_conn) | |||
|
371 | sub_socket = c.socket(zmq.SUB) | |||
|
372 | sub_socket.connect(sub_conn) | |||
|
373 | sub_socket.setsockopt(zmq.SUBSCRIBE, '') | |||
|
374 | ||||
|
375 | # Launch application | |||
|
376 | app = QtGui.QApplication(sys.argv) | |||
|
377 | widget = FrontendWidget(request_socket=request_socket, | |||
|
378 | sub_socket=sub_socket) | |||
|
379 | widget.setWindowTitle('Python') | |||
|
380 | widget.resize(640, 480) | |||
|
381 | widget.show() | |||
|
382 | sys.exit(app.exec_()) | |||
|
383 |
@@ -0,0 +1,221 b'' | |||||
|
1 | #------------------------------------------------------------------------------ | |||
|
2 | # Copyright (c) 2010, Enthought Inc | |||
|
3 | # All rights reserved. | |||
|
4 | # | |||
|
5 | # This software is provided without warranty under the terms of the BSD license. | |||
|
6 | ||||
|
7 | # | |||
|
8 | # Author: Enthought Inc | |||
|
9 | # Description: <Enthought pyface code editor> | |||
|
10 | #------------------------------------------------------------------------------ | |||
|
11 | ||||
|
12 | from PyQt4 import QtGui | |||
|
13 | ||||
|
14 | from pygments.lexer import RegexLexer, _TokenType, Text, Error | |||
|
15 | from pygments.lexers import CLexer, CppLexer, PythonLexer | |||
|
16 | from pygments.styles.default import DefaultStyle | |||
|
17 | from pygments.token import Comment | |||
|
18 | ||||
|
19 | ||||
|
20 | def get_tokens_unprocessed(self, text, stack=('root',)): | |||
|
21 | """ Split ``text`` into (tokentype, text) pairs. | |||
|
22 | ||||
|
23 | Monkeypatched to store the final stack on the object itself. | |||
|
24 | """ | |||
|
25 | pos = 0 | |||
|
26 | tokendefs = self._tokens | |||
|
27 | if hasattr(self, '_epd_state_stack'): | |||
|
28 | statestack = list(self._epd_state_stack) | |||
|
29 | else: | |||
|
30 | statestack = list(stack) | |||
|
31 | statetokens = tokendefs[statestack[-1]] | |||
|
32 | while 1: | |||
|
33 | for rexmatch, action, new_state in statetokens: | |||
|
34 | m = rexmatch(text, pos) | |||
|
35 | if m: | |||
|
36 | if type(action) is _TokenType: | |||
|
37 | yield pos, action, m.group() | |||
|
38 | else: | |||
|
39 | for item in action(self, m): | |||
|
40 | yield item | |||
|
41 | pos = m.end() | |||
|
42 | if new_state is not None: | |||
|
43 | # state transition | |||
|
44 | if isinstance(new_state, tuple): | |||
|
45 | for state in new_state: | |||
|
46 | if state == '#pop': | |||
|
47 | statestack.pop() | |||
|
48 | elif state == '#push': | |||
|
49 | statestack.append(statestack[-1]) | |||
|
50 | else: | |||
|
51 | statestack.append(state) | |||
|
52 | elif isinstance(new_state, int): | |||
|
53 | # pop | |||
|
54 | del statestack[new_state:] | |||
|
55 | elif new_state == '#push': | |||
|
56 | statestack.append(statestack[-1]) | |||
|
57 | else: | |||
|
58 | assert False, "wrong state def: %r" % new_state | |||
|
59 | statetokens = tokendefs[statestack[-1]] | |||
|
60 | break | |||
|
61 | else: | |||
|
62 | try: | |||
|
63 | if text[pos] == '\n': | |||
|
64 | # at EOL, reset state to "root" | |||
|
65 | pos += 1 | |||
|
66 | statestack = ['root'] | |||
|
67 | statetokens = tokendefs['root'] | |||
|
68 | yield pos, Text, u'\n' | |||
|
69 | continue | |||
|
70 | yield pos, Error, text[pos] | |||
|
71 | pos += 1 | |||
|
72 | except IndexError: | |||
|
73 | break | |||
|
74 | self._epd_state_stack = list(statestack) | |||
|
75 | ||||
|
76 | # Monkeypatch! | |||
|
77 | RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed | |||
|
78 | ||||
|
79 | ||||
|
80 | # Even with the above monkey patch to store state, multiline comments do not | |||
|
81 | # work since they are stateless (Pygments uses a single multiline regex for | |||
|
82 | # these comments, but Qt lexes by line). So we need to add a state for comments | |||
|
83 | # to the C and C++ lexers. This means that nested multiline comments will appear | |||
|
84 | # to be valid C/C++, but this is better than the alternative for now. | |||
|
85 | ||||
|
86 | def replace_pattern(tokens, new_pattern): | |||
|
87 | """ Given a RegexLexer token dictionary 'tokens', replace all patterns that | |||
|
88 | match the token specified in 'new_pattern' with 'new_pattern'. | |||
|
89 | """ | |||
|
90 | for state in tokens.values(): | |||
|
91 | for index, pattern in enumerate(state): | |||
|
92 | if isinstance(pattern, tuple) and pattern[1] == new_pattern[1]: | |||
|
93 | state[index] = new_pattern | |||
|
94 | ||||
|
95 | # More monkeypatching! | |||
|
96 | comment_start = (r'/\*', Comment.Multiline, 'comment') | |||
|
97 | comment_state = [ (r'[^*/]', Comment.Multiline), | |||
|
98 | (r'/\*', Comment.Multiline, '#push'), | |||
|
99 | (r'\*/', Comment.Multiline, '#pop'), | |||
|
100 | (r'[*/]', Comment.Multiline) ] | |||
|
101 | replace_pattern(CLexer.tokens, comment_start) | |||
|
102 | replace_pattern(CppLexer.tokens, comment_start) | |||
|
103 | CLexer.tokens['comment'] = comment_state | |||
|
104 | CppLexer.tokens['comment'] = comment_state | |||
|
105 | ||||
|
106 | ||||
|
107 | class BlockUserData(QtGui.QTextBlockUserData): | |||
|
108 | """ Storage for the user data associated with each line. | |||
|
109 | """ | |||
|
110 | ||||
|
111 | syntax_stack = ('root',) | |||
|
112 | ||||
|
113 | def __init__(self, **kwds): | |||
|
114 | for key, value in kwds.iteritems(): | |||
|
115 | setattr(self, key, value) | |||
|
116 | QtGui.QTextBlockUserData.__init__(self) | |||
|
117 | ||||
|
118 | def __repr__(self): | |||
|
119 | attrs = ['syntax_stack'] | |||
|
120 | kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr)) | |||
|
121 | for attr in attrs ]) | |||
|
122 | return 'BlockUserData(%s)' % kwds | |||
|
123 | ||||
|
124 | ||||
|
125 | class PygmentsHighlighter(QtGui.QSyntaxHighlighter): | |||
|
126 | """ Syntax highlighter that uses Pygments for parsing. """ | |||
|
127 | ||||
|
128 | def __init__(self, parent, lexer=None): | |||
|
129 | super(PygmentsHighlighter, self).__init__(parent) | |||
|
130 | ||||
|
131 | self._lexer = lexer if lexer else PythonLexer() | |||
|
132 | self._style = DefaultStyle | |||
|
133 | # Caches for formats and brushes. | |||
|
134 | self._brushes = {} | |||
|
135 | self._formats = {} | |||
|
136 | ||||
|
137 | def highlightBlock(self, qstring): | |||
|
138 | """ Highlight a block of text. | |||
|
139 | """ | |||
|
140 | qstring = unicode(qstring) | |||
|
141 | prev_data = self.previous_block_data() | |||
|
142 | ||||
|
143 | if prev_data is not None: | |||
|
144 | self._lexer._epd_state_stack = prev_data.syntax_stack | |||
|
145 | elif hasattr(self._lexer, '_epd_state_stack'): | |||
|
146 | del self._lexer._epd_state_stack | |||
|
147 | ||||
|
148 | index = 0 | |||
|
149 | # Lex the text using Pygments | |||
|
150 | for token, text in self._lexer.get_tokens(qstring): | |||
|
151 | l = len(text) | |||
|
152 | format = self._get_format(token) | |||
|
153 | if format is not None: | |||
|
154 | self.setFormat(index, l, format) | |||
|
155 | index += l | |||
|
156 | ||||
|
157 | if hasattr(self._lexer, '_epd_state_stack'): | |||
|
158 | data = BlockUserData(syntax_stack=self._lexer._epd_state_stack) | |||
|
159 | self.currentBlock().setUserData(data) | |||
|
160 | # Clean up for the next go-round. | |||
|
161 | del self._lexer._epd_state_stack | |||
|
162 | ||||
|
163 | def previous_block_data(self): | |||
|
164 | """ Convenience method for returning the previous block's user data. | |||
|
165 | """ | |||
|
166 | return self.currentBlock().previous().userData() | |||
|
167 | ||||
|
168 | def _get_format(self, token): | |||
|
169 | """ Returns a QTextCharFormat for token or None. | |||
|
170 | """ | |||
|
171 | if token in self._formats: | |||
|
172 | return self._formats[token] | |||
|
173 | result = None | |||
|
174 | for key, value in self._style.style_for_token(token) .items(): | |||
|
175 | if value: | |||
|
176 | if result is None: | |||
|
177 | result = QtGui.QTextCharFormat() | |||
|
178 | if key == 'color': | |||
|
179 | result.setForeground(self._get_brush(value)) | |||
|
180 | elif key == 'bgcolor': | |||
|
181 | result.setBackground(self._get_brush(value)) | |||
|
182 | elif key == 'bold': | |||
|
183 | result.setFontWeight(QtGui.QFont.Bold) | |||
|
184 | elif key == 'italic': | |||
|
185 | result.setFontItalic(True) | |||
|
186 | elif key == 'underline': | |||
|
187 | result.setUnderlineStyle( | |||
|
188 | QtGui.QTextCharFormat.SingleUnderline) | |||
|
189 | elif key == 'sans': | |||
|
190 | result.setFontStyleHint(QtGui.QFont.SansSerif) | |||
|
191 | elif key == 'roman': | |||
|
192 | result.setFontStyleHint(QtGui.QFont.Times) | |||
|
193 | elif key == 'mono': | |||
|
194 | result.setFontStyleHint(QtGui.QFont.TypeWriter) | |||
|
195 | elif key == 'border': | |||
|
196 | # Borders are normally used for errors. We can't do a border | |||
|
197 | # so instead we do a wavy underline | |||
|
198 | result.setUnderlineStyle( | |||
|
199 | QtGui.QTextCharFormat.WaveUnderline) | |||
|
200 | result.setUnderlineColor(self._get_color(value)) | |||
|
201 | self._formats[token] = result | |||
|
202 | return result | |||
|
203 | ||||
|
204 | def _get_brush(self, color): | |||
|
205 | """ Returns a brush for the color. | |||
|
206 | """ | |||
|
207 | result = self._brushes.get(color) | |||
|
208 | if result is None: | |||
|
209 | qcolor = self._get_color(color) | |||
|
210 | result = QtGui.QBrush(qcolor) | |||
|
211 | self._brushes[color] = result | |||
|
212 | ||||
|
213 | return result | |||
|
214 | ||||
|
215 | def _get_color(self, color): | |||
|
216 | qcolor = QtGui.QColor() | |||
|
217 | qcolor.setRgb(int(color[:2],base=16), | |||
|
218 | int(color[2:4], base=16), | |||
|
219 | int(color[4:6], base=16)) | |||
|
220 | return qcolor | |||
|
221 |
General Comments 0
You need to be logged in to leave comments.
Login now