##// END OF EJS Templates
Paved the way for PySide support....
Evan Patterson -
Show More
@@ -0,0 +1,22 b''
1 """ A Qt API selector that can be used to switch between PyQt and PySide.
2 """
3
4 import os
5
6 # Use PyQt by default until PySide is stable.
7 qt_api = os.environ.get('QT_API', 'pyqt')
8
9 if qt_api == 'pyqt':
10 # For PySide compatibility, use the new string API that automatically
11 # converts QStrings to unicode Python strings.
12 import sip
13 sip.setapi('QString', 2)
14
15 from PyQt4 import QtCore, QtGui, QtSvg
16
17 # Alias PyQt-specific functions for PySide compatibility.
18 QtCore.Signal = QtCore.pyqtSignal
19 QtCore.Slot = QtCore.pyqtSlot
20
21 else:
22 from PySide import QtCore, QtGui, QtSvg
@@ -1,233 +1,233 b''
1 1 """ Utilities for processing ANSI escape codes and special ASCII characters.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 from collections import namedtuple
9 9 import re
10 10
11 11 # System library imports
12 from PyQt4 import QtCore, QtGui
12 from IPython.external.qt import QtCore, QtGui
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Constants and datatypes
16 16 #-----------------------------------------------------------------------------
17 17
18 18 # An action for erase requests (ED and EL commands).
19 19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
20 20
21 21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
22 22 # and HVP commands).
23 23 # FIXME: Not implemented in AnsiCodeProcessor.
24 24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
25 25
26 26 # An action for scroll requests (SU and ST) and form feeds.
27 27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Classes
31 31 #-----------------------------------------------------------------------------
32 32
33 33 class AnsiCodeProcessor(object):
34 34 """ Translates special ASCII characters and ANSI escape codes into readable
35 35 attributes.
36 36 """
37 37
38 38 # Whether to increase intensity or set boldness for SGR code 1.
39 39 # (Different terminals handle this in different ways.)
40 40 bold_text_enabled = False
41 41
42 42 # Protected class variables.
43 43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
44 44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
45 45 _special_pattern = re.compile('([\f])')
46 46
47 47 #---------------------------------------------------------------------------
48 48 # AnsiCodeProcessor interface
49 49 #---------------------------------------------------------------------------
50 50
51 51 def __init__(self):
52 52 self.actions = []
53 53 self.reset_sgr()
54 54
55 55 def reset_sgr(self):
56 56 """ Reset graphics attributs to their default values.
57 57 """
58 58 self.intensity = 0
59 59 self.italic = False
60 60 self.bold = False
61 61 self.underline = False
62 62 self.foreground_color = None
63 63 self.background_color = None
64 64
65 65 def split_string(self, string):
66 66 """ Yields substrings for which the same escape code applies.
67 67 """
68 68 self.actions = []
69 69 start = 0
70 70
71 71 for match in self._ansi_pattern.finditer(string):
72 72 raw = string[start:match.start()]
73 73 substring = self._special_pattern.sub(self._replace_special, raw)
74 74 if substring or self.actions:
75 75 yield substring
76 76 start = match.end()
77 77
78 78 self.actions = []
79 79 try:
80 80 params = []
81 81 for param in match.group(1).split(';'):
82 82 if param:
83 83 params.append(int(param))
84 84 except ValueError:
85 85 # Silently discard badly formed escape codes.
86 86 pass
87 87 else:
88 88 self.set_csi_code(match.group(2), params)
89 89
90 90 raw = string[start:]
91 91 substring = self._special_pattern.sub(self._replace_special, raw)
92 92 if substring or self.actions:
93 93 yield substring
94 94
95 95 def set_csi_code(self, command, params=[]):
96 96 """ Set attributes based on CSI (Control Sequence Introducer) code.
97 97
98 98 Parameters
99 99 ----------
100 100 command : str
101 101 The code identifier, i.e. the final character in the sequence.
102 102
103 103 params : sequence of integers, optional
104 104 The parameter codes for the command.
105 105 """
106 106 if command == 'm': # SGR - Select Graphic Rendition
107 107 if params:
108 108 for code in params:
109 109 self.set_sgr_code(code)
110 110 else:
111 111 self.set_sgr_code(0)
112 112
113 113 elif (command == 'J' or # ED - Erase Data
114 114 command == 'K'): # EL - Erase in Line
115 115 code = params[0] if params else 0
116 116 if 0 <= code <= 2:
117 117 area = 'screen' if command == 'J' else 'line'
118 118 if code == 0:
119 119 erase_to = 'end'
120 120 elif code == 1:
121 121 erase_to = 'start'
122 122 elif code == 2:
123 123 erase_to = 'all'
124 124 self.actions.append(EraseAction('erase', area, erase_to))
125 125
126 126 elif (command == 'S' or # SU - Scroll Up
127 127 command == 'T'): # SD - Scroll Down
128 128 dir = 'up' if command == 'S' else 'down'
129 129 count = params[0] if params else 1
130 130 self.actions.append(ScrollAction('scroll', dir, 'line', count))
131 131
132 132 def set_sgr_code(self, code):
133 133 """ Set attributes based on SGR (Select Graphic Rendition) code.
134 134 """
135 135 if code == 0:
136 136 self.reset_sgr()
137 137 elif code == 1:
138 138 if self.bold_text_enabled:
139 139 self.bold = True
140 140 else:
141 141 self.intensity = 1
142 142 elif code == 2:
143 143 self.intensity = 0
144 144 elif code == 3:
145 145 self.italic = True
146 146 elif code == 4:
147 147 self.underline = True
148 148 elif code == 22:
149 149 self.intensity = 0
150 150 self.bold = False
151 151 elif code == 23:
152 152 self.italic = False
153 153 elif code == 24:
154 154 self.underline = False
155 155 elif code >= 30 and code <= 37:
156 156 self.foreground_color = code - 30
157 157 elif code == 39:
158 158 self.foreground_color = None
159 159 elif code >= 40 and code <= 47:
160 160 self.background_color = code - 40
161 161 elif code == 49:
162 162 self.background_color = None
163 163
164 164 #---------------------------------------------------------------------------
165 165 # Protected interface
166 166 #---------------------------------------------------------------------------
167 167
168 168 def _replace_special(self, match):
169 169 special = match.group(1)
170 170 if special == '\f':
171 171 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
172 172 return ''
173 173
174 174
175 175 class QtAnsiCodeProcessor(AnsiCodeProcessor):
176 176 """ Translates ANSI escape codes into QTextCharFormats.
177 177 """
178 178
179 179 # A map from color codes to RGB colors.
180 180 default_map = (# Normal, Bright/Light ANSI color code
181 181 ('black', 'grey'), # 0: black
182 182 ('darkred', 'red'), # 1: red
183 183 ('darkgreen', 'lime'), # 2: green
184 184 ('brown', 'yellow'), # 3: yellow
185 185 ('darkblue', 'deepskyblue'), # 4: blue
186 186 ('darkviolet', 'magenta'), # 5: magenta
187 187 ('steelblue', 'cyan'), # 6: cyan
188 188 ('grey', 'white')) # 7: white
189 189
190 190 def __init__(self):
191 191 super(QtAnsiCodeProcessor, self).__init__()
192 192 self.color_map = self.default_map
193 193
194 194 def get_format(self):
195 195 """ Returns a QTextCharFormat that encodes the current style attributes.
196 196 """
197 197 format = QtGui.QTextCharFormat()
198 198
199 199 # Set foreground color
200 200 if self.foreground_color is not None:
201 201 color = self.color_map[self.foreground_color][self.intensity]
202 202 format.setForeground(QtGui.QColor(color))
203 203
204 204 # Set background color
205 205 if self.background_color is not None:
206 206 color = self.color_map[self.background_color][self.intensity]
207 207 format.setBackground(QtGui.QColor(color))
208 208
209 209 # Set font weight/style options
210 210 if self.bold:
211 211 format.setFontWeight(QtGui.QFont.Bold)
212 212 else:
213 213 format.setFontWeight(QtGui.QFont.Normal)
214 214 format.setFontItalic(self.italic)
215 215 format.setFontUnderline(self.underline)
216 216
217 217 return format
218 218
219 219 def set_background_color(self, color):
220 220 """ Given a background color (a QColor), attempt to set a color map
221 221 that will be aesthetically pleasing.
222 222 """
223 223 if color.value() < 127:
224 224 # Colors appropriate for a terminal with a dark background.
225 225 self.color_map = self.default_map
226 226
227 227 else:
228 228 # Colors appropriate for a terminal with a light background. For
229 229 # now, only use non-bright colors...
230 230 self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]
231 231
232 232 # ...and replace white with black.
233 233 self.color_map[7] = ('black', 'black')
@@ -1,101 +1,100 b''
1 1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
2 2 """
3 3
4 4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from IPython.external.qt import QtCore, QtGui
6 6
7 7
8 8 class BracketMatcher(QtCore.QObject):
9 9 """ Matches square brackets, braces, and parentheses based on cursor
10 10 position.
11 11 """
12 12
13 13 # Protected class variables.
14 14 _opening_map = { '(':')', '{':'}', '[':']' }
15 15 _closing_map = { ')':'(', '}':'{', ']':'[' }
16 16
17 17 #--------------------------------------------------------------------------
18 18 # 'QObject' interface
19 19 #--------------------------------------------------------------------------
20 20
21 21 def __init__(self, text_edit):
22 22 """ Create a call tip manager that is attached to the specified Qt
23 23 text edit widget.
24 24 """
25 25 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
26 26 super(BracketMatcher, self).__init__()
27 27
28 28 # The format to apply to matching brackets.
29 29 self.format = QtGui.QTextCharFormat()
30 30 self.format.setBackground(QtGui.QColor('silver'))
31 31
32 32 self._text_edit = text_edit
33 33 text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
34 34
35 35 #--------------------------------------------------------------------------
36 36 # Protected interface
37 37 #--------------------------------------------------------------------------
38 38
39 39 def _find_match(self, position):
40 40 """ Given a valid position in the text document, try to find the
41 41 position of the matching bracket. Returns -1 if unsuccessful.
42 42 """
43 43 # Decide what character to search for and what direction to search in.
44 44 document = self._text_edit.document()
45 qchar = document.characterAt(position)
46 start_char = qchar.toAscii()
45 start_char = document.characterAt(position)
47 46 search_char = self._opening_map.get(start_char)
48 47 if search_char:
49 48 increment = 1
50 49 else:
51 50 search_char = self._closing_map.get(start_char)
52 51 if search_char:
53 52 increment = -1
54 53 else:
55 54 return -1
56 55
57 56 # Search for the character.
57 char = start_char
58 58 depth = 0
59 59 while position >= 0 and position < document.characterCount():
60 char = qchar.toAscii()
61 60 if char == start_char:
62 61 depth += 1
63 62 elif char == search_char:
64 63 depth -= 1
65 64 if depth == 0:
66 65 break
67 66 position += increment
68 qchar = document.characterAt(position)
67 char = document.characterAt(position)
69 68 else:
70 69 position = -1
71 70 return position
72 71
73 72 def _selection_for_character(self, position):
74 73 """ Convenience method for selecting a character.
75 74 """
76 75 selection = QtGui.QTextEdit.ExtraSelection()
77 76 cursor = self._text_edit.textCursor()
78 77 cursor.setPosition(position)
79 78 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
80 79 QtGui.QTextCursor.KeepAnchor)
81 80 selection.cursor = cursor
82 81 selection.format = self.format
83 82 return selection
84 83
85 84 #------ Signal handlers ----------------------------------------------------
86 85
87 86 def _cursor_position_changed(self):
88 87 """ Updates the document formatting based on the new cursor position.
89 88 """
90 89 # Clear out the old formatting.
91 90 self._text_edit.setExtraSelections([])
92 91
93 92 # Attempt to match a bracket for the new cursor position.
94 93 cursor = self._text_edit.textCursor()
95 94 if not cursor.hasSelection():
96 95 position = cursor.position() - 1
97 96 match_position = self._find_match(position)
98 97 if match_position != -1:
99 98 extra_selections = [ self._selection_for_character(pos)
100 99 for pos in (position, match_position) ]
101 100 self._text_edit.setExtraSelections(extra_selections)
@@ -1,225 +1,225 b''
1 1 # Standard library imports
2 2 import re
3 3 from textwrap import dedent
4 from unicodedata import category
4 5
5 6 # System library imports
6 from PyQt4 import QtCore, QtGui
7 from IPython.external.qt import QtCore, QtGui
7 8
8 9
9 10 class CallTipWidget(QtGui.QLabel):
10 11 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
11 12 """
12 13
13 14 #--------------------------------------------------------------------------
14 15 # 'QObject' interface
15 16 #--------------------------------------------------------------------------
16 17
17 18 def __init__(self, text_edit):
18 19 """ Create a call tip manager that is attached to the specified Qt
19 20 text edit widget.
20 21 """
21 22 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 23 super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
23 24
24 25 self._hide_timer = QtCore.QBasicTimer()
25 26 self._text_edit = text_edit
26 27
27 28 self.setFont(text_edit.document().defaultFont())
28 29 self.setForegroundRole(QtGui.QPalette.ToolTipText)
29 30 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
30 31 self.setPalette(QtGui.QToolTip.palette())
31 32
32 33 self.setAlignment(QtCore.Qt.AlignLeft)
33 34 self.setIndent(1)
34 35 self.setFrameStyle(QtGui.QFrame.NoFrame)
35 36 self.setMargin(1 + self.style().pixelMetric(
36 37 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
37 38 self.setWindowOpacity(self.style().styleHint(
38 39 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
39 40
40 41 def eventFilter(self, obj, event):
41 42 """ Reimplemented to hide on certain key presses and on text edit focus
42 43 changes.
43 44 """
44 45 if obj == self._text_edit:
45 46 etype = event.type()
46 47
47 48 if etype == QtCore.QEvent.KeyPress:
48 49 key = event.key()
49 50 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
50 51 self.hide()
51 52 elif key == QtCore.Qt.Key_Escape:
52 53 self.hide()
53 54 return True
54 55
55 56 elif etype == QtCore.QEvent.FocusOut:
56 57 self.hide()
57 58
58 59 elif etype == QtCore.QEvent.Enter:
59 60 self._hide_timer.stop()
60 61
61 62 elif etype == QtCore.QEvent.Leave:
62 63 self._hide_later()
63 64
64 65 return super(CallTipWidget, self).eventFilter(obj, event)
65 66
66 67 def timerEvent(self, event):
67 68 """ Reimplemented to hide the widget when the hide timer fires.
68 69 """
69 70 if event.timerId() == self._hide_timer.timerId():
70 71 self._hide_timer.stop()
71 72 self.hide()
72 73
73 74 #--------------------------------------------------------------------------
74 75 # 'QWidget' interface
75 76 #--------------------------------------------------------------------------
76 77
77 78 def enterEvent(self, event):
78 79 """ Reimplemented to cancel the hide timer.
79 80 """
80 81 super(CallTipWidget, self).enterEvent(event)
81 82 self._hide_timer.stop()
82 83
83 84 def hideEvent(self, event):
84 85 """ Reimplemented to disconnect signal handlers and event filter.
85 86 """
86 87 super(CallTipWidget, self).hideEvent(event)
87 88 self._text_edit.cursorPositionChanged.disconnect(
88 89 self._cursor_position_changed)
89 90 self._text_edit.removeEventFilter(self)
90 91
91 92 def leaveEvent(self, event):
92 93 """ Reimplemented to start the hide timer.
93 94 """
94 95 super(CallTipWidget, self).leaveEvent(event)
95 96 self._hide_later()
96 97
97 98 def paintEvent(self, event):
98 99 """ Reimplemented to paint the background panel.
99 100 """
100 101 painter = QtGui.QStylePainter(self)
101 102 option = QtGui.QStyleOptionFrame()
102 103 option.init(self)
103 104 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
104 105 painter.end()
105 106
106 107 super(CallTipWidget, self).paintEvent(event)
107 108
108 109 def setFont(self, font):
109 110 """ Reimplemented to allow use of this method as a slot.
110 111 """
111 112 super(CallTipWidget, self).setFont(font)
112 113
113 114 def showEvent(self, event):
114 115 """ Reimplemented to connect signal handlers and event filter.
115 116 """
116 117 super(CallTipWidget, self).showEvent(event)
117 118 self._text_edit.cursorPositionChanged.connect(
118 119 self._cursor_position_changed)
119 120 self._text_edit.installEventFilter(self)
120 121
121 122 #--------------------------------------------------------------------------
122 123 # 'CallTipWidget' interface
123 124 #--------------------------------------------------------------------------
124 125
125 126 def show_call_info(self, call_line=None, doc=None, maxlines=20):
126 127 """ Attempts to show the specified call line and docstring at the
127 128 current cursor location. The docstring is possibly truncated for
128 129 length.
129 130 """
130 131 if doc:
131 132 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
132 133 if match:
133 134 doc = doc[:match.end()] + '\n[Documentation continues...]'
134 135 else:
135 136 doc = ''
136 137
137 138 if call_line:
138 139 doc = '\n\n'.join([call_line, doc])
139 140 return self.show_tip(doc)
140 141
141 142 def show_tip(self, tip):
142 143 """ Attempts to show the specified tip at the current cursor location.
143 144 """
144 145 # Attempt to find the cursor position at which to show the call tip.
145 146 text_edit = self._text_edit
146 147 document = text_edit.document()
147 148 cursor = text_edit.textCursor()
148 149 search_pos = cursor.position() - 1
149 150 self._start_position, _ = self._find_parenthesis(search_pos,
150 151 forward=False)
151 152 if self._start_position == -1:
152 153 return False
153 154
154 155 # Set the text and resize the widget accordingly.
155 156 self.setText(tip)
156 157 self.resize(self.sizeHint())
157 158
158 159 # Locate and show the widget. Place the tip below the current line
159 160 # unless it would be off the screen. In that case, place it above
160 161 # the current line.
161 162 padding = 3 # Distance in pixels between cursor bounds and tip box.
162 163 cursor_rect = text_edit.cursorRect(cursor)
163 164 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
164 165 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
165 166 point.setY(point.y() + padding)
166 167 tip_height = self.size().height()
167 168 if point.y() + tip_height > screen_rect.height():
168 169 point = text_edit.mapToGlobal(cursor_rect.topRight())
169 170 point.setY(point.y() - tip_height - padding)
170 171 self.move(point)
171 172 self.show()
172 173 return True
173 174
174 175 #--------------------------------------------------------------------------
175 176 # Protected interface
176 177 #--------------------------------------------------------------------------
177 178
178 179 def _find_parenthesis(self, position, forward=True):
179 180 """ If 'forward' is True (resp. False), proceed forwards
180 181 (resp. backwards) through the line that contains 'position' until an
181 182 unmatched closing (resp. opening) parenthesis is found. Returns a
182 183 tuple containing the position of this parenthesis (or -1 if it is
183 184 not found) and the number commas (at depth 0) found along the way.
184 185 """
185 186 commas = depth = 0
186 187 document = self._text_edit.document()
187 qchar = document.characterAt(position)
188 while (position > 0 and qchar.isPrint() and
189 # Need to check explicitly for line/paragraph separators:
190 qchar.unicode() not in (0x2028, 0x2029)):
191 char = qchar.toAscii()
188 char = document.characterAt(position)
189 # Search until a match is found or a non-printable character is
190 # encountered.
191 while category(char) != 'Cc' and position > 0:
192 192 if char == ',' and depth == 0:
193 193 commas += 1
194 194 elif char == ')':
195 195 if forward and depth == 0:
196 196 break
197 197 depth += 1
198 198 elif char == '(':
199 199 if not forward and depth == 0:
200 200 break
201 201 depth -= 1
202 202 position += 1 if forward else -1
203 qchar = document.characterAt(position)
203 char = document.characterAt(position)
204 204 else:
205 205 position = -1
206 206 return position, commas
207 207
208 208 def _hide_later(self):
209 209 """ Hides the tooltip after some time has passed.
210 210 """
211 211 if not self._hide_timer.isActive():
212 212 self._hide_timer.start(300, self)
213 213
214 214 #------ Signal handlers ----------------------------------------------------
215 215
216 216 def _cursor_position_changed(self):
217 217 """ Updates the tip based on user cursor movement.
218 218 """
219 219 cursor = self._text_edit.textCursor()
220 220 if cursor.position() <= self._start_position:
221 221 self.hide()
222 222 else:
223 223 position, commas = self._find_parenthesis(self._start_position + 1)
224 224 if position != -1:
225 225 self.hide()
@@ -1,133 +1,133 b''
1 1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from IPython.external.qt import QtCore, QtGui
3 3
4 4
5 5 class CompletionWidget(QtGui.QListWidget):
6 6 """ A widget for GUI tab completion.
7 7 """
8 8
9 9 #--------------------------------------------------------------------------
10 10 # 'QObject' interface
11 11 #--------------------------------------------------------------------------
12 12
13 13 def __init__(self, text_edit):
14 14 """ Create a completion widget that is attached to the specified Qt
15 15 text edit widget.
16 16 """
17 17 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 18 super(CompletionWidget, self).__init__()
19 19
20 20 self._text_edit = text_edit
21 21
22 22 self.setAttribute(QtCore.Qt.WA_StaticContents)
23 23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
24 24
25 25 # Ensure that the text edit keeps focus when widget is displayed.
26 26 self.setFocusProxy(self._text_edit)
27 27
28 28 self.setFrameShadow(QtGui.QFrame.Plain)
29 29 self.setFrameShape(QtGui.QFrame.StyledPanel)
30 30
31 31 self.itemActivated.connect(self._complete_current)
32 32
33 33 def eventFilter(self, obj, event):
34 34 """ Reimplemented to handle keyboard input and to auto-hide when the
35 35 text edit loses focus.
36 36 """
37 37 if obj == self._text_edit:
38 38 etype = event.type()
39 39
40 40 if etype == QtCore.QEvent.KeyPress:
41 41 key, text = event.key(), event.text()
42 42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
43 43 QtCore.Qt.Key_Tab):
44 44 self._complete_current()
45 45 return True
46 46 elif key == QtCore.Qt.Key_Escape:
47 47 self.hide()
48 48 return True
49 49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
50 50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
51 51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
52 52 self.keyPressEvent(event)
53 53 return True
54 54
55 55 elif etype == QtCore.QEvent.FocusOut:
56 56 self.hide()
57 57
58 58 return super(CompletionWidget, self).eventFilter(obj, event)
59 59
60 60 #--------------------------------------------------------------------------
61 61 # 'QWidget' interface
62 62 #--------------------------------------------------------------------------
63 63
64 64 def hideEvent(self, event):
65 65 """ Reimplemented to disconnect signal handlers and event filter.
66 66 """
67 67 super(CompletionWidget, self).hideEvent(event)
68 68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
69 69 self._text_edit.removeEventFilter(self)
70 70
71 71 def showEvent(self, event):
72 72 """ Reimplemented to connect signal handlers and event filter.
73 73 """
74 74 super(CompletionWidget, self).showEvent(event)
75 75 self._text_edit.cursorPositionChanged.connect(self._update_current)
76 76 self._text_edit.installEventFilter(self)
77 77
78 78 #--------------------------------------------------------------------------
79 79 # 'CompletionWidget' interface
80 80 #--------------------------------------------------------------------------
81 81
82 82 def show_items(self, cursor, items):
83 83 """ Shows the completion widget with 'items' at the position specified
84 84 by 'cursor'.
85 85 """
86 86 text_edit = self._text_edit
87 87 point = text_edit.cursorRect(cursor).bottomRight()
88 88 point = text_edit.mapToGlobal(point)
89 89 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
90 90 if screen_rect.size().height() - point.y() - self.height() < 0:
91 91 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
92 92 point.setY(point.y() - self.height())
93 93 self.move(point)
94 94
95 95 self._start_position = cursor.position()
96 96 self.clear()
97 97 self.addItems(items)
98 98 self.setCurrentRow(0)
99 99 self.show()
100 100
101 101 #--------------------------------------------------------------------------
102 102 # Protected interface
103 103 #--------------------------------------------------------------------------
104 104
105 105 def _complete_current(self):
106 106 """ Perform the completion with the currently selected item.
107 107 """
108 108 self._current_text_cursor().insertText(self.currentItem().text())
109 109 self.hide()
110 110
111 111 def _current_text_cursor(self):
112 112 """ Returns a cursor with text between the start position and the
113 113 current position selected.
114 114 """
115 115 cursor = self._text_edit.textCursor()
116 116 if cursor.position() >= self._start_position:
117 117 cursor.setPosition(self._start_position,
118 118 QtGui.QTextCursor.KeepAnchor)
119 119 return cursor
120 120
121 121 def _update_current(self):
122 122 """ Updates the current item based on the current text.
123 123 """
124 124 prefix = self._current_text_cursor().selection().toPlainText()
125 125 if prefix:
126 126 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
127 127 QtCore.Qt.MatchCaseSensitive))
128 128 if items:
129 129 self.setCurrentItem(items[0])
130 130 else:
131 131 self.hide()
132 132 else:
133 133 self.hide()
@@ -1,1895 +1,1906 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 import os
8 9 from os.path import commonprefix
9 10 import re
10 import os
11 11 import sys
12 12 from textwrap import dedent
13 from unicodedata import category
13 14
14 15 # System library imports
15 from PyQt4 import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
16 17
17 18 # Local imports
18 19 from IPython.config.configurable import Configurable
19 20 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
20 21 from IPython.utils.traitlets import Bool, Enum, Int
21 22 from ansi_code_processor import QtAnsiCodeProcessor
22 23 from completion_widget import CompletionWidget
23 24
24 25 #-----------------------------------------------------------------------------
26 # Functions
27 #-----------------------------------------------------------------------------
28
29 def is_letter_or_number(char):
30 """ Returns whether the specified unicode character is a letter or a number.
31 """
32 cat = category(char)
33 return cat.startswith('L') or cat.startswith('N')
34
35 #-----------------------------------------------------------------------------
25 36 # Classes
26 37 #-----------------------------------------------------------------------------
27 38
28 39 class ConsoleWidget(Configurable, QtGui.QWidget):
29 40 """ An abstract base class for console-type widgets. This class has
30 41 functionality for:
31 42
32 43 * Maintaining a prompt and editing region
33 44 * Providing the traditional Unix-style console keyboard shortcuts
34 45 * Performing tab completion
35 46 * Paging text
36 47 * Handling ANSI escape codes
37 48
38 49 ConsoleWidget also provides a number of utility methods that will be
39 50 convenient to implementors of a console-style widget.
40 51 """
41 52 __metaclass__ = MetaQObjectHasTraits
42 53
43 54 #------ Configuration ------------------------------------------------------
44 55
45 56 # Whether to process ANSI escape codes.
46 57 ansi_codes = Bool(True, config=True)
47 58
48 59 # The maximum number of lines of text before truncation. Specifying a
49 60 # non-positive number disables text truncation (not recommended).
50 61 buffer_size = Int(500, config=True)
51 62
52 63 # Whether to use a list widget or plain text output for tab completion.
53 64 gui_completion = Bool(False, config=True)
54 65
55 66 # The type of underlying text widget to use. Valid values are 'plain', which
56 67 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
57 68 # NOTE: this value can only be specified during initialization.
58 69 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
59 70
60 71 # The type of paging to use. Valid values are:
61 72 # 'inside' : The widget pages like a traditional terminal.
62 73 # 'hsplit' : When paging is requested, the widget is split
63 74 # horizontally. The top pane contains the console, and the
64 75 # bottom pane contains the paged text.
65 76 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
66 77 # 'custom' : No action is taken by the widget beyond emitting a
67 78 # 'custom_page_requested(str)' signal.
68 79 # 'none' : The text is written directly to the console.
69 80 # NOTE: this value can only be specified during initialization.
70 81 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
71 82 default_value='inside', config=True)
72 83
73 84 # Whether to override ShortcutEvents for the keybindings defined by this
74 85 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
75 86 # priority (when it has focus) over, e.g., window-level menu shortcuts.
76 87 override_shortcuts = Bool(False)
77 88
78 89 #------ Signals ------------------------------------------------------------
79 90
80 91 # Signals that indicate ConsoleWidget state.
81 copy_available = QtCore.pyqtSignal(bool)
82 redo_available = QtCore.pyqtSignal(bool)
83 undo_available = QtCore.pyqtSignal(bool)
92 copy_available = QtCore.Signal(bool)
93 redo_available = QtCore.Signal(bool)
94 undo_available = QtCore.Signal(bool)
84 95
85 96 # Signal emitted when paging is needed and the paging style has been
86 97 # specified as 'custom'.
87 custom_page_requested = QtCore.pyqtSignal(object)
98 custom_page_requested = QtCore.Signal(object)
88 99
89 100 # Signal emitted when the font is changed.
90 font_changed = QtCore.pyqtSignal(QtGui.QFont)
101 font_changed = QtCore.Signal(QtGui.QFont)
91 102
92 103 #------ Protected class variables ------------------------------------------
93 104
94 105 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
95 106 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
96 107 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
97 108 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
98 109 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
99 110 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
100 111 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
101 112
102 113 _shortcuts = set(_ctrl_down_remap.keys() +
103 114 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
104 115 QtCore.Qt.Key_V ])
105 116
106 117 #---------------------------------------------------------------------------
107 118 # 'QObject' interface
108 119 #---------------------------------------------------------------------------
109 120
110 121 def __init__(self, parent=None, **kw):
111 122 """ Create a ConsoleWidget.
112 123
113 124 Parameters:
114 125 -----------
115 126 parent : QWidget, optional [default None]
116 127 The parent for this widget.
117 128 """
118 129 QtGui.QWidget.__init__(self, parent)
119 130 Configurable.__init__(self, **kw)
120 131
121 132 # Create the layout and underlying text widget.
122 133 layout = QtGui.QStackedLayout(self)
123 134 layout.setContentsMargins(0, 0, 0, 0)
124 135 self._control = self._create_control()
125 136 self._page_control = None
126 137 self._splitter = None
127 138 if self.paging in ('hsplit', 'vsplit'):
128 139 self._splitter = QtGui.QSplitter()
129 140 if self.paging == 'hsplit':
130 141 self._splitter.setOrientation(QtCore.Qt.Horizontal)
131 142 else:
132 143 self._splitter.setOrientation(QtCore.Qt.Vertical)
133 144 self._splitter.addWidget(self._control)
134 145 layout.addWidget(self._splitter)
135 146 else:
136 147 layout.addWidget(self._control)
137 148
138 149 # Create the paging widget, if necessary.
139 150 if self.paging in ('inside', 'hsplit', 'vsplit'):
140 151 self._page_control = self._create_page_control()
141 152 if self._splitter:
142 153 self._page_control.hide()
143 154 self._splitter.addWidget(self._page_control)
144 155 else:
145 156 layout.addWidget(self._page_control)
146 157
147 158 # Initialize protected variables. Some variables contain useful state
148 159 # information for subclasses; they should be considered read-only.
149 160 self._ansi_processor = QtAnsiCodeProcessor()
150 161 self._completion_widget = CompletionWidget(self._control)
151 162 self._continuation_prompt = '> '
152 163 self._continuation_prompt_html = None
153 164 self._executing = False
154 165 self._filter_drag = False
155 166 self._filter_resize = False
156 167 self._prompt = ''
157 168 self._prompt_html = None
158 169 self._prompt_pos = 0
159 170 self._prompt_sep = ''
160 171 self._reading = False
161 172 self._reading_callback = None
162 173 self._tab_width = 8
163 174 self._text_completing_pos = 0
164 175 self._filename = 'ipython.html'
165 176 self._png_mode=None
166 177
167 178 # Set a monospaced font.
168 179 self.reset_font()
169 180
170 181 # Configure actions.
171 182 action = QtGui.QAction('Print', None)
172 183 action.setEnabled(True)
173 184 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
174 185 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
175 186 # only override if there is a collision
176 187 # Qt ctrl = cmd on OSX, so the match gets a false positive on darwin
177 188 printkey = "Ctrl+Shift+P"
178 189 action.setShortcut(printkey)
179 190 action.triggered.connect(self.print_)
180 191 self.addAction(action)
181 192 self._print_action = action
182 193
183 194 action = QtGui.QAction('Save as HTML/XML', None)
184 195 action.setEnabled(self.can_export())
185 196 action.setShortcut(QtGui.QKeySequence.Save)
186 197 action.triggered.connect(self.export)
187 198 self.addAction(action)
188 199 self._export_action = action
189 200
190 201 action = QtGui.QAction('Select All', None)
191 202 action.setEnabled(True)
192 203 action.setShortcut(QtGui.QKeySequence.SelectAll)
193 204 action.triggered.connect(self.select_all)
194 205 self.addAction(action)
195 206 self._select_all_action = action
196 207
197 208
198 209 def eventFilter(self, obj, event):
199 210 """ Reimplemented to ensure a console-like behavior in the underlying
200 211 text widgets.
201 212 """
202 213 etype = event.type()
203 214 if etype == QtCore.QEvent.KeyPress:
204 215
205 216 # Re-map keys for all filtered widgets.
206 217 key = event.key()
207 218 if self._control_key_down(event.modifiers()) and \
208 219 key in self._ctrl_down_remap:
209 220 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
210 221 self._ctrl_down_remap[key],
211 222 QtCore.Qt.NoModifier)
212 223 QtGui.qApp.sendEvent(obj, new_event)
213 224 return True
214 225
215 226 elif obj == self._control:
216 227 return self._event_filter_console_keypress(event)
217 228
218 229 elif obj == self._page_control:
219 230 return self._event_filter_page_keypress(event)
220 231
221 232 # Make middle-click paste safe.
222 233 elif etype == QtCore.QEvent.MouseButtonRelease and \
223 234 event.button() == QtCore.Qt.MidButton and \
224 235 obj == self._control.viewport():
225 236 cursor = self._control.cursorForPosition(event.pos())
226 237 self._control.setTextCursor(cursor)
227 238 self.paste(QtGui.QClipboard.Selection)
228 239 return True
229 240
230 241 # Manually adjust the scrollbars *after* a resize event is dispatched.
231 242 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
232 243 self._filter_resize = True
233 244 QtGui.qApp.sendEvent(obj, event)
234 245 self._adjust_scrollbars()
235 246 self._filter_resize = False
236 247 return True
237 248
238 249 # Override shortcuts for all filtered widgets.
239 250 elif etype == QtCore.QEvent.ShortcutOverride and \
240 251 self.override_shortcuts and \
241 252 self._control_key_down(event.modifiers()) and \
242 253 event.key() in self._shortcuts:
243 254 event.accept()
244 255
245 256 # Ensure that drags are safe. The problem is that the drag starting
246 257 # logic, which determines whether the drag is a Copy or Move, is locked
247 258 # down in QTextControl. If the widget is editable, which it must be if
248 259 # we're not executing, the drag will be a Move. The following hack
249 260 # prevents QTextControl from deleting the text by clearing the selection
250 261 # when a drag leave event originating from this widget is dispatched.
251 262 # The fact that we have to clear the user's selection is unfortunate,
252 263 # but the alternative--trying to prevent Qt from using its hardwired
253 264 # drag logic and writing our own--is worse.
254 265 elif etype == QtCore.QEvent.DragEnter and \
255 266 obj == self._control.viewport() and \
256 267 event.source() == self._control.viewport():
257 268 self._filter_drag = True
258 269 elif etype == QtCore.QEvent.DragLeave and \
259 270 obj == self._control.viewport() and \
260 271 self._filter_drag:
261 272 cursor = self._control.textCursor()
262 273 cursor.clearSelection()
263 274 self._control.setTextCursor(cursor)
264 275 self._filter_drag = False
265 276
266 277 # Ensure that drops are safe.
267 278 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
268 279 cursor = self._control.cursorForPosition(event.pos())
269 280 if self._in_buffer(cursor.position()):
270 text = unicode(event.mimeData().text())
281 text = event.mimeData().text()
271 282 self._insert_plain_text_into_buffer(cursor, text)
272 283
273 284 # Qt is expecting to get something here--drag and drop occurs in its
274 285 # own event loop. Send a DragLeave event to end it.
275 286 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
276 287 return True
277 288
278 289 return super(ConsoleWidget, self).eventFilter(obj, event)
279 290
280 291 #---------------------------------------------------------------------------
281 292 # 'QWidget' interface
282 293 #---------------------------------------------------------------------------
283 294
284 295 def sizeHint(self):
285 296 """ Reimplemented to suggest a size that is 80 characters wide and
286 297 25 lines high.
287 298 """
288 299 font_metrics = QtGui.QFontMetrics(self.font)
289 300 margin = (self._control.frameWidth() +
290 301 self._control.document().documentMargin()) * 2
291 302 style = self.style()
292 303 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
293 304
294 305 # Note 1: Despite my best efforts to take the various margins into
295 306 # account, the width is still coming out a bit too small, so we include
296 307 # a fudge factor of one character here.
297 308 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
298 309 # to a Qt bug on certain Mac OS systems where it returns 0.
299 310 width = font_metrics.width(' ') * 81 + margin
300 311 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
301 312 if self.paging == 'hsplit':
302 313 width = width * 2 + splitwidth
303 314
304 315 height = font_metrics.height() * 25 + margin
305 316 if self.paging == 'vsplit':
306 317 height = height * 2 + splitwidth
307 318
308 319 return QtCore.QSize(width, height)
309 320
310 321 #---------------------------------------------------------------------------
311 322 # 'ConsoleWidget' public interface
312 323 #---------------------------------------------------------------------------
313 324
314 325 def can_copy(self):
315 326 """ Returns whether text can be copied to the clipboard.
316 327 """
317 328 return self._control.textCursor().hasSelection()
318 329
319 330 def can_cut(self):
320 331 """ Returns whether text can be cut to the clipboard.
321 332 """
322 333 cursor = self._control.textCursor()
323 334 return (cursor.hasSelection() and
324 335 self._in_buffer(cursor.anchor()) and
325 336 self._in_buffer(cursor.position()))
326 337
327 338 def can_paste(self):
328 339 """ Returns whether text can be pasted from the clipboard.
329 340 """
330 341 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
331 return not QtGui.QApplication.clipboard().text().isEmpty()
342 return bool(QtGui.QApplication.clipboard().text())
332 343 return False
333 344
334 345 def can_export(self):
335 346 """Returns whether we can export. Currently only rich widgets
336 347 can export html.
337 348 """
338 349 return self.kind == "rich"
339 350
340 351 def clear(self, keep_input=True):
341 352 """ Clear the console.
342 353
343 354 Parameters:
344 355 -----------
345 356 keep_input : bool, optional (default True)
346 357 If set, restores the old input buffer if a new prompt is written.
347 358 """
348 359 if self._executing:
349 360 self._control.clear()
350 361 else:
351 362 if keep_input:
352 363 input_buffer = self.input_buffer
353 364 self._control.clear()
354 365 self._show_prompt()
355 366 if keep_input:
356 367 self.input_buffer = input_buffer
357 368
358 369 def copy(self):
359 370 """ Copy the currently selected text to the clipboard.
360 371 """
361 372 self._control.copy()
362 373
363 374 def cut(self):
364 375 """ Copy the currently selected text to the clipboard and delete it
365 376 if it's inside the input buffer.
366 377 """
367 378 self.copy()
368 379 if self.can_cut():
369 380 self._control.textCursor().removeSelectedText()
370 381
371 382 def execute(self, source=None, hidden=False, interactive=False):
372 383 """ Executes source or the input buffer, possibly prompting for more
373 384 input.
374 385
375 386 Parameters:
376 387 -----------
377 388 source : str, optional
378 389
379 390 The source to execute. If not specified, the input buffer will be
380 391 used. If specified and 'hidden' is False, the input buffer will be
381 392 replaced with the source before execution.
382 393
383 394 hidden : bool, optional (default False)
384 395
385 396 If set, no output will be shown and the prompt will not be modified.
386 397 In other words, it will be completely invisible to the user that
387 398 an execution has occurred.
388 399
389 400 interactive : bool, optional (default False)
390 401
391 402 Whether the console is to treat the source as having been manually
392 403 entered by the user. The effect of this parameter depends on the
393 404 subclass implementation.
394 405
395 406 Raises:
396 407 -------
397 408 RuntimeError
398 409 If incomplete input is given and 'hidden' is True. In this case,
399 410 it is not possible to prompt for more input.
400 411
401 412 Returns:
402 413 --------
403 414 A boolean indicating whether the source was executed.
404 415 """
405 416 # WARNING: The order in which things happen here is very particular, in
406 417 # large part because our syntax highlighting is fragile. If you change
407 418 # something, test carefully!
408 419
409 420 # Decide what to execute.
410 421 if source is None:
411 422 source = self.input_buffer
412 423 if not hidden:
413 424 # A newline is appended later, but it should be considered part
414 425 # of the input buffer.
415 426 source += '\n'
416 427 elif not hidden:
417 428 self.input_buffer = source
418 429
419 430 # Execute the source or show a continuation prompt if it is incomplete.
420 431 complete = self._is_complete(source, interactive)
421 432 if hidden:
422 433 if complete:
423 434 self._execute(source, hidden)
424 435 else:
425 436 error = 'Incomplete noninteractive input: "%s"'
426 437 raise RuntimeError(error % source)
427 438 else:
428 439 if complete:
429 440 self._append_plain_text('\n')
430 441 self._executing_input_buffer = self.input_buffer
431 442 self._executing = True
432 443 self._prompt_finished()
433 444
434 445 # The maximum block count is only in effect during execution.
435 446 # This ensures that _prompt_pos does not become invalid due to
436 447 # text truncation.
437 448 self._control.document().setMaximumBlockCount(self.buffer_size)
438 449
439 450 # Setting a positive maximum block count will automatically
440 451 # disable the undo/redo history, but just to be safe:
441 452 self._control.setUndoRedoEnabled(False)
442 453
443 454 # Perform actual execution.
444 455 self._execute(source, hidden)
445 456
446 457 else:
447 458 # Do this inside an edit block so continuation prompts are
448 459 # removed seamlessly via undo/redo.
449 460 cursor = self._get_end_cursor()
450 461 cursor.beginEditBlock()
451 462 cursor.insertText('\n')
452 463 self._insert_continuation_prompt(cursor)
453 464 cursor.endEditBlock()
454 465
455 466 # Do not do this inside the edit block. It works as expected
456 467 # when using a QPlainTextEdit control, but does not have an
457 468 # effect when using a QTextEdit. I believe this is a Qt bug.
458 469 self._control.moveCursor(QtGui.QTextCursor.End)
459 470
460 471 return complete
461 472
462 473 def _get_input_buffer(self):
463 474 """ The text that the user has entered entered at the current prompt.
464 475 """
465 476 # If we're executing, the input buffer may not even exist anymore due to
466 477 # the limit imposed by 'buffer_size'. Therefore, we store it.
467 478 if self._executing:
468 479 return self._executing_input_buffer
469 480
470 481 cursor = self._get_end_cursor()
471 482 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
472 input_buffer = unicode(cursor.selection().toPlainText())
483 input_buffer = cursor.selection().toPlainText()
473 484
474 485 # Strip out continuation prompts.
475 486 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
476 487
477 488 def _set_input_buffer(self, string):
478 489 """ Replaces the text in the input buffer with 'string'.
479 490 """
480 491 # For now, it is an error to modify the input buffer during execution.
481 492 if self._executing:
482 493 raise RuntimeError("Cannot change input buffer during execution.")
483 494
484 495 # Remove old text.
485 496 cursor = self._get_end_cursor()
486 497 cursor.beginEditBlock()
487 498 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
488 499 cursor.removeSelectedText()
489 500
490 501 # Insert new text with continuation prompts.
491 502 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
492 503 cursor.endEditBlock()
493 504 self._control.moveCursor(QtGui.QTextCursor.End)
494 505
495 506 input_buffer = property(_get_input_buffer, _set_input_buffer)
496 507
497 508 def _get_font(self):
498 509 """ The base font being used by the ConsoleWidget.
499 510 """
500 511 return self._control.document().defaultFont()
501 512
502 513 def _set_font(self, font):
503 514 """ Sets the base font for the ConsoleWidget to the specified QFont.
504 515 """
505 516 font_metrics = QtGui.QFontMetrics(font)
506 517 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
507 518
508 519 self._completion_widget.setFont(font)
509 520 self._control.document().setDefaultFont(font)
510 521 if self._page_control:
511 522 self._page_control.document().setDefaultFont(font)
512 523
513 524 self.font_changed.emit(font)
514 525
515 526 font = property(_get_font, _set_font)
516 527
517 528 def paste(self, mode=QtGui.QClipboard.Clipboard):
518 529 """ Paste the contents of the clipboard into the input region.
519 530
520 531 Parameters:
521 532 -----------
522 533 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
523 534
524 535 Controls which part of the system clipboard is used. This can be
525 536 used to access the selection clipboard in X11 and the Find buffer
526 537 in Mac OS. By default, the regular clipboard is used.
527 538 """
528 539 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
529 540 # Make sure the paste is safe.
530 541 self._keep_cursor_in_buffer()
531 542 cursor = self._control.textCursor()
532 543
533 544 # Remove any trailing newline, which confuses the GUI and forces the
534 545 # user to backspace.
535 text = unicode(QtGui.QApplication.clipboard().text(mode)).rstrip()
546 text = QtGui.QApplication.clipboard().text(mode).rstrip()
536 547 self._insert_plain_text_into_buffer(cursor, dedent(text))
537 548
538 549 def print_(self, printer = None):
539 550 """ Print the contents of the ConsoleWidget to the specified QPrinter.
540 551 """
541 552 if (not printer):
542 553 printer = QtGui.QPrinter()
543 554 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
544 555 return
545 556 self._control.print_(printer)
546 557
547 558 def export(self, parent = None):
548 559 """Export HTML/XML in various modes from one Dialog."""
549 560 parent = parent or None # sometimes parent is False
550 561 dialog = QtGui.QFileDialog(parent, 'Save Console as...')
551 562 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
552 563 filters = [
553 564 'HTML with PNG figures (*.html *.htm)',
554 565 'XHTML with inline SVG figures (*.xhtml *.xml)'
555 566 ]
556 567 dialog.setNameFilters(filters)
557 568 if self._filename:
558 569 dialog.selectFile(self._filename)
559 570 root,ext = os.path.splitext(self._filename)
560 571 if ext.lower() in ('.xml', '.xhtml'):
561 572 dialog.selectNameFilter(filters[-1])
562 573 if dialog.exec_():
563 574 filename = str(dialog.selectedFiles()[0])
564 575 self._filename = filename
565 576 choice = str(dialog.selectedNameFilter())
566 577
567 578 if choice.startswith('XHTML'):
568 579 exporter = self.export_xhtml
569 580 else:
570 581 exporter = self.export_html
571 582
572 583 try:
573 584 return exporter(filename)
574 585 except Exception, e:
575 586 title = self.window().windowTitle()
576 587 msg = "Error while saving to: %s\n"%filename+str(e)
577 588 reply = QtGui.QMessageBox.warning(self, title, msg,
578 589 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
579 590 return None
580 591
581 592 def export_html(self, filename):
582 593 """ Export the contents of the ConsoleWidget as HTML.
583 594
584 595 Parameters:
585 596 -----------
586 597 filename : str
587 598 The file to be saved.
588 599 inline : bool, optional [default True]
589 600 If True, include images as inline PNGs. Otherwise,
590 601 include them as links to external PNG files, mimicking
591 602 web browsers' "Web Page, Complete" behavior.
592 603 """
593 604 # N.B. this is overly restrictive, but Qt's output is
594 605 # predictable...
595 606 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
596 607 html = self.fix_html_encoding(
597 608 str(self._control.toHtml().toUtf8()))
598 609 if self._png_mode:
599 610 # preference saved, don't ask again
600 611 if img_re.search(html):
601 612 inline = (self._png_mode == 'inline')
602 613 else:
603 614 inline = True
604 615 elif img_re.search(html):
605 616 # there are images
606 617 widget = QtGui.QWidget()
607 618 layout = QtGui.QVBoxLayout(widget)
608 619 title = self.window().windowTitle()
609 620 msg = "Exporting HTML with PNGs"
610 621 info = "Would you like inline PNGs (single large html file) or "+\
611 622 "external image files?"
612 623 checkbox = QtGui.QCheckBox("&Don't ask again")
613 624 checkbox.setShortcut('D')
614 625 ib = QtGui.QPushButton("&Inline", self)
615 626 ib.setShortcut('I')
616 627 eb = QtGui.QPushButton("&External", self)
617 628 eb.setShortcut('E')
618 629 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
619 630 box.setInformativeText(info)
620 631 box.addButton(ib,QtGui.QMessageBox.NoRole)
621 632 box.addButton(eb,QtGui.QMessageBox.YesRole)
622 633 box.setDefaultButton(ib)
623 634 layout.setSpacing(0)
624 635 layout.addWidget(box)
625 636 layout.addWidget(checkbox)
626 637 widget.setLayout(layout)
627 638 widget.show()
628 639 reply = box.exec_()
629 640 inline = (reply == 0)
630 641 if checkbox.checkState():
631 642 # don't ask anymore, always use this choice
632 643 if inline:
633 644 self._png_mode='inline'
634 645 else:
635 646 self._png_mode='external'
636 647 else:
637 648 # no images
638 649 inline = True
639 650
640 651 if inline:
641 652 path = None
642 653 else:
643 654 root,ext = os.path.splitext(filename)
644 655 path = root+"_files"
645 656 if os.path.isfile(path):
646 657 raise OSError("%s exists, but is not a directory."%path)
647 658
648 659 f = open(filename, 'w')
649 660 try:
650 661 f.write(img_re.sub(
651 662 lambda x: self.image_tag(x, path = path, format = "png"),
652 663 html))
653 664 except Exception, e:
654 665 f.close()
655 666 raise e
656 667 else:
657 668 f.close()
658 669 return filename
659 670
660 671
661 672 def export_xhtml(self, filename):
662 673 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
663 674 """
664 675 f = open(filename, 'w')
665 676 try:
666 677 # N.B. this is overly restrictive, but Qt's output is
667 678 # predictable...
668 679 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
669 680 html = str(self._control.toHtml().toUtf8())
670 681 # Hack to make xhtml header -- note that we are not doing
671 682 # any check for valid xml
672 683 offset = html.find("<html>")
673 684 assert(offset > -1)
674 685 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
675 686 html[offset+6:])
676 687 # And now declare UTF-8 encoding
677 688 html = self.fix_html_encoding(html)
678 689 f.write(img_re.sub(
679 690 lambda x: self.image_tag(x, path = None, format = "svg"),
680 691 html))
681 692 except Exception, e:
682 693 f.close()
683 694 raise e
684 695 else:
685 696 f.close()
686 697 return filename
687 698
688 699 def fix_html_encoding(self, html):
689 700 """ Return html string, with a UTF-8 declaration added to <HEAD>.
690 701
691 702 Assumes that html is Qt generated and has already been UTF-8 encoded
692 703 and coerced to a python string. If the expected head element is
693 704 not found, the given object is returned unmodified.
694 705
695 706 This patching is needed for proper rendering of some characters
696 707 (e.g., indented commands) when viewing exported HTML on a local
697 708 system (i.e., without seeing an encoding declaration in an HTTP
698 709 header).
699 710
700 711 C.f. http://www.w3.org/International/O-charset for details.
701 712 """
702 713 offset = html.find("<head>")
703 714 if(offset > -1):
704 715 html = (html[:offset+6]+
705 716 '\n<meta http-equiv="Content-Type" '+
706 717 'content="text/html; charset=utf-8" />\n'+
707 718 html[offset+6:])
708 719
709 720 return html
710 721
711 722 def image_tag(self, match, path = None, format = "png"):
712 723 """ Return (X)HTML mark-up for the image-tag given by match.
713 724
714 725 Parameters
715 726 ----------
716 727 match : re.SRE_Match
717 728 A match to an HTML image tag as exported by Qt, with
718 729 match.group("Name") containing the matched image ID.
719 730
720 731 path : string|None, optional [default None]
721 732 If not None, specifies a path to which supporting files
722 733 may be written (e.g., for linked images).
723 734 If None, all images are to be included inline.
724 735
725 736 format : "png"|"svg", optional [default "png"]
726 737 Format for returned or referenced images.
727 738
728 739 Subclasses supporting image display should override this
729 740 method.
730 741 """
731 742
732 743 # Default case -- not enough information to generate tag
733 744 return ""
734 745
735 746 def prompt_to_top(self):
736 747 """ Moves the prompt to the top of the viewport.
737 748 """
738 749 if not self._executing:
739 750 prompt_cursor = self._get_prompt_cursor()
740 751 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
741 752 self._set_cursor(prompt_cursor)
742 753 self._set_top_cursor(prompt_cursor)
743 754
744 755 def redo(self):
745 756 """ Redo the last operation. If there is no operation to redo, nothing
746 757 happens.
747 758 """
748 759 self._control.redo()
749 760
750 761 def reset_font(self):
751 762 """ Sets the font to the default fixed-width font for this platform.
752 763 """
753 764 if sys.platform == 'win32':
754 765 # Consolas ships with Vista/Win7, fallback to Courier if needed
755 766 family, fallback = 'Consolas', 'Courier'
756 767 elif sys.platform == 'darwin':
757 768 # OSX always has Monaco, no need for a fallback
758 769 family, fallback = 'Monaco', None
759 770 else:
760 771 # FIXME: remove Consolas as a default on Linux once our font
761 772 # selections are configurable by the user.
762 773 family, fallback = 'Consolas', 'Monospace'
763 774 font = get_font(family, fallback)
764 775 font.setPointSize(QtGui.qApp.font().pointSize())
765 776 font.setStyleHint(QtGui.QFont.TypeWriter)
766 777 self._set_font(font)
767 778
768 779 def change_font_size(self, delta):
769 780 """Change the font size by the specified amount (in points).
770 781 """
771 782 font = self.font
772 783 font.setPointSize(font.pointSize() + delta)
773 784 self._set_font(font)
774 785
775 786 def select_all(self):
776 787 """ Selects all the text in the buffer.
777 788 """
778 789 self._control.selectAll()
779 790
780 791 def _get_tab_width(self):
781 792 """ The width (in terms of space characters) for tab characters.
782 793 """
783 794 return self._tab_width
784 795
785 796 def _set_tab_width(self, tab_width):
786 797 """ Sets the width (in terms of space characters) for tab characters.
787 798 """
788 799 font_metrics = QtGui.QFontMetrics(self.font)
789 800 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
790 801
791 802 self._tab_width = tab_width
792 803
793 804 tab_width = property(_get_tab_width, _set_tab_width)
794 805
795 806 def undo(self):
796 807 """ Undo the last operation. If there is no operation to undo, nothing
797 808 happens.
798 809 """
799 810 self._control.undo()
800 811
801 812 #---------------------------------------------------------------------------
802 813 # 'ConsoleWidget' abstract interface
803 814 #---------------------------------------------------------------------------
804 815
805 816 def _is_complete(self, source, interactive):
806 817 """ Returns whether 'source' can be executed. When triggered by an
807 818 Enter/Return key press, 'interactive' is True; otherwise, it is
808 819 False.
809 820 """
810 821 raise NotImplementedError
811 822
812 823 def _execute(self, source, hidden):
813 824 """ Execute 'source'. If 'hidden', do not show any output.
814 825 """
815 826 raise NotImplementedError
816 827
817 828 def _prompt_started_hook(self):
818 829 """ Called immediately after a new prompt is displayed.
819 830 """
820 831 pass
821 832
822 833 def _prompt_finished_hook(self):
823 834 """ Called immediately after a prompt is finished, i.e. when some input
824 835 will be processed and a new prompt displayed.
825 836 """
826 837 pass
827 838
828 839 def _up_pressed(self):
829 840 """ Called when the up key is pressed. Returns whether to continue
830 841 processing the event.
831 842 """
832 843 return True
833 844
834 845 def _down_pressed(self):
835 846 """ Called when the down key is pressed. Returns whether to continue
836 847 processing the event.
837 848 """
838 849 return True
839 850
840 851 def _tab_pressed(self):
841 852 """ Called when the tab key is pressed. Returns whether to continue
842 853 processing the event.
843 854 """
844 855 return False
845 856
846 857 #--------------------------------------------------------------------------
847 858 # 'ConsoleWidget' protected interface
848 859 #--------------------------------------------------------------------------
849 860
850 861 def _append_html(self, html):
851 862 """ Appends html at the end of the console buffer.
852 863 """
853 864 cursor = self._get_end_cursor()
854 865 self._insert_html(cursor, html)
855 866
856 867 def _append_html_fetching_plain_text(self, html):
857 868 """ Appends 'html', then returns the plain text version of it.
858 869 """
859 870 cursor = self._get_end_cursor()
860 871 return self._insert_html_fetching_plain_text(cursor, html)
861 872
862 873 def _append_plain_text(self, text):
863 874 """ Appends plain text at the end of the console buffer, processing
864 875 ANSI codes if enabled.
865 876 """
866 877 cursor = self._get_end_cursor()
867 878 self._insert_plain_text(cursor, text)
868 879
869 880 def _append_plain_text_keeping_prompt(self, text):
870 881 """ Writes 'text' after the current prompt, then restores the old prompt
871 882 with its old input buffer.
872 883 """
873 884 input_buffer = self.input_buffer
874 885 self._append_plain_text('\n')
875 886 self._prompt_finished()
876 887
877 888 self._append_plain_text(text)
878 889 self._show_prompt()
879 890 self.input_buffer = input_buffer
880 891
881 892 def _cancel_text_completion(self):
882 893 """ If text completion is progress, cancel it.
883 894 """
884 895 if self._text_completing_pos:
885 896 self._clear_temporary_buffer()
886 897 self._text_completing_pos = 0
887 898
888 899 def _clear_temporary_buffer(self):
889 900 """ Clears the "temporary text" buffer, i.e. all the text following
890 901 the prompt region.
891 902 """
892 903 # Select and remove all text below the input buffer.
893 904 cursor = self._get_prompt_cursor()
894 905 prompt = self._continuation_prompt.lstrip()
895 906 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
896 907 temp_cursor = QtGui.QTextCursor(cursor)
897 908 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
898 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
909 text = temp_cursor.selection().toPlainText().lstrip()
899 910 if not text.startswith(prompt):
900 911 break
901 912 else:
902 913 # We've reached the end of the input buffer and no text follows.
903 914 return
904 915 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
905 916 cursor.movePosition(QtGui.QTextCursor.End,
906 917 QtGui.QTextCursor.KeepAnchor)
907 918 cursor.removeSelectedText()
908 919
909 920 # After doing this, we have no choice but to clear the undo/redo
910 921 # history. Otherwise, the text is not "temporary" at all, because it
911 922 # can be recalled with undo/redo. Unfortunately, Qt does not expose
912 923 # fine-grained control to the undo/redo system.
913 924 if self._control.isUndoRedoEnabled():
914 925 self._control.setUndoRedoEnabled(False)
915 926 self._control.setUndoRedoEnabled(True)
916 927
917 928 def _complete_with_items(self, cursor, items):
918 929 """ Performs completion with 'items' at the specified cursor location.
919 930 """
920 931 self._cancel_text_completion()
921 932
922 933 if len(items) == 1:
923 934 cursor.setPosition(self._control.textCursor().position(),
924 935 QtGui.QTextCursor.KeepAnchor)
925 936 cursor.insertText(items[0])
926 937
927 938 elif len(items) > 1:
928 939 current_pos = self._control.textCursor().position()
929 940 prefix = commonprefix(items)
930 941 if prefix:
931 942 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
932 943 cursor.insertText(prefix)
933 944 current_pos = cursor.position()
934 945
935 946 if self.gui_completion:
936 947 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
937 948 self._completion_widget.show_items(cursor, items)
938 949 else:
939 950 cursor.beginEditBlock()
940 951 self._append_plain_text('\n')
941 952 self._page(self._format_as_columns(items))
942 953 cursor.endEditBlock()
943 954
944 955 cursor.setPosition(current_pos)
945 956 self._control.moveCursor(QtGui.QTextCursor.End)
946 957 self._control.setTextCursor(cursor)
947 958 self._text_completing_pos = current_pos
948 959
949 960 def _context_menu_make(self, pos):
950 961 """ Creates a context menu for the given QPoint (in widget coordinates).
951 962 """
952 963 menu = QtGui.QMenu(self)
953 964
954 965 cut_action = menu.addAction('Cut', self.cut)
955 966 cut_action.setEnabled(self.can_cut())
956 967 cut_action.setShortcut(QtGui.QKeySequence.Cut)
957 968
958 969 copy_action = menu.addAction('Copy', self.copy)
959 970 copy_action.setEnabled(self.can_copy())
960 971 copy_action.setShortcut(QtGui.QKeySequence.Copy)
961 972
962 973 paste_action = menu.addAction('Paste', self.paste)
963 974 paste_action.setEnabled(self.can_paste())
964 975 paste_action.setShortcut(QtGui.QKeySequence.Paste)
965 976
966 977 menu.addSeparator()
967 978 menu.addAction(self._select_all_action)
968 979
969 980 menu.addSeparator()
970 981 menu.addAction(self._export_action)
971 982 menu.addAction(self._print_action)
972 983
973 984 return menu
974 985
975 986 def _control_key_down(self, modifiers, include_command=False):
976 987 """ Given a KeyboardModifiers flags object, return whether the Control
977 988 key is down.
978 989
979 990 Parameters:
980 991 -----------
981 992 include_command : bool, optional (default True)
982 993 Whether to treat the Command key as a (mutually exclusive) synonym
983 994 for Control when in Mac OS.
984 995 """
985 996 # Note that on Mac OS, ControlModifier corresponds to the Command key
986 997 # while MetaModifier corresponds to the Control key.
987 998 if sys.platform == 'darwin':
988 999 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
989 1000 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
990 1001 else:
991 1002 return bool(modifiers & QtCore.Qt.ControlModifier)
992 1003
993 1004 def _create_control(self):
994 1005 """ Creates and connects the underlying text widget.
995 1006 """
996 1007 # Create the underlying control.
997 1008 if self.kind == 'plain':
998 1009 control = QtGui.QPlainTextEdit()
999 1010 elif self.kind == 'rich':
1000 1011 control = QtGui.QTextEdit()
1001 1012 control.setAcceptRichText(False)
1002 1013
1003 1014 # Install event filters. The filter on the viewport is needed for
1004 1015 # mouse events and drag events.
1005 1016 control.installEventFilter(self)
1006 1017 control.viewport().installEventFilter(self)
1007 1018
1008 1019 # Connect signals.
1009 1020 control.cursorPositionChanged.connect(self._cursor_position_changed)
1010 1021 control.customContextMenuRequested.connect(
1011 1022 self._custom_context_menu_requested)
1012 1023 control.copyAvailable.connect(self.copy_available)
1013 1024 control.redoAvailable.connect(self.redo_available)
1014 1025 control.undoAvailable.connect(self.undo_available)
1015 1026
1016 1027 # Hijack the document size change signal to prevent Qt from adjusting
1017 1028 # the viewport's scrollbar. We are relying on an implementation detail
1018 1029 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1019 1030 # this functionality we cannot create a nice terminal interface.
1020 1031 layout = control.document().documentLayout()
1021 1032 layout.documentSizeChanged.disconnect()
1022 1033 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1023 1034
1024 1035 # Configure the control.
1025 1036 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1026 1037 control.setReadOnly(True)
1027 1038 control.setUndoRedoEnabled(False)
1028 1039 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1029 1040 return control
1030 1041
1031 1042 def _create_page_control(self):
1032 1043 """ Creates and connects the underlying paging widget.
1033 1044 """
1034 1045 if self.kind == 'plain':
1035 1046 control = QtGui.QPlainTextEdit()
1036 1047 elif self.kind == 'rich':
1037 1048 control = QtGui.QTextEdit()
1038 1049 control.installEventFilter(self)
1039 1050 control.setReadOnly(True)
1040 1051 control.setUndoRedoEnabled(False)
1041 1052 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1042 1053 return control
1043 1054
1044 1055 def _event_filter_console_keypress(self, event):
1045 1056 """ Filter key events for the underlying text widget to create a
1046 1057 console-like interface.
1047 1058 """
1048 1059 intercepted = False
1049 1060 cursor = self._control.textCursor()
1050 1061 position = cursor.position()
1051 1062 key = event.key()
1052 1063 ctrl_down = self._control_key_down(event.modifiers())
1053 1064 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1054 1065 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1055 1066
1056 1067 #------ Special sequences ----------------------------------------------
1057 1068
1058 1069 if event.matches(QtGui.QKeySequence.Copy):
1059 1070 self.copy()
1060 1071 intercepted = True
1061 1072
1062 1073 elif event.matches(QtGui.QKeySequence.Cut):
1063 1074 self.cut()
1064 1075 intercepted = True
1065 1076
1066 1077 elif event.matches(QtGui.QKeySequence.Paste):
1067 1078 self.paste()
1068 1079 intercepted = True
1069 1080
1070 1081 #------ Special modifier logic -----------------------------------------
1071 1082
1072 1083 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1073 1084 intercepted = True
1074 1085
1075 1086 # Special handling when tab completing in text mode.
1076 1087 self._cancel_text_completion()
1077 1088
1078 1089 if self._in_buffer(position):
1079 1090 if self._reading:
1080 1091 self._append_plain_text('\n')
1081 1092 self._reading = False
1082 1093 if self._reading_callback:
1083 1094 self._reading_callback()
1084 1095
1085 1096 # If the input buffer is a single line or there is only
1086 1097 # whitespace after the cursor, execute. Otherwise, split the
1087 1098 # line with a continuation prompt.
1088 1099 elif not self._executing:
1089 1100 cursor.movePosition(QtGui.QTextCursor.End,
1090 1101 QtGui.QTextCursor.KeepAnchor)
1091 at_end = cursor.selectedText().trimmed().isEmpty()
1102 at_end = len(cursor.selectedText().strip()) == 0
1092 1103 single_line = (self._get_end_cursor().blockNumber() ==
1093 1104 self._get_prompt_cursor().blockNumber())
1094 1105 if (at_end or shift_down or single_line) and not ctrl_down:
1095 1106 self.execute(interactive = not shift_down)
1096 1107 else:
1097 1108 # Do this inside an edit block for clean undo/redo.
1098 1109 cursor.beginEditBlock()
1099 1110 cursor.setPosition(position)
1100 1111 cursor.insertText('\n')
1101 1112 self._insert_continuation_prompt(cursor)
1102 1113 cursor.endEditBlock()
1103 1114
1104 1115 # Ensure that the whole input buffer is visible.
1105 1116 # FIXME: This will not be usable if the input buffer is
1106 1117 # taller than the console widget.
1107 1118 self._control.moveCursor(QtGui.QTextCursor.End)
1108 1119 self._control.setTextCursor(cursor)
1109 1120
1110 1121 #------ Control/Cmd modifier -------------------------------------------
1111 1122
1112 1123 elif ctrl_down:
1113 1124 if key == QtCore.Qt.Key_G:
1114 1125 self._keyboard_quit()
1115 1126 intercepted = True
1116 1127
1117 1128 elif key == QtCore.Qt.Key_K:
1118 1129 if self._in_buffer(position):
1119 1130 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1120 1131 QtGui.QTextCursor.KeepAnchor)
1121 1132 if not cursor.hasSelection():
1122 1133 # Line deletion (remove continuation prompt)
1123 1134 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1124 1135 QtGui.QTextCursor.KeepAnchor)
1125 1136 cursor.movePosition(QtGui.QTextCursor.Right,
1126 1137 QtGui.QTextCursor.KeepAnchor,
1127 1138 len(self._continuation_prompt))
1128 1139 cursor.removeSelectedText()
1129 1140 intercepted = True
1130 1141
1131 1142 elif key == QtCore.Qt.Key_L:
1132 1143 self.prompt_to_top()
1133 1144 intercepted = True
1134 1145
1135 1146 elif key == QtCore.Qt.Key_O:
1136 1147 if self._page_control and self._page_control.isVisible():
1137 1148 self._page_control.setFocus()
1138 1149 intercepted = True
1139 1150
1140 1151 elif key == QtCore.Qt.Key_Y:
1141 1152 self.paste()
1142 1153 intercepted = True
1143 1154
1144 1155 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1145 1156 intercepted = True
1146 1157
1147 1158 elif key == QtCore.Qt.Key_Plus:
1148 1159 self.change_font_size(1)
1149 1160 intercepted = True
1150 1161
1151 1162 elif key == QtCore.Qt.Key_Minus:
1152 1163 self.change_font_size(-1)
1153 1164 intercepted = True
1154 1165
1155 1166 #------ Alt modifier ---------------------------------------------------
1156 1167
1157 1168 elif alt_down:
1158 1169 if key == QtCore.Qt.Key_B:
1159 1170 self._set_cursor(self._get_word_start_cursor(position))
1160 1171 intercepted = True
1161 1172
1162 1173 elif key == QtCore.Qt.Key_F:
1163 1174 self._set_cursor(self._get_word_end_cursor(position))
1164 1175 intercepted = True
1165 1176
1166 1177 elif key == QtCore.Qt.Key_Backspace:
1167 1178 cursor = self._get_word_start_cursor(position)
1168 1179 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1169 1180 cursor.removeSelectedText()
1170 1181 intercepted = True
1171 1182
1172 1183 elif key == QtCore.Qt.Key_D:
1173 1184 cursor = self._get_word_end_cursor(position)
1174 1185 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1175 1186 cursor.removeSelectedText()
1176 1187 intercepted = True
1177 1188
1178 1189 elif key == QtCore.Qt.Key_Delete:
1179 1190 intercepted = True
1180 1191
1181 1192 elif key == QtCore.Qt.Key_Greater:
1182 1193 self._control.moveCursor(QtGui.QTextCursor.End)
1183 1194 intercepted = True
1184 1195
1185 1196 elif key == QtCore.Qt.Key_Less:
1186 1197 self._control.setTextCursor(self._get_prompt_cursor())
1187 1198 intercepted = True
1188 1199
1189 1200 #------ No modifiers ---------------------------------------------------
1190 1201
1191 1202 else:
1192 1203 if shift_down:
1193 1204 anchormode=QtGui.QTextCursor.KeepAnchor
1194 1205 else:
1195 1206 anchormode=QtGui.QTextCursor.MoveAnchor
1196 1207
1197 1208 if key == QtCore.Qt.Key_Escape:
1198 1209 self._keyboard_quit()
1199 1210 intercepted = True
1200 1211
1201 1212 elif key == QtCore.Qt.Key_Up:
1202 1213 if self._reading or not self._up_pressed():
1203 1214 intercepted = True
1204 1215 else:
1205 1216 prompt_line = self._get_prompt_cursor().blockNumber()
1206 1217 intercepted = cursor.blockNumber() <= prompt_line
1207 1218
1208 1219 elif key == QtCore.Qt.Key_Down:
1209 1220 if self._reading or not self._down_pressed():
1210 1221 intercepted = True
1211 1222 else:
1212 1223 end_line = self._get_end_cursor().blockNumber()
1213 1224 intercepted = cursor.blockNumber() == end_line
1214 1225
1215 1226 elif key == QtCore.Qt.Key_Tab:
1216 1227 if not self._reading:
1217 1228 intercepted = not self._tab_pressed()
1218 1229
1219 1230 elif key == QtCore.Qt.Key_Left:
1220 1231
1221 1232 # Move to the previous line
1222 1233 line, col = cursor.blockNumber(), cursor.columnNumber()
1223 1234 if line > self._get_prompt_cursor().blockNumber() and \
1224 1235 col == len(self._continuation_prompt):
1225 1236 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1226 1237 mode=anchormode)
1227 1238 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1228 1239 mode=anchormode)
1229 1240 intercepted = True
1230 1241
1231 1242 # Regular left movement
1232 1243 else:
1233 1244 intercepted = not self._in_buffer(position - 1)
1234 1245
1235 1246 elif key == QtCore.Qt.Key_Right:
1236 1247 original_block_number = cursor.blockNumber()
1237 1248 cursor.movePosition(QtGui.QTextCursor.Right,
1238 1249 mode=anchormode)
1239 1250 if cursor.blockNumber() != original_block_number:
1240 1251 cursor.movePosition(QtGui.QTextCursor.Right,
1241 1252 n=len(self._continuation_prompt),
1242 1253 mode=anchormode)
1243 1254 self._set_cursor(cursor)
1244 1255 intercepted = True
1245 1256
1246 1257 elif key == QtCore.Qt.Key_Home:
1247 1258 start_line = cursor.blockNumber()
1248 1259 if start_line == self._get_prompt_cursor().blockNumber():
1249 1260 start_pos = self._prompt_pos
1250 1261 else:
1251 1262 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1252 1263 QtGui.QTextCursor.KeepAnchor)
1253 1264 start_pos = cursor.position()
1254 1265 start_pos += len(self._continuation_prompt)
1255 1266 cursor.setPosition(position)
1256 1267 if shift_down and self._in_buffer(position):
1257 1268 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1258 1269 else:
1259 1270 cursor.setPosition(start_pos)
1260 1271 self._set_cursor(cursor)
1261 1272 intercepted = True
1262 1273
1263 1274 elif key == QtCore.Qt.Key_Backspace:
1264 1275
1265 1276 # Line deletion (remove continuation prompt)
1266 1277 line, col = cursor.blockNumber(), cursor.columnNumber()
1267 1278 if not self._reading and \
1268 1279 col == len(self._continuation_prompt) and \
1269 1280 line > self._get_prompt_cursor().blockNumber():
1270 1281 cursor.beginEditBlock()
1271 1282 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1272 1283 QtGui.QTextCursor.KeepAnchor)
1273 1284 cursor.removeSelectedText()
1274 1285 cursor.deletePreviousChar()
1275 1286 cursor.endEditBlock()
1276 1287 intercepted = True
1277 1288
1278 1289 # Regular backwards deletion
1279 1290 else:
1280 1291 anchor = cursor.anchor()
1281 1292 if anchor == position:
1282 1293 intercepted = not self._in_buffer(position - 1)
1283 1294 else:
1284 1295 intercepted = not self._in_buffer(min(anchor, position))
1285 1296
1286 1297 elif key == QtCore.Qt.Key_Delete:
1287 1298
1288 1299 # Line deletion (remove continuation prompt)
1289 1300 if not self._reading and self._in_buffer(position) and \
1290 1301 cursor.atBlockEnd() and not cursor.hasSelection():
1291 1302 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1292 1303 QtGui.QTextCursor.KeepAnchor)
1293 1304 cursor.movePosition(QtGui.QTextCursor.Right,
1294 1305 QtGui.QTextCursor.KeepAnchor,
1295 1306 len(self._continuation_prompt))
1296 1307 cursor.removeSelectedText()
1297 1308 intercepted = True
1298 1309
1299 1310 # Regular forwards deletion:
1300 1311 else:
1301 1312 anchor = cursor.anchor()
1302 1313 intercepted = (not self._in_buffer(anchor) or
1303 1314 not self._in_buffer(position))
1304 1315
1305 1316 # Don't move the cursor if control is down to allow copy-paste using
1306 1317 # the keyboard in any part of the buffer.
1307 1318 if not ctrl_down:
1308 1319 self._keep_cursor_in_buffer()
1309 1320
1310 1321 return intercepted
1311 1322
1312 1323 def _event_filter_page_keypress(self, event):
1313 1324 """ Filter key events for the paging widget to create console-like
1314 1325 interface.
1315 1326 """
1316 1327 key = event.key()
1317 1328 ctrl_down = self._control_key_down(event.modifiers())
1318 1329 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1319 1330
1320 1331 if ctrl_down:
1321 1332 if key == QtCore.Qt.Key_O:
1322 1333 self._control.setFocus()
1323 1334 intercept = True
1324 1335
1325 1336 elif alt_down:
1326 1337 if key == QtCore.Qt.Key_Greater:
1327 1338 self._page_control.moveCursor(QtGui.QTextCursor.End)
1328 1339 intercepted = True
1329 1340
1330 1341 elif key == QtCore.Qt.Key_Less:
1331 1342 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1332 1343 intercepted = True
1333 1344
1334 1345 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1335 1346 if self._splitter:
1336 1347 self._page_control.hide()
1337 1348 else:
1338 1349 self.layout().setCurrentWidget(self._control)
1339 1350 return True
1340 1351
1341 1352 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1342 1353 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1343 1354 QtCore.Qt.Key_PageDown,
1344 1355 QtCore.Qt.NoModifier)
1345 1356 QtGui.qApp.sendEvent(self._page_control, new_event)
1346 1357 return True
1347 1358
1348 1359 elif key == QtCore.Qt.Key_Backspace:
1349 1360 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1350 1361 QtCore.Qt.Key_PageUp,
1351 1362 QtCore.Qt.NoModifier)
1352 1363 QtGui.qApp.sendEvent(self._page_control, new_event)
1353 1364 return True
1354 1365
1355 1366 return False
1356 1367
1357 1368 def _format_as_columns(self, items, separator=' '):
1358 1369 """ Transform a list of strings into a single string with columns.
1359 1370
1360 1371 Parameters
1361 1372 ----------
1362 1373 items : sequence of strings
1363 1374 The strings to process.
1364 1375
1365 1376 separator : str, optional [default is two spaces]
1366 1377 The string that separates columns.
1367 1378
1368 1379 Returns
1369 1380 -------
1370 1381 The formatted string.
1371 1382 """
1372 1383 # Note: this code is adapted from columnize 0.3.2.
1373 1384 # See http://code.google.com/p/pycolumnize/
1374 1385
1375 1386 # Calculate the number of characters available.
1376 1387 width = self._control.viewport().width()
1377 1388 char_width = QtGui.QFontMetrics(self.font).width(' ')
1378 1389 displaywidth = max(10, (width / char_width) - 1)
1379 1390
1380 1391 # Some degenerate cases.
1381 1392 size = len(items)
1382 1393 if size == 0:
1383 1394 return '\n'
1384 1395 elif size == 1:
1385 1396 return '%s\n' % items[0]
1386 1397
1387 1398 # Try every row count from 1 upwards
1388 1399 array_index = lambda nrows, row, col: nrows*col + row
1389 1400 for nrows in range(1, size):
1390 1401 ncols = (size + nrows - 1) // nrows
1391 1402 colwidths = []
1392 1403 totwidth = -len(separator)
1393 1404 for col in range(ncols):
1394 1405 # Get max column width for this column
1395 1406 colwidth = 0
1396 1407 for row in range(nrows):
1397 1408 i = array_index(nrows, row, col)
1398 1409 if i >= size: break
1399 1410 x = items[i]
1400 1411 colwidth = max(colwidth, len(x))
1401 1412 colwidths.append(colwidth)
1402 1413 totwidth += colwidth + len(separator)
1403 1414 if totwidth > displaywidth:
1404 1415 break
1405 1416 if totwidth <= displaywidth:
1406 1417 break
1407 1418
1408 1419 # The smallest number of rows computed and the max widths for each
1409 1420 # column has been obtained. Now we just have to format each of the rows.
1410 1421 string = ''
1411 1422 for row in range(nrows):
1412 1423 texts = []
1413 1424 for col in range(ncols):
1414 1425 i = row + nrows*col
1415 1426 if i >= size:
1416 1427 texts.append('')
1417 1428 else:
1418 1429 texts.append(items[i])
1419 1430 while texts and not texts[-1]:
1420 1431 del texts[-1]
1421 1432 for col in range(len(texts)):
1422 1433 texts[col] = texts[col].ljust(colwidths[col])
1423 1434 string += '%s\n' % separator.join(texts)
1424 1435 return string
1425 1436
1426 1437 def _get_block_plain_text(self, block):
1427 1438 """ Given a QTextBlock, return its unformatted text.
1428 1439 """
1429 1440 cursor = QtGui.QTextCursor(block)
1430 1441 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1431 1442 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1432 1443 QtGui.QTextCursor.KeepAnchor)
1433 return unicode(cursor.selection().toPlainText())
1444 return cursor.selection().toPlainText()
1434 1445
1435 1446 def _get_cursor(self):
1436 1447 """ Convenience method that returns a cursor for the current position.
1437 1448 """
1438 1449 return self._control.textCursor()
1439 1450
1440 1451 def _get_end_cursor(self):
1441 1452 """ Convenience method that returns a cursor for the last character.
1442 1453 """
1443 1454 cursor = self._control.textCursor()
1444 1455 cursor.movePosition(QtGui.QTextCursor.End)
1445 1456 return cursor
1446 1457
1447 1458 def _get_input_buffer_cursor_column(self):
1448 1459 """ Returns the column of the cursor in the input buffer, excluding the
1449 1460 contribution by the prompt, or -1 if there is no such column.
1450 1461 """
1451 1462 prompt = self._get_input_buffer_cursor_prompt()
1452 1463 if prompt is None:
1453 1464 return -1
1454 1465 else:
1455 1466 cursor = self._control.textCursor()
1456 1467 return cursor.columnNumber() - len(prompt)
1457 1468
1458 1469 def _get_input_buffer_cursor_line(self):
1459 1470 """ Returns the text of the line of the input buffer that contains the
1460 1471 cursor, or None if there is no such line.
1461 1472 """
1462 1473 prompt = self._get_input_buffer_cursor_prompt()
1463 1474 if prompt is None:
1464 1475 return None
1465 1476 else:
1466 1477 cursor = self._control.textCursor()
1467 1478 text = self._get_block_plain_text(cursor.block())
1468 1479 return text[len(prompt):]
1469 1480
1470 1481 def _get_input_buffer_cursor_prompt(self):
1471 1482 """ Returns the (plain text) prompt for line of the input buffer that
1472 1483 contains the cursor, or None if there is no such line.
1473 1484 """
1474 1485 if self._executing:
1475 1486 return None
1476 1487 cursor = self._control.textCursor()
1477 1488 if cursor.position() >= self._prompt_pos:
1478 1489 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1479 1490 return self._prompt
1480 1491 else:
1481 1492 return self._continuation_prompt
1482 1493 else:
1483 1494 return None
1484 1495
1485 1496 def _get_prompt_cursor(self):
1486 1497 """ Convenience method that returns a cursor for the prompt position.
1487 1498 """
1488 1499 cursor = self._control.textCursor()
1489 1500 cursor.setPosition(self._prompt_pos)
1490 1501 return cursor
1491 1502
1492 1503 def _get_selection_cursor(self, start, end):
1493 1504 """ Convenience method that returns a cursor with text selected between
1494 1505 the positions 'start' and 'end'.
1495 1506 """
1496 1507 cursor = self._control.textCursor()
1497 1508 cursor.setPosition(start)
1498 1509 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1499 1510 return cursor
1500 1511
1501 1512 def _get_word_start_cursor(self, position):
1502 1513 """ Find the start of the word to the left the given position. If a
1503 1514 sequence of non-word characters precedes the first word, skip over
1504 1515 them. (This emulates the behavior of bash, emacs, etc.)
1505 1516 """
1506 1517 document = self._control.document()
1507 1518 position -= 1
1508 1519 while position >= self._prompt_pos and \
1509 not document.characterAt(position).isLetterOrNumber():
1520 not is_letter_or_number(document.characterAt(position)):
1510 1521 position -= 1
1511 1522 while position >= self._prompt_pos and \
1512 document.characterAt(position).isLetterOrNumber():
1523 is_letter_or_number(document.characterAt(position)):
1513 1524 position -= 1
1514 1525 cursor = self._control.textCursor()
1515 1526 cursor.setPosition(position + 1)
1516 1527 return cursor
1517 1528
1518 1529 def _get_word_end_cursor(self, position):
1519 1530 """ Find the end of the word to the right the given position. If a
1520 1531 sequence of non-word characters precedes the first word, skip over
1521 1532 them. (This emulates the behavior of bash, emacs, etc.)
1522 1533 """
1523 1534 document = self._control.document()
1524 1535 end = self._get_end_cursor().position()
1525 1536 while position < end and \
1526 not document.characterAt(position).isLetterOrNumber():
1537 not is_letter_or_number(document.characterAt(position)):
1527 1538 position += 1
1528 1539 while position < end and \
1529 document.characterAt(position).isLetterOrNumber():
1540 is_letter_or_number(document.characterAt(position)):
1530 1541 position += 1
1531 1542 cursor = self._control.textCursor()
1532 1543 cursor.setPosition(position)
1533 1544 return cursor
1534 1545
1535 1546 def _insert_continuation_prompt(self, cursor):
1536 1547 """ Inserts new continuation prompt using the specified cursor.
1537 1548 """
1538 1549 if self._continuation_prompt_html is None:
1539 1550 self._insert_plain_text(cursor, self._continuation_prompt)
1540 1551 else:
1541 1552 self._continuation_prompt = self._insert_html_fetching_plain_text(
1542 1553 cursor, self._continuation_prompt_html)
1543 1554
1544 1555 def _insert_html(self, cursor, html):
1545 1556 """ Inserts HTML using the specified cursor in such a way that future
1546 1557 formatting is unaffected.
1547 1558 """
1548 1559 cursor.beginEditBlock()
1549 1560 cursor.insertHtml(html)
1550 1561
1551 1562 # After inserting HTML, the text document "remembers" it's in "html
1552 1563 # mode", which means that subsequent calls adding plain text will result
1553 1564 # in unwanted formatting, lost tab characters, etc. The following code
1554 1565 # hacks around this behavior, which I consider to be a bug in Qt, by
1555 1566 # (crudely) resetting the document's style state.
1556 1567 cursor.movePosition(QtGui.QTextCursor.Left,
1557 1568 QtGui.QTextCursor.KeepAnchor)
1558 1569 if cursor.selection().toPlainText() == ' ':
1559 1570 cursor.removeSelectedText()
1560 1571 else:
1561 1572 cursor.movePosition(QtGui.QTextCursor.Right)
1562 1573 cursor.insertText(' ', QtGui.QTextCharFormat())
1563 1574 cursor.endEditBlock()
1564 1575
1565 1576 def _insert_html_fetching_plain_text(self, cursor, html):
1566 1577 """ Inserts HTML using the specified cursor, then returns its plain text
1567 1578 version.
1568 1579 """
1569 1580 cursor.beginEditBlock()
1570 1581 cursor.removeSelectedText()
1571 1582
1572 1583 start = cursor.position()
1573 1584 self._insert_html(cursor, html)
1574 1585 end = cursor.position()
1575 1586 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1576 text = unicode(cursor.selection().toPlainText())
1587 text = cursor.selection().toPlainText()
1577 1588
1578 1589 cursor.setPosition(end)
1579 1590 cursor.endEditBlock()
1580 1591 return text
1581 1592
1582 1593 def _insert_plain_text(self, cursor, text):
1583 1594 """ Inserts plain text using the specified cursor, processing ANSI codes
1584 1595 if enabled.
1585 1596 """
1586 1597 cursor.beginEditBlock()
1587 1598 if self.ansi_codes:
1588 1599 for substring in self._ansi_processor.split_string(text):
1589 1600 for act in self._ansi_processor.actions:
1590 1601
1591 1602 # Unlike real terminal emulators, we don't distinguish
1592 1603 # between the screen and the scrollback buffer. A screen
1593 1604 # erase request clears everything.
1594 1605 if act.action == 'erase' and act.area == 'screen':
1595 1606 cursor.select(QtGui.QTextCursor.Document)
1596 1607 cursor.removeSelectedText()
1597 1608
1598 1609 # Simulate a form feed by scrolling just past the last line.
1599 1610 elif act.action == 'scroll' and act.unit == 'page':
1600 1611 cursor.insertText('\n')
1601 1612 cursor.endEditBlock()
1602 1613 self._set_top_cursor(cursor)
1603 1614 cursor.joinPreviousEditBlock()
1604 1615 cursor.deletePreviousChar()
1605 1616
1606 1617 format = self._ansi_processor.get_format()
1607 1618 cursor.insertText(substring, format)
1608 1619 else:
1609 1620 cursor.insertText(text)
1610 1621 cursor.endEditBlock()
1611 1622
1612 1623 def _insert_plain_text_into_buffer(self, cursor, text):
1613 1624 """ Inserts text into the input buffer using the specified cursor (which
1614 1625 must be in the input buffer), ensuring that continuation prompts are
1615 1626 inserted as necessary.
1616 1627 """
1617 lines = unicode(text).splitlines(True)
1628 lines = text.splitlines(True)
1618 1629 if lines:
1619 1630 cursor.beginEditBlock()
1620 1631 cursor.insertText(lines[0])
1621 1632 for line in lines[1:]:
1622 1633 if self._continuation_prompt_html is None:
1623 1634 cursor.insertText(self._continuation_prompt)
1624 1635 else:
1625 1636 self._continuation_prompt = \
1626 1637 self._insert_html_fetching_plain_text(
1627 1638 cursor, self._continuation_prompt_html)
1628 1639 cursor.insertText(line)
1629 1640 cursor.endEditBlock()
1630 1641
1631 1642 def _in_buffer(self, position=None):
1632 1643 """ Returns whether the current cursor (or, if specified, a position) is
1633 1644 inside the editing region.
1634 1645 """
1635 1646 cursor = self._control.textCursor()
1636 1647 if position is None:
1637 1648 position = cursor.position()
1638 1649 else:
1639 1650 cursor.setPosition(position)
1640 1651 line = cursor.blockNumber()
1641 1652 prompt_line = self._get_prompt_cursor().blockNumber()
1642 1653 if line == prompt_line:
1643 1654 return position >= self._prompt_pos
1644 1655 elif line > prompt_line:
1645 1656 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1646 1657 prompt_pos = cursor.position() + len(self._continuation_prompt)
1647 1658 return position >= prompt_pos
1648 1659 return False
1649 1660
1650 1661 def _keep_cursor_in_buffer(self):
1651 1662 """ Ensures that the cursor is inside the editing region. Returns
1652 1663 whether the cursor was moved.
1653 1664 """
1654 1665 moved = not self._in_buffer()
1655 1666 if moved:
1656 1667 cursor = self._control.textCursor()
1657 1668 cursor.movePosition(QtGui.QTextCursor.End)
1658 1669 self._control.setTextCursor(cursor)
1659 1670 return moved
1660 1671
1661 1672 def _keyboard_quit(self):
1662 1673 """ Cancels the current editing task ala Ctrl-G in Emacs.
1663 1674 """
1664 1675 if self._text_completing_pos:
1665 1676 self._cancel_text_completion()
1666 1677 else:
1667 1678 self.input_buffer = ''
1668 1679
1669 1680 def _page(self, text, html=False):
1670 1681 """ Displays text using the pager if it exceeds the height of the
1671 1682 viewport.
1672 1683
1673 1684 Parameters:
1674 1685 -----------
1675 1686 html : bool, optional (default False)
1676 1687 If set, the text will be interpreted as HTML instead of plain text.
1677 1688 """
1678 1689 line_height = QtGui.QFontMetrics(self.font).height()
1679 1690 minlines = self._control.viewport().height() / line_height
1680 1691 if self.paging != 'none' and \
1681 1692 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1682 1693 if self.paging == 'custom':
1683 1694 self.custom_page_requested.emit(text)
1684 1695 else:
1685 1696 self._page_control.clear()
1686 1697 cursor = self._page_control.textCursor()
1687 1698 if html:
1688 1699 self._insert_html(cursor, text)
1689 1700 else:
1690 1701 self._insert_plain_text(cursor, text)
1691 1702 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1692 1703
1693 1704 self._page_control.viewport().resize(self._control.size())
1694 1705 if self._splitter:
1695 1706 self._page_control.show()
1696 1707 self._page_control.setFocus()
1697 1708 else:
1698 1709 self.layout().setCurrentWidget(self._page_control)
1699 1710 elif html:
1700 1711 self._append_plain_html(text)
1701 1712 else:
1702 1713 self._append_plain_text(text)
1703 1714
1704 1715 def _prompt_finished(self):
1705 1716 """ Called immediately after a prompt is finished, i.e. when some input
1706 1717 will be processed and a new prompt displayed.
1707 1718 """
1708 1719 # Flush all state from the input splitter so the next round of
1709 1720 # reading input starts with a clean buffer.
1710 1721 self._input_splitter.reset()
1711 1722
1712 1723 self._control.setReadOnly(True)
1713 1724 self._prompt_finished_hook()
1714 1725
1715 1726 def _prompt_started(self):
1716 1727 """ Called immediately after a new prompt is displayed.
1717 1728 """
1718 1729 # Temporarily disable the maximum block count to permit undo/redo and
1719 1730 # to ensure that the prompt position does not change due to truncation.
1720 1731 self._control.document().setMaximumBlockCount(0)
1721 1732 self._control.setUndoRedoEnabled(True)
1722 1733
1723 1734 self._control.setReadOnly(False)
1724 1735 self._control.moveCursor(QtGui.QTextCursor.End)
1725 1736 self._executing = False
1726 1737 self._prompt_started_hook()
1727 1738
1728 1739 def _readline(self, prompt='', callback=None):
1729 1740 """ Reads one line of input from the user.
1730 1741
1731 1742 Parameters
1732 1743 ----------
1733 1744 prompt : str, optional
1734 1745 The prompt to print before reading the line.
1735 1746
1736 1747 callback : callable, optional
1737 1748 A callback to execute with the read line. If not specified, input is
1738 1749 read *synchronously* and this method does not return until it has
1739 1750 been read.
1740 1751
1741 1752 Returns
1742 1753 -------
1743 1754 If a callback is specified, returns nothing. Otherwise, returns the
1744 1755 input string with the trailing newline stripped.
1745 1756 """
1746 1757 if self._reading:
1747 1758 raise RuntimeError('Cannot read a line. Widget is already reading.')
1748 1759
1749 1760 if not callback and not self.isVisible():
1750 1761 # If the user cannot see the widget, this function cannot return.
1751 1762 raise RuntimeError('Cannot synchronously read a line if the widget '
1752 1763 'is not visible!')
1753 1764
1754 1765 self._reading = True
1755 1766 self._show_prompt(prompt, newline=False)
1756 1767
1757 1768 if callback is None:
1758 1769 self._reading_callback = None
1759 1770 while self._reading:
1760 1771 QtCore.QCoreApplication.processEvents()
1761 1772 return self.input_buffer.rstrip('\n')
1762 1773
1763 1774 else:
1764 1775 self._reading_callback = lambda: \
1765 1776 callback(self.input_buffer.rstrip('\n'))
1766 1777
1767 1778 def _set_continuation_prompt(self, prompt, html=False):
1768 1779 """ Sets the continuation prompt.
1769 1780
1770 1781 Parameters
1771 1782 ----------
1772 1783 prompt : str
1773 1784 The prompt to show when more input is needed.
1774 1785
1775 1786 html : bool, optional (default False)
1776 1787 If set, the prompt will be inserted as formatted HTML. Otherwise,
1777 1788 the prompt will be treated as plain text, though ANSI color codes
1778 1789 will be handled.
1779 1790 """
1780 1791 if html:
1781 1792 self._continuation_prompt_html = prompt
1782 1793 else:
1783 1794 self._continuation_prompt = prompt
1784 1795 self._continuation_prompt_html = None
1785 1796
1786 1797 def _set_cursor(self, cursor):
1787 1798 """ Convenience method to set the current cursor.
1788 1799 """
1789 1800 self._control.setTextCursor(cursor)
1790 1801
1791 1802 def _set_top_cursor(self, cursor):
1792 1803 """ Scrolls the viewport so that the specified cursor is at the top.
1793 1804 """
1794 1805 scrollbar = self._control.verticalScrollBar()
1795 1806 scrollbar.setValue(scrollbar.maximum())
1796 1807 original_cursor = self._control.textCursor()
1797 1808 self._control.setTextCursor(cursor)
1798 1809 self._control.ensureCursorVisible()
1799 1810 self._control.setTextCursor(original_cursor)
1800 1811
1801 1812 def _show_prompt(self, prompt=None, html=False, newline=True):
1802 1813 """ Writes a new prompt at the end of the buffer.
1803 1814
1804 1815 Parameters
1805 1816 ----------
1806 1817 prompt : str, optional
1807 1818 The prompt to show. If not specified, the previous prompt is used.
1808 1819
1809 1820 html : bool, optional (default False)
1810 1821 Only relevant when a prompt is specified. If set, the prompt will
1811 1822 be inserted as formatted HTML. Otherwise, the prompt will be treated
1812 1823 as plain text, though ANSI color codes will be handled.
1813 1824
1814 1825 newline : bool, optional (default True)
1815 1826 If set, a new line will be written before showing the prompt if
1816 1827 there is not already a newline at the end of the buffer.
1817 1828 """
1818 1829 # Insert a preliminary newline, if necessary.
1819 1830 if newline:
1820 1831 cursor = self._get_end_cursor()
1821 1832 if cursor.position() > 0:
1822 1833 cursor.movePosition(QtGui.QTextCursor.Left,
1823 1834 QtGui.QTextCursor.KeepAnchor)
1824 if unicode(cursor.selection().toPlainText()) != '\n':
1835 if cursor.selection().toPlainText() != '\n':
1825 1836 self._append_plain_text('\n')
1826 1837
1827 1838 # Write the prompt.
1828 1839 self._append_plain_text(self._prompt_sep)
1829 1840 if prompt is None:
1830 1841 if self._prompt_html is None:
1831 1842 self._append_plain_text(self._prompt)
1832 1843 else:
1833 1844 self._append_html(self._prompt_html)
1834 1845 else:
1835 1846 if html:
1836 1847 self._prompt = self._append_html_fetching_plain_text(prompt)
1837 1848 self._prompt_html = prompt
1838 1849 else:
1839 1850 self._append_plain_text(prompt)
1840 1851 self._prompt = prompt
1841 1852 self._prompt_html = None
1842 1853
1843 1854 self._prompt_pos = self._get_end_cursor().position()
1844 1855 self._prompt_started()
1845 1856
1846 1857 #------ Signal handlers ----------------------------------------------------
1847 1858
1848 1859 def _adjust_scrollbars(self):
1849 1860 """ Expands the vertical scrollbar beyond the range set by Qt.
1850 1861 """
1851 1862 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1852 1863 # and qtextedit.cpp.
1853 1864 document = self._control.document()
1854 1865 scrollbar = self._control.verticalScrollBar()
1855 1866 viewport_height = self._control.viewport().height()
1856 1867 if isinstance(self._control, QtGui.QPlainTextEdit):
1857 1868 maximum = max(0, document.lineCount() - 1)
1858 1869 step = viewport_height / self._control.fontMetrics().lineSpacing()
1859 1870 else:
1860 1871 # QTextEdit does not do line-based layout and blocks will not in
1861 1872 # general have the same height. Therefore it does not make sense to
1862 1873 # attempt to scroll in line height increments.
1863 1874 maximum = document.size().height()
1864 1875 step = viewport_height
1865 1876 diff = maximum - scrollbar.maximum()
1866 1877 scrollbar.setRange(0, maximum)
1867 1878 scrollbar.setPageStep(step)
1868 1879 # Compensate for undesirable scrolling that occurs automatically due to
1869 1880 # maximumBlockCount() text truncation.
1870 1881 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1871 1882 scrollbar.setValue(scrollbar.value() + diff)
1872 1883
1873 1884 def _cursor_position_changed(self):
1874 1885 """ Clears the temporary buffer based on the cursor position.
1875 1886 """
1876 1887 if self._text_completing_pos:
1877 1888 document = self._control.document()
1878 1889 if self._text_completing_pos < document.characterCount():
1879 1890 cursor = self._control.textCursor()
1880 1891 pos = cursor.position()
1881 1892 text_cursor = self._control.textCursor()
1882 1893 text_cursor.setPosition(self._text_completing_pos)
1883 1894 if pos < self._text_completing_pos or \
1884 1895 cursor.blockNumber() > text_cursor.blockNumber():
1885 1896 self._clear_temporary_buffer()
1886 1897 self._text_completing_pos = 0
1887 1898 else:
1888 1899 self._clear_temporary_buffer()
1889 1900 self._text_completing_pos = 0
1890 1901
1891 1902 def _custom_context_menu_requested(self, pos):
1892 1903 """ Shows a context menu at the given QPoint (in widget coordinates).
1893 1904 """
1894 1905 menu = self._context_menu_make(pos)
1895 1906 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,598 +1,598 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 from PyQt4 import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11 11
12 12 # Local imports
13 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 14 from IPython.core.oinspect import call_tip
15 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 16 from IPython.utils.traitlets import Bool
17 17 from bracket_matcher import BracketMatcher
18 18 from call_tip_widget import CallTipWidget
19 19 from completion_lexer import CompletionLexer
20 20 from history_console_widget import HistoryConsoleWidget
21 21 from pygments_highlighter import PygmentsHighlighter
22 22
23 23
24 24 class FrontendHighlighter(PygmentsHighlighter):
25 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 26 prompts.
27 27 """
28 28
29 29 def __init__(self, frontend):
30 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 31 self._current_offset = 0
32 32 self._frontend = frontend
33 33 self.highlighting_on = False
34 34
35 def highlightBlock(self, qstring):
35 def highlightBlock(self, string):
36 36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 37 """
38 38 if not self.highlighting_on:
39 39 return
40 40
41 # The input to this function is unicode string that may contain
41 # The input to this function is a unicode string that may contain
42 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 43 # the string as plain text so we can compare it.
44 44 current_block = self.currentBlock()
45 45 string = self._frontend._get_block_plain_text(current_block)
46 46
47 47 # Decide whether to check for the regular or continuation prompt.
48 48 if current_block.contains(self._frontend._prompt_pos):
49 49 prompt = self._frontend._prompt
50 50 else:
51 51 prompt = self._frontend._continuation_prompt
52 52
53 53 # Don't highlight the part of the string that contains the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 qstring.remove(0, len(prompt))
56 string = string[len(prompt):]
57 57 else:
58 58 self._current_offset = 0
59 59
60 PygmentsHighlighter.highlightBlock(self, qstring)
60 PygmentsHighlighter.highlightBlock(self, string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 PygmentsHighlighter.setFormat(self, start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # An option and corresponding signal for overriding the default kernel
82 82 # interrupt behavior.
83 83 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.pyqtSignal()
84 custom_interrupt_requested = QtCore.Signal()
85 85
86 86 # An option and corresponding signals for overriding the default kernel
87 87 # restart behavior.
88 88 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.pyqtSignal(float)
90 custom_restart_requested = QtCore.pyqtSignal()
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
91 91
92 92 # Emitted when an 'execute_reply' has been received from the kernel and
93 93 # processed by the FrontendWidget.
94 executed = QtCore.pyqtSignal(object)
94 executed = QtCore.Signal(object)
95 95
96 96 # Emitted when an exit request has been received from the kernel.
97 exit_requested = QtCore.pyqtSignal()
97 exit_requested = QtCore.Signal()
98 98
99 99 # Protected class variables.
100 100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 103 _input_splitter_class = InputSplitter
104 104 _local_kernel = False
105 105
106 106 #---------------------------------------------------------------------------
107 107 # 'object' interface
108 108 #---------------------------------------------------------------------------
109 109
110 110 def __init__(self, *args, **kw):
111 111 super(FrontendWidget, self).__init__(*args, **kw)
112 112
113 113 # FrontendWidget protected variables.
114 114 self._bracket_matcher = BracketMatcher(self._control)
115 115 self._call_tip_widget = CallTipWidget(self._control)
116 116 self._completion_lexer = CompletionLexer(PythonLexer())
117 117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
118 118 self._hidden = False
119 119 self._highlighter = FrontendHighlighter(self)
120 120 self._input_splitter = self._input_splitter_class(input_mode='cell')
121 121 self._kernel_manager = None
122 122 self._request_info = {}
123 123
124 124 # Configure the ConsoleWidget.
125 125 self.tab_width = 4
126 126 self._set_continuation_prompt('... ')
127 127
128 128 # Configure the CallTipWidget.
129 129 self._call_tip_widget.setFont(self.font)
130 130 self.font_changed.connect(self._call_tip_widget.setFont)
131 131
132 132 # Configure actions.
133 133 action = self._copy_raw_action
134 134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
135 135 action.setEnabled(False)
136 136 action.setShortcut(QtGui.QKeySequence(key))
137 137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
138 138 action.triggered.connect(self.copy_raw)
139 139 self.copy_available.connect(action.setEnabled)
140 140 self.addAction(action)
141 141
142 142 # Connect signal handlers.
143 143 document = self._control.document()
144 144 document.contentsChange.connect(self._document_contents_change)
145 145
146 146 # set flag for whether we are connected via localhost
147 147 self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel)
148 148
149 149 #---------------------------------------------------------------------------
150 150 # 'ConsoleWidget' public interface
151 151 #---------------------------------------------------------------------------
152 152
153 153 def copy(self):
154 154 """ Copy the currently selected text to the clipboard, removing prompts.
155 155 """
156 text = unicode(self._control.textCursor().selection().toPlainText())
156 text = self._control.textCursor().selection().toPlainText()
157 157 if text:
158 158 lines = map(transform_classic_prompt, text.splitlines())
159 159 text = '\n'.join(lines)
160 160 QtGui.QApplication.clipboard().setText(text)
161 161
162 162 #---------------------------------------------------------------------------
163 163 # 'ConsoleWidget' abstract interface
164 164 #---------------------------------------------------------------------------
165 165
166 166 def _is_complete(self, source, interactive):
167 167 """ Returns whether 'source' can be completely processed and a new
168 168 prompt created. When triggered by an Enter/Return key press,
169 169 'interactive' is True; otherwise, it is False.
170 170 """
171 171 complete = self._input_splitter.push(source)
172 172 if interactive:
173 173 complete = not self._input_splitter.push_accepts_more()
174 174 return complete
175 175
176 176 def _execute(self, source, hidden):
177 177 """ Execute 'source'. If 'hidden', do not show any output.
178 178
179 179 See parent class :meth:`execute` docstring for full details.
180 180 """
181 181 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
182 182 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
183 183 self._hidden = hidden
184 184
185 185 def _prompt_started_hook(self):
186 186 """ Called immediately after a new prompt is displayed.
187 187 """
188 188 if not self._reading:
189 189 self._highlighter.highlighting_on = True
190 190
191 191 def _prompt_finished_hook(self):
192 192 """ Called immediately after a prompt is finished, i.e. when some input
193 193 will be processed and a new prompt displayed.
194 194 """
195 195 if not self._reading:
196 196 self._highlighter.highlighting_on = False
197 197
198 198 def _tab_pressed(self):
199 199 """ Called when the tab key is pressed. Returns whether to continue
200 200 processing the event.
201 201 """
202 202 # Perform tab completion if:
203 203 # 1) The cursor is in the input buffer.
204 204 # 2) There is a non-whitespace character before the cursor.
205 205 text = self._get_input_buffer_cursor_line()
206 206 if text is None:
207 207 return False
208 208 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
209 209 if complete:
210 210 self._complete()
211 211 return not complete
212 212
213 213 #---------------------------------------------------------------------------
214 214 # 'ConsoleWidget' protected interface
215 215 #---------------------------------------------------------------------------
216 216
217 217 def _context_menu_make(self, pos):
218 218 """ Reimplemented to add an action for raw copy.
219 219 """
220 220 menu = super(FrontendWidget, self)._context_menu_make(pos)
221 221 for before_action in menu.actions():
222 222 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
223 223 QtGui.QKeySequence.ExactMatch:
224 224 menu.insertAction(before_action, self._copy_raw_action)
225 225 break
226 226 return menu
227 227
228 228 def _event_filter_console_keypress(self, event):
229 229 """ Reimplemented for execution interruption and smart backspace.
230 230 """
231 231 key = event.key()
232 232 if self._control_key_down(event.modifiers(), include_command=False):
233 233
234 234 if key == QtCore.Qt.Key_C and self._executing:
235 235 self.interrupt_kernel()
236 236 return True
237 237
238 238 elif key == QtCore.Qt.Key_Period:
239 239 message = 'Are you sure you want to restart the kernel?'
240 240 self.restart_kernel(message, now=False)
241 241 return True
242 242
243 243 elif not event.modifiers() & QtCore.Qt.AltModifier:
244 244
245 245 # Smart backspace: remove four characters in one backspace if:
246 246 # 1) everything left of the cursor is whitespace
247 247 # 2) the four characters immediately left of the cursor are spaces
248 248 if key == QtCore.Qt.Key_Backspace:
249 249 col = self._get_input_buffer_cursor_column()
250 250 cursor = self._control.textCursor()
251 251 if col > 3 and not cursor.hasSelection():
252 252 text = self._get_input_buffer_cursor_line()[:col]
253 253 if text.endswith(' ') and not text.strip():
254 254 cursor.movePosition(QtGui.QTextCursor.Left,
255 255 QtGui.QTextCursor.KeepAnchor, 4)
256 256 cursor.removeSelectedText()
257 257 return True
258 258
259 259 return super(FrontendWidget, self)._event_filter_console_keypress(event)
260 260
261 261 def _insert_continuation_prompt(self, cursor):
262 262 """ Reimplemented for auto-indentation.
263 263 """
264 264 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
265 265 cursor.insertText(' ' * self._input_splitter.indent_spaces)
266 266
267 267 #---------------------------------------------------------------------------
268 268 # 'BaseFrontendMixin' abstract interface
269 269 #---------------------------------------------------------------------------
270 270
271 271 def _handle_complete_reply(self, rep):
272 272 """ Handle replies for tab completion.
273 273 """
274 274 cursor = self._get_cursor()
275 275 info = self._request_info.get('complete')
276 276 if info and info.id == rep['parent_header']['msg_id'] and \
277 277 info.pos == cursor.position():
278 278 text = '.'.join(self._get_context())
279 279 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
280 280 self._complete_with_items(cursor, rep['content']['matches'])
281 281
282 282 def _handle_execute_reply(self, msg):
283 283 """ Handles replies for code execution.
284 284 """
285 285 info = self._request_info.get('execute')
286 286 if info and info.id == msg['parent_header']['msg_id'] and \
287 287 info.kind == 'user' and not self._hidden:
288 288 # Make sure that all output from the SUB channel has been processed
289 289 # before writing a new prompt.
290 290 self.kernel_manager.sub_channel.flush()
291 291
292 292 # Reset the ANSI style information to prevent bad text in stdout
293 293 # from messing up our colors. We're not a true terminal so we're
294 294 # allowed to do this.
295 295 if self.ansi_codes:
296 296 self._ansi_processor.reset_sgr()
297 297
298 298 content = msg['content']
299 299 status = content['status']
300 300 if status == 'ok':
301 301 self._process_execute_ok(msg)
302 302 elif status == 'error':
303 303 self._process_execute_error(msg)
304 304 elif status == 'abort':
305 305 self._process_execute_abort(msg)
306 306
307 307 self._show_interpreter_prompt_for_reply(msg)
308 308 self.executed.emit(msg)
309 309
310 310 def _handle_input_request(self, msg):
311 311 """ Handle requests for raw_input.
312 312 """
313 313 if self._hidden:
314 314 raise RuntimeError('Request for raw input during hidden execution.')
315 315
316 316 # Make sure that all output from the SUB channel has been processed
317 317 # before entering readline mode.
318 318 self.kernel_manager.sub_channel.flush()
319 319
320 320 def callback(line):
321 321 self.kernel_manager.rep_channel.input(line)
322 322 self._readline(msg['content']['prompt'], callback=callback)
323 323
324 324 def _handle_kernel_died(self, since_last_heartbeat):
325 325 """ Handle the kernel's death by asking if the user wants to restart.
326 326 """
327 327 if self.custom_restart:
328 328 self.custom_restart_kernel_died.emit(since_last_heartbeat)
329 329 else:
330 330 message = 'The kernel heartbeat has been inactive for %.2f ' \
331 331 'seconds. Do you want to restart the kernel? You may ' \
332 332 'first want to check the network connection.' % \
333 333 since_last_heartbeat
334 334 self.restart_kernel(message, now=True)
335 335
336 336 def _handle_object_info_reply(self, rep):
337 337 """ Handle replies for call tips.
338 338 """
339 339 cursor = self._get_cursor()
340 340 info = self._request_info.get('call_tip')
341 341 if info and info.id == rep['parent_header']['msg_id'] and \
342 342 info.pos == cursor.position():
343 343 # Get the information for a call tip. For now we format the call
344 344 # line as string, later we can pass False to format_call and
345 345 # syntax-highlight it ourselves for nicer formatting in the
346 346 # calltip.
347 347 call_info, doc = call_tip(rep['content'], format_call=True)
348 348 if call_info or doc:
349 349 self._call_tip_widget.show_call_info(call_info, doc)
350 350
351 351 def _handle_pyout(self, msg):
352 352 """ Handle display hook output.
353 353 """
354 354 if not self._hidden and self._is_from_this_session(msg):
355 355 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
356 356
357 357 def _handle_stream(self, msg):
358 358 """ Handle stdout, stderr, and stdin.
359 359 """
360 360 if not self._hidden and self._is_from_this_session(msg):
361 361 # Most consoles treat tabs as being 8 space characters. Convert tabs
362 362 # to spaces so that output looks as expected regardless of this
363 363 # widget's tab width.
364 364 text = msg['content']['data'].expandtabs(8)
365 365
366 366 self._append_plain_text(text)
367 367 self._control.moveCursor(QtGui.QTextCursor.End)
368 368
369 369 def _handle_shutdown_reply(self, msg):
370 370 """ Handle shutdown signal, only if from other console.
371 371 """
372 372 if not self._hidden and not self._is_from_this_session(msg):
373 373 if self._local_kernel:
374 374 if not msg['content']['restart']:
375 375 sys.exit(0)
376 376 else:
377 377 # we just got notified of a restart!
378 378 time.sleep(0.25) # wait 1/4 sec to reset
379 379 # lest the request for a new prompt
380 380 # goes to the old kernel
381 381 self.reset()
382 382 else: # remote kernel, prompt on Kernel shutdown/reset
383 383 title = self.window().windowTitle()
384 384 if not msg['content']['restart']:
385 385 reply = QtGui.QMessageBox.question(self, title,
386 386 "Kernel has been shutdown permanently. Close the Console?",
387 387 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
388 388 if reply == QtGui.QMessageBox.Yes:
389 389 sys.exit(0)
390 390 else:
391 391 reply = QtGui.QMessageBox.question(self, title,
392 392 "Kernel has been reset. Clear the Console?",
393 393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
394 394 if reply == QtGui.QMessageBox.Yes:
395 395 time.sleep(0.25) # wait 1/4 sec to reset
396 396 # lest the request for a new prompt
397 397 # goes to the old kernel
398 398 self.reset()
399 399
400 400 def _started_channels(self):
401 401 """ Called when the KernelManager channels have started listening or
402 402 when the frontend is assigned an already listening KernelManager.
403 403 """
404 404 self.reset()
405 405
406 406 #---------------------------------------------------------------------------
407 407 # 'FrontendWidget' public interface
408 408 #---------------------------------------------------------------------------
409 409
410 410 def copy_raw(self):
411 411 """ Copy the currently selected text to the clipboard without attempting
412 412 to remove prompts or otherwise alter the text.
413 413 """
414 414 self._control.copy()
415 415
416 416 def execute_file(self, path, hidden=False):
417 417 """ Attempts to execute file with 'path'. If 'hidden', no output is
418 418 shown.
419 419 """
420 420 self.execute('execfile("%s")' % path, hidden=hidden)
421 421
422 422 def interrupt_kernel(self):
423 423 """ Attempts to interrupt the running kernel.
424 424 """
425 425 if self.custom_interrupt:
426 426 self.custom_interrupt_requested.emit()
427 427 elif self.kernel_manager.has_kernel:
428 428 self.kernel_manager.interrupt_kernel()
429 429 else:
430 430 self._append_plain_text('Kernel process is either remote or '
431 431 'unspecified. Cannot interrupt.\n')
432 432
433 433 def reset(self):
434 434 """ Resets the widget to its initial state. Similar to ``clear``, but
435 435 also re-writes the banner and aborts execution if necessary.
436 436 """
437 437 if self._executing:
438 438 self._executing = False
439 439 self._request_info['execute'] = None
440 440 self._reading = False
441 441 self._highlighter.highlighting_on = False
442 442
443 443 self._control.clear()
444 444 self._append_plain_text(self._get_banner())
445 445 self._show_interpreter_prompt()
446 446
447 447 def restart_kernel(self, message, now=False):
448 448 """ Attempts to restart the running kernel.
449 449 """
450 450 # FIXME: now should be configurable via a checkbox in the dialog. Right
451 451 # now at least the heartbeat path sets it to True and the manual restart
452 452 # to False. But those should just be the pre-selected states of a
453 453 # checkbox that the user could override if so desired. But I don't know
454 454 # enough Qt to go implementing the checkbox now.
455 455
456 456 if self.custom_restart:
457 457 self.custom_restart_requested.emit()
458 458
459 459 elif self.kernel_manager.has_kernel:
460 460 # Pause the heart beat channel to prevent further warnings.
461 461 self.kernel_manager.hb_channel.pause()
462 462
463 463 # Prompt the user to restart the kernel. Un-pause the heartbeat if
464 464 # they decline. (If they accept, the heartbeat will be un-paused
465 465 # automatically when the kernel is restarted.)
466 466 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
467 467 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
468 468 message, buttons)
469 469 if result == QtGui.QMessageBox.Yes:
470 470 try:
471 471 self.kernel_manager.restart_kernel(now=now)
472 472 except RuntimeError:
473 473 self._append_plain_text('Kernel started externally. '
474 474 'Cannot restart.\n')
475 475 else:
476 476 self.reset()
477 477 else:
478 478 self.kernel_manager.hb_channel.unpause()
479 479
480 480 else:
481 481 self._append_plain_text('Kernel process is either remote or '
482 482 'unspecified. Cannot restart.\n')
483 483
484 484 #---------------------------------------------------------------------------
485 485 # 'FrontendWidget' protected interface
486 486 #---------------------------------------------------------------------------
487 487
488 488 def _call_tip(self):
489 489 """ Shows a call tip, if appropriate, at the current cursor location.
490 490 """
491 491 # Decide if it makes sense to show a call tip
492 492 cursor = self._get_cursor()
493 493 cursor.movePosition(QtGui.QTextCursor.Left)
494 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
494 if cursor.document().characterAt(cursor.position()) != '(':
495 495 return False
496 496 context = self._get_context(cursor)
497 497 if not context:
498 498 return False
499 499
500 500 # Send the metadata request to the kernel
501 501 name = '.'.join(context)
502 502 msg_id = self.kernel_manager.xreq_channel.object_info(name)
503 503 pos = self._get_cursor().position()
504 504 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
505 505 return True
506 506
507 507 def _complete(self):
508 508 """ Performs completion at the current cursor location.
509 509 """
510 510 context = self._get_context()
511 511 if context:
512 512 # Send the completion request to the kernel
513 513 msg_id = self.kernel_manager.xreq_channel.complete(
514 514 '.'.join(context), # text
515 515 self._get_input_buffer_cursor_line(), # line
516 516 self._get_input_buffer_cursor_column(), # cursor_pos
517 517 self.input_buffer) # block
518 518 pos = self._get_cursor().position()
519 519 info = self._CompletionRequest(msg_id, pos)
520 520 self._request_info['complete'] = info
521 521
522 522 def _get_banner(self):
523 523 """ Gets a banner to display at the beginning of a session.
524 524 """
525 525 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
526 526 '"license" for more information.'
527 527 return banner % (sys.version, sys.platform)
528 528
529 529 def _get_context(self, cursor=None):
530 530 """ Gets the context for the specified cursor (or the current cursor
531 531 if none is specified).
532 532 """
533 533 if cursor is None:
534 534 cursor = self._get_cursor()
535 535 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
536 536 QtGui.QTextCursor.KeepAnchor)
537 text = unicode(cursor.selection().toPlainText())
537 text = cursor.selection().toPlainText()
538 538 return self._completion_lexer.get_context(text)
539 539
540 540 def _process_execute_abort(self, msg):
541 541 """ Process a reply for an aborted execution request.
542 542 """
543 543 self._append_plain_text("ERROR: execution aborted\n")
544 544
545 545 def _process_execute_error(self, msg):
546 546 """ Process a reply for an execution request that resulted in an error.
547 547 """
548 548 content = msg['content']
549 549 # If a SystemExit is passed along, this means exit() was called - also
550 550 # all the ipython %exit magic syntax of '-k' to be used to keep
551 551 # the kernel running
552 552 if content['ename']=='SystemExit':
553 553 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
554 554 self._keep_kernel_on_exit = keepkernel
555 555 self.exit_requested.emit()
556 556 else:
557 557 traceback = ''.join(content['traceback'])
558 558 self._append_plain_text(traceback)
559 559
560 560 def _process_execute_ok(self, msg):
561 561 """ Process a reply for a successful execution equest.
562 562 """
563 563 payload = msg['content']['payload']
564 564 for item in payload:
565 565 if not self._process_execute_payload(item):
566 566 warning = 'Warning: received unknown payload of type %s'
567 567 print(warning % repr(item['source']))
568 568
569 569 def _process_execute_payload(self, item):
570 570 """ Process a single payload item from the list of payload items in an
571 571 execution reply. Returns whether the payload was handled.
572 572 """
573 573 # The basic FrontendWidget doesn't handle payloads, as they are a
574 574 # mechanism for going beyond the standard Python interpreter model.
575 575 return False
576 576
577 577 def _show_interpreter_prompt(self):
578 578 """ Shows a prompt for the interpreter.
579 579 """
580 580 self._show_prompt('>>> ')
581 581
582 582 def _show_interpreter_prompt_for_reply(self, msg):
583 583 """ Shows a prompt for the interpreter given an 'execute_reply' message.
584 584 """
585 585 self._show_interpreter_prompt()
586 586
587 587 #------ Signal handlers ----------------------------------------------------
588 588
589 589 def _document_contents_change(self, position, removed, added):
590 590 """ Called whenever the document's content changes. Display a call tip
591 591 if appropriate.
592 592 """
593 593 # Calculate where the cursor should be *after* the change:
594 594 position += added
595 595
596 596 document = self._control.document()
597 597 if position == self._get_cursor().position():
598 598 self._call_tip()
@@ -1,163 +1,163 b''
1 1 # System library imports
2 from PyQt4 import QtGui
2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from console_widget import ConsoleWidget
6 6
7 7
8 8 class HistoryConsoleWidget(ConsoleWidget):
9 9 """ A ConsoleWidget that keeps a history of the commands that have been
10 10 executed and provides a readline-esque interface to this history.
11 11 """
12 12
13 13 #---------------------------------------------------------------------------
14 14 # 'object' interface
15 15 #---------------------------------------------------------------------------
16 16
17 17 def __init__(self, *args, **kw):
18 18 super(HistoryConsoleWidget, self).__init__(*args, **kw)
19 19
20 20 # HistoryConsoleWidget protected variables.
21 21 self._history = []
22 22 self._history_index = 0
23 23 self._history_prefix = ''
24 24
25 25 #---------------------------------------------------------------------------
26 26 # 'ConsoleWidget' public interface
27 27 #---------------------------------------------------------------------------
28 28
29 29 def execute(self, source=None, hidden=False, interactive=False):
30 30 """ Reimplemented to the store history.
31 31 """
32 32 if not hidden:
33 33 history = self.input_buffer if source is None else source
34 34
35 35 executed = super(HistoryConsoleWidget, self).execute(
36 36 source, hidden, interactive)
37 37
38 38 if executed and not hidden:
39 39 # Save the command unless it was an empty string or was identical
40 40 # to the previous command.
41 41 history = history.rstrip()
42 42 if history and (not self._history or self._history[-1] != history):
43 43 self._history.append(history)
44 44
45 45 # Move the history index to the most recent item.
46 46 self._history_index = len(self._history)
47 47
48 48 return executed
49 49
50 50 #---------------------------------------------------------------------------
51 51 # 'ConsoleWidget' abstract interface
52 52 #---------------------------------------------------------------------------
53 53
54 54 def _up_pressed(self):
55 55 """ Called when the up key is pressed. Returns whether to continue
56 56 processing the event.
57 57 """
58 58 prompt_cursor = self._get_prompt_cursor()
59 59 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
60 60
61 61 # Set a search prefix based on the cursor position.
62 62 col = self._get_input_buffer_cursor_column()
63 63 input_buffer = self.input_buffer
64 64 if self._history_index == len(self._history) or \
65 65 (self._history_prefix and col != len(self._history_prefix)):
66 66 self._history_index = len(self._history)
67 67 self._history_prefix = input_buffer[:col]
68 68
69 69 # Perform the search.
70 70 self.history_previous(self._history_prefix)
71 71
72 72 # Go to the first line of the prompt for seemless history scrolling.
73 73 # Emulate readline: keep the cursor position fixed for a prefix
74 74 # search.
75 75 cursor = self._get_prompt_cursor()
76 76 if self._history_prefix:
77 77 cursor.movePosition(QtGui.QTextCursor.Right,
78 78 n=len(self._history_prefix))
79 79 else:
80 80 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
81 81 self._set_cursor(cursor)
82 82
83 83 return False
84 84
85 85 return True
86 86
87 87 def _down_pressed(self):
88 88 """ Called when the down key is pressed. Returns whether to continue
89 89 processing the event.
90 90 """
91 91 end_cursor = self._get_end_cursor()
92 92 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
93 93
94 94 # Perform the search.
95 95 self.history_next(self._history_prefix)
96 96
97 97 # Emulate readline: keep the cursor position fixed for a prefix
98 98 # search. (We don't need to move the cursor to the end of the buffer
99 99 # in the other case because this happens automatically when the
100 100 # input buffer is set.)
101 101 if self._history_prefix:
102 102 cursor = self._get_prompt_cursor()
103 103 cursor.movePosition(QtGui.QTextCursor.Right,
104 104 n=len(self._history_prefix))
105 105 self._set_cursor(cursor)
106 106
107 107 return False
108 108
109 109 return True
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'HistoryConsoleWidget' public interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def history_previous(self, prefix=''):
116 116 """ If possible, set the input buffer to a previous item in the history.
117 117
118 118 Parameters:
119 119 -----------
120 120 prefix : str, optional
121 121 If specified, search for an item with this prefix.
122 122 """
123 123 index = self._history_index
124 124 while index > 0:
125 125 index -= 1
126 126 history = self._history[index]
127 127 if history.startswith(prefix):
128 128 break
129 129 else:
130 130 history = None
131 131
132 132 if history is not None:
133 133 self._history_index = index
134 134 self.input_buffer = history
135 135
136 136 def history_next(self, prefix=''):
137 137 """ Set the input buffer to a subsequent item in the history, or to the
138 138 original search prefix if there is no such item.
139 139
140 140 Parameters:
141 141 -----------
142 142 prefix : str, optional
143 143 If specified, search for an item with this prefix.
144 144 """
145 145 while self._history_index < len(self._history) - 1:
146 146 self._history_index += 1
147 147 history = self._history[self._history_index]
148 148 if history.startswith(prefix):
149 149 break
150 150 else:
151 151 self._history_index = len(self._history)
152 152 history = prefix
153 153 self.input_buffer = history
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'HistoryConsoleWidget' protected interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def _set_history(self, history):
160 160 """ Replace the current history with a sequence of history items.
161 161 """
162 162 self._history = list(history)
163 163 self._history_index = len(self._history)
@@ -1,497 +1,497 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 # Standard library imports
14 14 from collections import namedtuple
15 15 import re
16 16 from subprocess import Popen
17 17 from textwrap import dedent
18 18
19 19 # System library imports
20 from PyQt4 import QtCore, QtGui
20 from IPython.external.qt import QtCore, QtGui
21 21
22 22 # Local imports
23 23 from IPython.core.inputsplitter import IPythonInputSplitter, \
24 24 transform_ipy_prompt
25 25 from IPython.core.usage import default_gui_banner
26 26 from IPython.utils.traitlets import Bool, Str
27 27 from frontend_widget import FrontendWidget
28 28 from styles import (default_light_style_sheet, default_light_syntax_style,
29 29 default_dark_style_sheet, default_dark_syntax_style,
30 30 default_bw_style_sheet, default_bw_syntax_style)
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Constants
34 34 #-----------------------------------------------------------------------------
35 35
36 36 # Default strings to build and display input and output prompts (and separators
37 37 # in between)
38 38 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
39 39 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
40 40 default_input_sep = '\n'
41 41 default_output_sep = ''
42 42 default_output_sep2 = ''
43 43
44 44 # Base path for most payload sources.
45 45 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
46 46
47 47 #-----------------------------------------------------------------------------
48 48 # IPythonWidget class
49 49 #-----------------------------------------------------------------------------
50 50
51 51 class IPythonWidget(FrontendWidget):
52 52 """ A FrontendWidget for an IPython kernel.
53 53 """
54 54
55 55 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 56 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 57 # settings.
58 58 custom_edit = Bool(False)
59 custom_edit_requested = QtCore.pyqtSignal(object, object)
59 custom_edit_requested = QtCore.Signal(object, object)
60 60
61 61 # A command for invoking a system text editor. If the string contains a
62 62 # {filename} format specifier, it will be used. Otherwise, the filename will
63 63 # be appended to the end the command.
64 64 editor = Str('default', config=True)
65 65
66 66 # The editor command to use when a specific line number is requested. The
67 67 # string should contain two format specifiers: {line} and {filename}. If
68 68 # this parameter is not specified, the line number option to the %edit magic
69 69 # will be ignored.
70 70 editor_line = Str(config=True)
71 71
72 72 # A CSS stylesheet. The stylesheet can contain classes for:
73 73 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
74 74 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
75 75 # 3. IPython: .error, .in-prompt, .out-prompt, etc
76 76 style_sheet = Str(config=True)
77 77
78 78 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
79 79 # the style sheet is queried for Pygments style information.
80 80 syntax_style = Str(config=True)
81 81
82 82 # Prompts.
83 83 in_prompt = Str(default_in_prompt, config=True)
84 84 out_prompt = Str(default_out_prompt, config=True)
85 85 input_sep = Str(default_input_sep, config=True)
86 86 output_sep = Str(default_output_sep, config=True)
87 87 output_sep2 = Str(default_output_sep2, config=True)
88 88
89 89 # FrontendWidget protected class variables.
90 90 _input_splitter_class = IPythonInputSplitter
91 91
92 92 # IPythonWidget protected class variables.
93 93 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
94 94 _payload_source_edit = zmq_shell_source + '.edit_magic'
95 95 _payload_source_exit = zmq_shell_source + '.ask_exit'
96 96 _payload_source_loadpy = zmq_shell_source + '.magic_loadpy'
97 97 _payload_source_page = 'IPython.zmq.page.page'
98 98
99 99 #---------------------------------------------------------------------------
100 100 # 'object' interface
101 101 #---------------------------------------------------------------------------
102 102
103 103 def __init__(self, *args, **kw):
104 104 super(IPythonWidget, self).__init__(*args, **kw)
105 105
106 106 # IPythonWidget protected variables.
107 107 self._code_to_load = None
108 108 self._payload_handlers = {
109 109 self._payload_source_edit : self._handle_payload_edit,
110 110 self._payload_source_exit : self._handle_payload_exit,
111 111 self._payload_source_page : self._handle_payload_page,
112 112 self._payload_source_loadpy : self._handle_payload_loadpy }
113 113 self._previous_prompt_obj = None
114 114 self._keep_kernel_on_exit = None
115 115
116 116 # Initialize widget styling.
117 117 if self.style_sheet:
118 118 self._style_sheet_changed()
119 119 self._syntax_style_changed()
120 120 else:
121 121 self.set_default_style()
122 122
123 123 #---------------------------------------------------------------------------
124 124 # 'BaseFrontendMixin' abstract interface
125 125 #---------------------------------------------------------------------------
126 126
127 127 def _handle_complete_reply(self, rep):
128 128 """ Reimplemented to support IPython's improved completion machinery.
129 129 """
130 130 cursor = self._get_cursor()
131 131 info = self._request_info.get('complete')
132 132 if info and info.id == rep['parent_header']['msg_id'] and \
133 133 info.pos == cursor.position():
134 134 matches = rep['content']['matches']
135 135 text = rep['content']['matched_text']
136 136 offset = len(text)
137 137
138 138 # Clean up matches with period and path separators if the matched
139 139 # text has not been transformed. This is done by truncating all
140 140 # but the last component and then suitably decreasing the offset
141 141 # between the current cursor position and the start of completion.
142 142 if len(matches) > 1 and matches[0][:offset] == text:
143 143 parts = re.split(r'[./\\]', text)
144 144 sep_count = len(parts) - 1
145 145 if sep_count:
146 146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
147 147 matches = [ match[chop_length:] for match in matches ]
148 148 offset -= chop_length
149 149
150 150 # Move the cursor to the start of the match and complete.
151 151 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
152 152 self._complete_with_items(cursor, matches)
153 153
154 154 def _handle_execute_reply(self, msg):
155 155 """ Reimplemented to support prompt requests.
156 156 """
157 157 info = self._request_info.get('execute')
158 158 if info and info.id == msg['parent_header']['msg_id']:
159 159 if info.kind == 'prompt':
160 160 number = msg['content']['execution_count'] + 1
161 161 self._show_interpreter_prompt(number)
162 162 else:
163 163 super(IPythonWidget, self)._handle_execute_reply(msg)
164 164
165 165 def _handle_history_reply(self, msg):
166 166 """ Implemented to handle history replies, which are only supported by
167 167 the IPython kernel.
168 168 """
169 169 history_dict = msg['content']['history']
170 170 input_history_dict = {}
171 171 for key,val in history_dict.items():
172 172 input_history_dict[int(key)] = val
173 173 items = [ val.rstrip() for _, val in sorted(input_history_dict.items()) ]
174 174 self._set_history(items)
175 175
176 176 def _handle_pyout(self, msg):
177 177 """ Reimplemented for IPython-style "display hook".
178 178 """
179 179 if not self._hidden and self._is_from_this_session(msg):
180 180 content = msg['content']
181 181 prompt_number = content['execution_count']
182 182 data = content['data']
183 183 if data.has_key('text/html'):
184 184 self._append_plain_text(self.output_sep)
185 185 self._append_html(self._make_out_prompt(prompt_number))
186 186 html = data['text/html']
187 187 self._append_plain_text('\n')
188 188 self._append_html(html + self.output_sep2)
189 189 elif data.has_key('text/plain'):
190 190 self._append_plain_text(self.output_sep)
191 191 self._append_html(self._make_out_prompt(prompt_number))
192 192 text = data['text/plain']
193 193 self._append_plain_text(text + self.output_sep2)
194 194
195 195 def _handle_display_data(self, msg):
196 196 """ The base handler for the ``display_data`` message.
197 197 """
198 198 # For now, we don't display data from other frontends, but we
199 199 # eventually will as this allows all frontends to monitor the display
200 200 # data. But we need to figure out how to handle this in the GUI.
201 201 if not self._hidden and self._is_from_this_session(msg):
202 202 source = msg['content']['source']
203 203 data = msg['content']['data']
204 204 metadata = msg['content']['metadata']
205 205 # In the regular IPythonWidget, we simply print the plain text
206 206 # representation.
207 207 if data.has_key('text/html'):
208 208 html = data['text/html']
209 209 self._append_html(html)
210 210 elif data.has_key('text/plain'):
211 211 text = data['text/plain']
212 212 self._append_plain_text(text)
213 213 # This newline seems to be needed for text and html output.
214 214 self._append_plain_text(u'\n')
215 215
216 216 def _started_channels(self):
217 217 """ Reimplemented to make a history request.
218 218 """
219 219 super(IPythonWidget, self)._started_channels()
220 220 self.kernel_manager.xreq_channel.history(raw=True, output=False)
221 221
222 222 #---------------------------------------------------------------------------
223 223 # 'ConsoleWidget' public interface
224 224 #---------------------------------------------------------------------------
225 225
226 226 def copy(self):
227 227 """ Copy the currently selected text to the clipboard, removing prompts
228 228 if possible.
229 229 """
230 text = unicode(self._control.textCursor().selection().toPlainText())
230 text = self._control.textCursor().selection().toPlainText()
231 231 if text:
232 232 lines = map(transform_ipy_prompt, text.splitlines())
233 233 text = '\n'.join(lines)
234 234 QtGui.QApplication.clipboard().setText(text)
235 235
236 236 #---------------------------------------------------------------------------
237 237 # 'FrontendWidget' public interface
238 238 #---------------------------------------------------------------------------
239 239
240 240 def execute_file(self, path, hidden=False):
241 241 """ Reimplemented to use the 'run' magic.
242 242 """
243 243 self.execute('%%run %s' % path, hidden=hidden)
244 244
245 245 #---------------------------------------------------------------------------
246 246 # 'FrontendWidget' protected interface
247 247 #---------------------------------------------------------------------------
248 248
249 249 def _complete(self):
250 250 """ Reimplemented to support IPython's improved completion machinery.
251 251 """
252 252 # We let the kernel split the input line, so we *always* send an empty
253 253 # text field. Readline-based frontends do get a real text field which
254 254 # they can use.
255 255 text = ''
256 256
257 257 # Send the completion request to the kernel
258 258 msg_id = self.kernel_manager.xreq_channel.complete(
259 259 text, # text
260 260 self._get_input_buffer_cursor_line(), # line
261 261 self._get_input_buffer_cursor_column(), # cursor_pos
262 262 self.input_buffer) # block
263 263 pos = self._get_cursor().position()
264 264 info = self._CompletionRequest(msg_id, pos)
265 265 self._request_info['complete'] = info
266 266
267 267 def _get_banner(self):
268 268 """ Reimplemented to return IPython's default banner.
269 269 """
270 270 return default_gui_banner
271 271
272 272 def _process_execute_error(self, msg):
273 273 """ Reimplemented for IPython-style traceback formatting.
274 274 """
275 275 content = msg['content']
276 276 traceback = '\n'.join(content['traceback']) + '\n'
277 277 if False:
278 278 # FIXME: For now, tracebacks come as plain text, so we can't use
279 279 # the html renderer yet. Once we refactor ultratb to produce
280 280 # properly styled tracebacks, this branch should be the default
281 281 traceback = traceback.replace(' ', '&nbsp;')
282 282 traceback = traceback.replace('\n', '<br/>')
283 283
284 284 ename = content['ename']
285 285 ename_styled = '<span class="error">%s</span>' % ename
286 286 traceback = traceback.replace(ename, ename_styled)
287 287
288 288 self._append_html(traceback)
289 289 else:
290 290 # This is the fallback for now, using plain text with ansi escapes
291 291 self._append_plain_text(traceback)
292 292
293 293 def _process_execute_payload(self, item):
294 294 """ Reimplemented to dispatch payloads to handler methods.
295 295 """
296 296 handler = self._payload_handlers.get(item['source'])
297 297 if handler is None:
298 298 # We have no handler for this type of payload, simply ignore it
299 299 return False
300 300 else:
301 301 handler(item)
302 302 return True
303 303
304 304 def _show_interpreter_prompt(self, number=None):
305 305 """ Reimplemented for IPython-style prompts.
306 306 """
307 307 # If a number was not specified, make a prompt number request.
308 308 if number is None:
309 309 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
310 310 info = self._ExecutionRequest(msg_id, 'prompt')
311 311 self._request_info['execute'] = info
312 312 return
313 313
314 314 # Show a new prompt and save information about it so that it can be
315 315 # updated later if the prompt number turns out to be wrong.
316 316 self._prompt_sep = self.input_sep
317 317 self._show_prompt(self._make_in_prompt(number), html=True)
318 318 block = self._control.document().lastBlock()
319 319 length = len(self._prompt)
320 320 self._previous_prompt_obj = self._PromptBlock(block, length, number)
321 321
322 322 # Update continuation prompt to reflect (possibly) new prompt length.
323 323 self._set_continuation_prompt(
324 324 self._make_continuation_prompt(self._prompt), html=True)
325 325
326 326 # Load code from the %loadpy magic, if necessary.
327 327 if self._code_to_load is not None:
328 self.input_buffer = dedent(unicode(self._code_to_load).rstrip())
328 self.input_buffer = dedent(self._code_to_load.rstrip())
329 329 self._code_to_load = None
330 330
331 331 def _show_interpreter_prompt_for_reply(self, msg):
332 332 """ Reimplemented for IPython-style prompts.
333 333 """
334 334 # Update the old prompt number if necessary.
335 335 content = msg['content']
336 336 previous_prompt_number = content['execution_count']
337 337 if self._previous_prompt_obj and \
338 338 self._previous_prompt_obj.number != previous_prompt_number:
339 339 block = self._previous_prompt_obj.block
340 340
341 341 # Make sure the prompt block has not been erased.
342 342 if block.isValid() and not block.text().isEmpty():
343 343
344 344 # Remove the old prompt and insert a new prompt.
345 345 cursor = QtGui.QTextCursor(block)
346 346 cursor.movePosition(QtGui.QTextCursor.Right,
347 347 QtGui.QTextCursor.KeepAnchor,
348 348 self._previous_prompt_obj.length)
349 349 prompt = self._make_in_prompt(previous_prompt_number)
350 350 self._prompt = self._insert_html_fetching_plain_text(
351 351 cursor, prompt)
352 352
353 353 # When the HTML is inserted, Qt blows away the syntax
354 354 # highlighting for the line, so we need to rehighlight it.
355 355 self._highlighter.rehighlightBlock(cursor.block())
356 356
357 357 self._previous_prompt_obj = None
358 358
359 359 # Show a new prompt with the kernel's estimated prompt number.
360 360 self._show_interpreter_prompt(previous_prompt_number + 1)
361 361
362 362 #---------------------------------------------------------------------------
363 363 # 'IPythonWidget' interface
364 364 #---------------------------------------------------------------------------
365 365
366 366 def set_default_style(self, colors='lightbg'):
367 367 """ Sets the widget style to the class defaults.
368 368
369 369 Parameters:
370 370 -----------
371 371 colors : str, optional (default lightbg)
372 372 Whether to use the default IPython light background or dark
373 373 background or B&W style.
374 374 """
375 375 colors = colors.lower()
376 376 if colors=='lightbg':
377 377 self.style_sheet = default_light_style_sheet
378 378 self.syntax_style = default_light_syntax_style
379 379 elif colors=='linux':
380 380 self.style_sheet = default_dark_style_sheet
381 381 self.syntax_style = default_dark_syntax_style
382 382 elif colors=='nocolor':
383 383 self.style_sheet = default_bw_style_sheet
384 384 self.syntax_style = default_bw_syntax_style
385 385 else:
386 386 raise KeyError("No such color scheme: %s"%colors)
387 387
388 388 #---------------------------------------------------------------------------
389 389 # 'IPythonWidget' protected interface
390 390 #---------------------------------------------------------------------------
391 391
392 392 def _edit(self, filename, line=None):
393 393 """ Opens a Python script for editing.
394 394
395 395 Parameters:
396 396 -----------
397 397 filename : str
398 398 A path to a local system file.
399 399
400 400 line : int, optional
401 401 A line of interest in the file.
402 402 """
403 403 if self.custom_edit:
404 404 self.custom_edit_requested.emit(filename, line)
405 405 elif self.editor == 'default':
406 406 self._append_plain_text('No default editor available.\n')
407 407 else:
408 408 try:
409 409 filename = '"%s"' % filename
410 410 if line and self.editor_line:
411 411 command = self.editor_line.format(filename=filename,
412 412 line=line)
413 413 else:
414 414 try:
415 415 command = self.editor.format()
416 416 except KeyError:
417 417 command = self.editor.format(filename=filename)
418 418 else:
419 419 command += ' ' + filename
420 420 except KeyError:
421 421 self._append_plain_text('Invalid editor command.\n')
422 422 else:
423 423 try:
424 424 Popen(command, shell=True)
425 425 except OSError:
426 426 msg = 'Opening editor with command "%s" failed.\n'
427 427 self._append_plain_text(msg % command)
428 428
429 429 def _make_in_prompt(self, number):
430 430 """ Given a prompt number, returns an HTML In prompt.
431 431 """
432 432 body = self.in_prompt % number
433 433 return '<span class="in-prompt">%s</span>' % body
434 434
435 435 def _make_continuation_prompt(self, prompt):
436 436 """ Given a plain text version of an In prompt, returns an HTML
437 437 continuation prompt.
438 438 """
439 439 end_chars = '...: '
440 440 space_count = len(prompt.lstrip('\n')) - len(end_chars)
441 441 body = '&nbsp;' * space_count + end_chars
442 442 return '<span class="in-prompt">%s</span>' % body
443 443
444 444 def _make_out_prompt(self, number):
445 445 """ Given a prompt number, returns an HTML Out prompt.
446 446 """
447 447 body = self.out_prompt % number
448 448 return '<span class="out-prompt">%s</span>' % body
449 449
450 450 #------ Payload handlers --------------------------------------------------
451 451
452 452 # Payload handlers with a generic interface: each takes the opaque payload
453 453 # dict, unpacks it and calls the underlying functions with the necessary
454 454 # arguments.
455 455
456 456 def _handle_payload_edit(self, item):
457 457 self._edit(item['filename'], item['line_number'])
458 458
459 459 def _handle_payload_exit(self, item):
460 460 self._keep_kernel_on_exit = item['keepkernel']
461 461 self.exit_requested.emit()
462 462
463 463 def _handle_payload_loadpy(self, item):
464 464 # Simple save the text of the .py file for later. The text is written
465 465 # to the buffer when _prompt_started_hook is called.
466 466 self._code_to_load = item['text']
467 467
468 468 def _handle_payload_page(self, item):
469 469 # Since the plain text widget supports only a very small subset of HTML
470 470 # and we have no control over the HTML source, we only page HTML
471 471 # payloads in the rich text widget.
472 472 if item['html'] and self.kind == 'rich':
473 473 self._page(item['html'], html=True)
474 474 else:
475 475 self._page(item['text'], html=False)
476 476
477 477 #------ Trait change handlers --------------------------------------------
478 478
479 479 def _style_sheet_changed(self):
480 480 """ Set the style sheets of the underlying widgets.
481 481 """
482 482 self.setStyleSheet(self.style_sheet)
483 483 self._control.document().setDefaultStyleSheet(self.style_sheet)
484 484 if self._page_control:
485 485 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
486 486
487 487 bg_color = self._control.palette().background().color()
488 488 self._ansi_processor.set_background_color(bg_color)
489 489
490 490 def _syntax_style_changed(self):
491 491 """ Set the style for the syntax highlighter.
492 492 """
493 493 if self.syntax_style:
494 494 self._highlighter.set_style(self.syntax_style)
495 495 else:
496 496 self._highlighter.set_style_sheet(self.style_sheet)
497 497
@@ -1,273 +1,275 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Imports
6 6 #-----------------------------------------------------------------------------
7 7
8 8 # Systemm library imports
9 from PyQt4 import QtGui
9 from IPython.external.qt import QtGui
10 10 from pygments.styles import get_all_styles
11
11 12 # Local imports
12 13 from IPython.external.argparse import ArgumentParser
13 14 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 15 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 16 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 17 from IPython.frontend.qt.console import styles
17 18 from IPython.frontend.qt.kernelmanager import QtKernelManager
18 19
19 20 #-----------------------------------------------------------------------------
20 21 # Network Constants
21 22 #-----------------------------------------------------------------------------
22 23
23 24 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
24 25
25 26 #-----------------------------------------------------------------------------
26 27 # Classes
27 28 #-----------------------------------------------------------------------------
28 29
29 30 class MainWindow(QtGui.QMainWindow):
30 31
31 32 #---------------------------------------------------------------------------
32 33 # 'object' interface
33 34 #---------------------------------------------------------------------------
34 35
35 36 def __init__(self, app, frontend, existing=False, may_close=True):
36 37 """ Create a MainWindow for the specified FrontendWidget.
37 38
38 39 The app is passed as an argument to allow for different
39 40 closing behavior depending on whether we are the Kernel's parent.
40 41
41 42 If existing is True, then this Console does not own the Kernel.
42 43
43 44 If may_close is True, then this Console is permitted to close the kernel
44 45 """
45 46 super(MainWindow, self).__init__()
46 47 self._app = app
47 48 self._frontend = frontend
48 49 self._existing = existing
49 50 if existing:
50 51 self._may_close = may_close
51 52 else:
52 53 self._may_close = True
53 54 self._frontend.exit_requested.connect(self.close)
54 55 self.setCentralWidget(frontend)
55 56
56 57 #---------------------------------------------------------------------------
57 58 # QWidget interface
58 59 #---------------------------------------------------------------------------
59 60
60 61 def closeEvent(self, event):
61 62 """ Close the window and the kernel (if necessary).
62 63
63 64 This will prompt the user if they are finished with the kernel, and if
64 65 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
65 66 it closes without prompt.
66 67 """
67 68 keepkernel = None #Use the prompt by default
68 69 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
69 70 keepkernel = self._frontend._keep_kernel_on_exit
70 71
71 72 kernel_manager = self._frontend.kernel_manager
72 73
73 74 if keepkernel is None: #show prompt
74 75 if kernel_manager and kernel_manager.channels_running:
75 76 title = self.window().windowTitle()
76 77 cancel = QtGui.QMessageBox.Cancel
77 78 okay = QtGui.QMessageBox.Ok
78 79 if self._may_close:
79 80 msg = "You are closing this Console window."
80 81 info = "Would you like to quit the Kernel and all attached Consoles as well?"
81 82 justthis = QtGui.QPushButton("&No, just this Console", self)
82 83 justthis.setShortcut('N')
83 84 closeall = QtGui.QPushButton("&Yes, quit everything", self)
84 85 closeall.setShortcut('Y')
85 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
86 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
87 title, msg)
86 88 box.setInformativeText(info)
87 89 box.addButton(cancel)
88 90 box.addButton(justthis, QtGui.QMessageBox.NoRole)
89 91 box.addButton(closeall, QtGui.QMessageBox.YesRole)
90 92 box.setDefaultButton(closeall)
91 93 box.setEscapeButton(cancel)
92 94 reply = box.exec_()
93 95 if reply == 1: # close All
94 96 kernel_manager.shutdown_kernel()
95 97 #kernel_manager.stop_channels()
96 98 event.accept()
97 99 elif reply == 0: # close Console
98 100 if not self._existing:
99 101 # Have kernel: don't quit, just close the window
100 102 self._app.setQuitOnLastWindowClosed(False)
101 103 self.deleteLater()
102 104 event.accept()
103 105 else:
104 106 event.ignore()
105 107 else:
106 108 reply = QtGui.QMessageBox.question(self, title,
107 109 "Are you sure you want to close this Console?"+
108 110 "\nThe Kernel and other Consoles will remain active.",
109 111 okay|cancel,
110 112 defaultButton=okay
111 113 )
112 114 if reply == okay:
113 115 event.accept()
114 116 else:
115 117 event.ignore()
116 118 elif keepkernel: #close console but leave kernel running (no prompt)
117 119 if kernel_manager and kernel_manager.channels_running:
118 120 if not self._existing:
119 121 # I have the kernel: don't quit, just close the window
120 122 self._app.setQuitOnLastWindowClosed(False)
121 123 event.accept()
122 124 else: #close console and kernel (no prompt)
123 125 if kernel_manager and kernel_manager.channels_running:
124 126 kernel_manager.shutdown_kernel()
125 127 event.accept()
126 128
127 129 #-----------------------------------------------------------------------------
128 130 # Main entry point
129 131 #-----------------------------------------------------------------------------
130 132
131 133 def main():
132 134 """ Entry point for application.
133 135 """
134 136 # Parse command line arguments.
135 137 parser = ArgumentParser()
136 138 kgroup = parser.add_argument_group('kernel options')
137 139 kgroup.add_argument('-e', '--existing', action='store_true',
138 140 help='connect to an existing kernel')
139 141 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
140 142 help=\
141 143 "set the kernel\'s IP address [default localhost].\
142 144 If the IP address is something other than localhost, then \
143 145 Consoles on other machines will be able to connect\
144 146 to the Kernel, so be careful!")
145 147 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
146 148 help='set the XREQ channel port [default random]')
147 149 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
148 150 help='set the SUB channel port [default random]')
149 151 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
150 152 help='set the REP channel port [default random]')
151 153 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
152 154 help='set the heartbeat port [default random]')
153 155
154 156 egroup = kgroup.add_mutually_exclusive_group()
155 157 egroup.add_argument('--pure', action='store_true', help = \
156 158 'use a pure Python kernel instead of an IPython kernel')
157 159 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
158 160 const='auto', help = \
159 161 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
160 162 given, the GUI backend is matplotlib's, otherwise use one of: \
161 163 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
162 164
163 165 wgroup = parser.add_argument_group('widget options')
164 166 wgroup.add_argument('--paging', type=str, default='inside',
165 167 choices = ['inside', 'hsplit', 'vsplit', 'none'],
166 168 help='set the paging style [default inside]')
167 169 wgroup.add_argument('--rich', action='store_true',
168 170 help='enable rich text support')
169 171 wgroup.add_argument('--gui-completion', action='store_true',
170 172 help='use a GUI widget for tab completion')
171 173 wgroup.add_argument('--style', type=str,
172 174 choices = list(get_all_styles()),
173 175 help='specify a pygments style for by name.')
174 176 wgroup.add_argument('--stylesheet', type=str,
175 177 help="path to a custom CSS stylesheet.")
176 178 wgroup.add_argument('--colors', type=str,
177 179 help="Set the color scheme (LightBG,Linux,NoColor). This is guessed\
178 180 based on the pygments style if not set.")
179 181
180 182 args = parser.parse_args()
181 183
182 184 # parse the colors arg down to current known labels
183 185 if args.colors:
184 186 colors=args.colors.lower()
185 187 if colors in ('lightbg', 'light'):
186 188 colors='lightbg'
187 189 elif colors in ('dark', 'linux'):
188 190 colors='linux'
189 191 else:
190 192 colors='nocolor'
191 193 elif args.style:
192 194 if args.style=='bw':
193 195 colors='nocolor'
194 196 elif styles.dark_style(args.style):
195 197 colors='linux'
196 198 else:
197 199 colors='lightbg'
198 200 else:
199 201 colors=None
200 202
201 203 # Don't let Qt or ZMQ swallow KeyboardInterupts.
202 204 import signal
203 205 signal.signal(signal.SIGINT, signal.SIG_DFL)
204 206
205 207 # Create a KernelManager and start a kernel.
206 208 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
207 209 sub_address=(args.ip, args.sub),
208 210 rep_address=(args.ip, args.rep),
209 211 hb_address=(args.ip, args.hb))
210 212 if not args.existing:
211 213 # if not args.ip in LOCAL_IPS+ALL_ALIAS:
212 214 # raise ValueError("Must bind a local ip, such as: %s"%LOCAL_IPS)
213 215
214 216 kwargs = dict(ip=args.ip)
215 217 if args.pure:
216 218 kwargs['ipython']=False
217 219 else:
218 220 kwargs['colors']=colors
219 221 if args.pylab:
220 222 kwargs['pylab']=args.pylab
221 223
222 224 kernel_manager.start_kernel(**kwargs)
223 225 kernel_manager.start_channels()
224 226
225 227 local_kernel = (not args.existing) or args.ip in LOCAL_IPS
226 228 # Create the widget.
227 229 app = QtGui.QApplication([])
228 230 if args.pure:
229 231 kind = 'rich' if args.rich else 'plain'
230 232 widget = FrontendWidget(kind=kind, paging=args.paging, local_kernel=local_kernel)
231 233 elif args.rich or args.pylab:
232 234 widget = RichIPythonWidget(paging=args.paging, local_kernel=local_kernel)
233 235 else:
234 236 widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
235 237 widget.gui_completion = args.gui_completion
236 238 widget.kernel_manager = kernel_manager
237 239
238 240 # configure the style:
239 241 if not args.pure: # only IPythonWidget supports styles
240 242 if args.style:
241 243 widget.syntax_style = args.style
242 244 widget.style_sheet = styles.sheet_from_template(args.style, colors)
243 245 widget._syntax_style_changed()
244 246 widget._style_sheet_changed()
245 247 elif colors:
246 248 # use a default style
247 249 widget.set_default_style(colors=colors)
248 250 else:
249 251 # this is redundant for now, but allows the widget's
250 252 # defaults to change
251 253 widget.set_default_style()
252 254
253 255 if args.stylesheet:
254 256 # we got an expicit stylesheet
255 257 if os.path.isfile(args.stylesheet):
256 258 with open(args.stylesheet) as f:
257 259 sheet = f.read()
258 260 widget.style_sheet = sheet
259 261 widget._style_sheet_changed()
260 262 else:
261 263 raise IOError("Stylesheet %r not found."%args.stylesheet)
262 264
263 265 # Create the main window.
264 266 window = MainWindow(app, widget, args.existing, may_close=local_kernel)
265 267 window.setWindowTitle('Python' if args.pure else 'IPython')
266 268 window.show()
267 269
268 270 # Start the application main loop.
269 271 app.exec_()
270 272
271 273
272 274 if __name__ == '__main__':
273 275 main()
@@ -1,226 +1,224 b''
1 1 # System library imports.
2 from PyQt4 import QtGui
2 from IPython.external.qt import QtGui
3 3 from pygments.formatters.html import HtmlFormatter
4 4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
5 5 from pygments.lexers import PythonLexer
6 6 from pygments.styles import get_style_by_name
7 7
8 8
9 9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 10 """ Split ``text`` into (tokentype, text) pairs.
11 11
12 12 Monkeypatched to store the final stack on the object itself.
13 13 """
14 14 pos = 0
15 15 tokendefs = self._tokens
16 16 if hasattr(self, '_saved_state_stack'):
17 17 statestack = list(self._saved_state_stack)
18 18 else:
19 19 statestack = list(stack)
20 20 statetokens = tokendefs[statestack[-1]]
21 21 while 1:
22 22 for rexmatch, action, new_state in statetokens:
23 23 m = rexmatch(text, pos)
24 24 if m:
25 25 if type(action) is _TokenType:
26 26 yield pos, action, m.group()
27 27 else:
28 28 for item in action(self, m):
29 29 yield item
30 30 pos = m.end()
31 31 if new_state is not None:
32 32 # state transition
33 33 if isinstance(new_state, tuple):
34 34 for state in new_state:
35 35 if state == '#pop':
36 36 statestack.pop()
37 37 elif state == '#push':
38 38 statestack.append(statestack[-1])
39 39 else:
40 40 statestack.append(state)
41 41 elif isinstance(new_state, int):
42 42 # pop
43 43 del statestack[new_state:]
44 44 elif new_state == '#push':
45 45 statestack.append(statestack[-1])
46 46 else:
47 47 assert False, "wrong state def: %r" % new_state
48 48 statetokens = tokendefs[statestack[-1]]
49 49 break
50 50 else:
51 51 try:
52 52 if text[pos] == '\n':
53 53 # at EOL, reset state to "root"
54 54 pos += 1
55 55 statestack = ['root']
56 56 statetokens = tokendefs['root']
57 57 yield pos, Text, u'\n'
58 58 continue
59 59 yield pos, Error, text[pos]
60 60 pos += 1
61 61 except IndexError:
62 62 break
63 63 self._saved_state_stack = list(statestack)
64 64
65 65 # Monkeypatch!
66 66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67 67
68 68
69 69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
70 70 """ Storage for the user data associated with each line.
71 71 """
72 72
73 73 syntax_stack = ('root',)
74 74
75 75 def __init__(self, **kwds):
76 76 for key, value in kwds.iteritems():
77 77 setattr(self, key, value)
78 78 QtGui.QTextBlockUserData.__init__(self)
79 79
80 80 def __repr__(self):
81 81 attrs = ['syntax_stack']
82 82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 83 for attr in attrs ])
84 84 return 'PygmentsBlockUserData(%s)' % kwds
85 85
86 86
87 87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 88 """ Syntax highlighter that uses Pygments for parsing. """
89 89
90 90 #---------------------------------------------------------------------------
91 91 # 'QSyntaxHighlighter' interface
92 92 #---------------------------------------------------------------------------
93 93
94 94 def __init__(self, parent, lexer=None):
95 95 super(PygmentsHighlighter, self).__init__(parent)
96 96
97 97 self._document = QtGui.QTextDocument()
98 98 self._formatter = HtmlFormatter(nowrap=True)
99 99 self._lexer = lexer if lexer else PythonLexer()
100 100 self.set_style('default')
101 101
102 def highlightBlock(self, qstring):
102 def highlightBlock(self, string):
103 103 """ Highlight a block of text.
104 104 """
105 qstring = unicode(qstring)
106 105 prev_data = self.currentBlock().previous().userData()
107
108 106 if prev_data is not None:
109 107 self._lexer._saved_state_stack = prev_data.syntax_stack
110 108 elif hasattr(self._lexer, '_saved_state_stack'):
111 109 del self._lexer._saved_state_stack
112 110
113 111 # Lex the text using Pygments
114 112 index = 0
115 for token, text in self._lexer.get_tokens(qstring):
113 for token, text in self._lexer.get_tokens(string):
116 114 length = len(text)
117 115 self.setFormat(index, length, self._get_format(token))
118 116 index += length
119 117
120 118 if hasattr(self._lexer, '_saved_state_stack'):
121 119 data = PygmentsBlockUserData(
122 120 syntax_stack=self._lexer._saved_state_stack)
123 121 self.currentBlock().setUserData(data)
124 122 # Clean up for the next go-round.
125 123 del self._lexer._saved_state_stack
126 124
127 125 #---------------------------------------------------------------------------
128 126 # 'PygmentsHighlighter' interface
129 127 #---------------------------------------------------------------------------
130 128
131 129 def set_style(self, style):
132 130 """ Sets the style to the specified Pygments style.
133 131 """
134 132 if isinstance(style, basestring):
135 133 style = get_style_by_name(style)
136 134 self._style = style
137 135 self._clear_caches()
138 136
139 137 def set_style_sheet(self, stylesheet):
140 138 """ Sets a CSS stylesheet. The classes in the stylesheet should
141 139 correspond to those generated by:
142 140
143 141 pygmentize -S <style> -f html
144 142
145 143 Note that 'set_style' and 'set_style_sheet' completely override each
146 144 other, i.e. they cannot be used in conjunction.
147 145 """
148 146 self._document.setDefaultStyleSheet(stylesheet)
149 147 self._style = None
150 148 self._clear_caches()
151 149
152 150 #---------------------------------------------------------------------------
153 151 # Protected interface
154 152 #---------------------------------------------------------------------------
155 153
156 154 def _clear_caches(self):
157 155 """ Clear caches for brushes and formats.
158 156 """
159 157 self._brushes = {}
160 158 self._formats = {}
161 159
162 160 def _get_format(self, token):
163 161 """ Returns a QTextCharFormat for token or None.
164 162 """
165 163 if token in self._formats:
166 164 return self._formats[token]
167 165
168 166 if self._style is None:
169 167 result = self._get_format_from_document(token, self._document)
170 168 else:
171 169 result = self._get_format_from_style(token, self._style)
172 170
173 171 self._formats[token] = result
174 172 return result
175 173
176 174 def _get_format_from_document(self, token, document):
177 175 """ Returns a QTextCharFormat for token by
178 176 """
179 177 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
180 178 self._document.setHtml(html)
181 179 return QtGui.QTextCursor(self._document).charFormat()
182 180
183 181 def _get_format_from_style(self, token, style):
184 182 """ Returns a QTextCharFormat for token by reading a Pygments style.
185 183 """
186 184 result = QtGui.QTextCharFormat()
187 185 for key, value in style.style_for_token(token).items():
188 186 if value:
189 187 if key == 'color':
190 188 result.setForeground(self._get_brush(value))
191 189 elif key == 'bgcolor':
192 190 result.setBackground(self._get_brush(value))
193 191 elif key == 'bold':
194 192 result.setFontWeight(QtGui.QFont.Bold)
195 193 elif key == 'italic':
196 194 result.setFontItalic(True)
197 195 elif key == 'underline':
198 196 result.setUnderlineStyle(
199 197 QtGui.QTextCharFormat.SingleUnderline)
200 198 elif key == 'sans':
201 199 result.setFontStyleHint(QtGui.QFont.SansSerif)
202 200 elif key == 'roman':
203 201 result.setFontStyleHint(QtGui.QFont.Times)
204 202 elif key == 'mono':
205 203 result.setFontStyleHint(QtGui.QFont.TypeWriter)
206 204 return result
207 205
208 206 def _get_brush(self, color):
209 207 """ Returns a brush for the color.
210 208 """
211 209 result = self._brushes.get(color)
212 210 if result is None:
213 211 qcolor = self._get_color(color)
214 212 result = QtGui.QBrush(qcolor)
215 213 self._brushes[color] = result
216 214 return result
217 215
218 216 def _get_color(self, color):
219 217 """ Returns a QColor built from a Pygments color string.
220 218 """
221 219 qcolor = QtGui.QColor()
222 220 qcolor.setRgb(int(color[:2], base=16),
223 221 int(color[2:4], base=16),
224 222 int(color[4:6], base=16))
225 223 return qcolor
226 224
@@ -1,271 +1,273 b''
1 # System library imports
1 # Standard libary imports.
2 from base64 import decodestring
2 3 import os
3 4 import re
4 from base64 import decodestring
5 from PyQt4 import QtCore, QtGui
5
6 # System libary imports.
7 from IPython.external.qt import QtCore, QtGui
6 8
7 9 # Local imports
8 10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
9 11 from ipython_widget import IPythonWidget
10 12
11 13
12 14 class RichIPythonWidget(IPythonWidget):
13 15 """ An IPythonWidget that supports rich text, including lists, images, and
14 16 tables. Note that raw performance will be reduced compared to the plain
15 17 text version.
16 18 """
17 19
18 20 # RichIPythonWidget protected class variables.
19 21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
20 22 _svg_text_format_property = 1
21 23
22 24 #---------------------------------------------------------------------------
23 25 # 'object' interface
24 26 #---------------------------------------------------------------------------
25 27
26 28 def __init__(self, *args, **kw):
27 29 """ Create a RichIPythonWidget.
28 30 """
29 31 kw['kind'] = 'rich'
30 32 super(RichIPythonWidget, self).__init__(*args, **kw)
31 33 # Dictionary for resolving Qt names to images when
32 34 # generating XHTML output
33 35 self._name_to_svg = {}
34 36
35 37 #---------------------------------------------------------------------------
36 38 # 'ConsoleWidget' protected interface
37 39 #---------------------------------------------------------------------------
38 40
39 41 def _context_menu_make(self, pos):
40 42 """ Reimplemented to return a custom context menu for images.
41 43 """
42 44 format = self._control.cursorForPosition(pos).charFormat()
43 45 name = format.stringProperty(QtGui.QTextFormat.ImageName)
44 46 if name.isEmpty():
45 47 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
46 48 else:
47 49 menu = QtGui.QMenu()
48 50
49 51 menu.addAction('Copy Image', lambda: self._copy_image(name))
50 52 menu.addAction('Save Image As...', lambda: self._save_image(name))
51 53 menu.addSeparator()
52 54
53 55 svg = format.stringProperty(self._svg_text_format_property)
54 56 if not svg.isEmpty():
55 57 menu.addSeparator()
56 58 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
57 59 menu.addAction('Save SVG As...',
58 60 lambda: save_svg(svg, self._control))
59 61 return menu
60 62
61 63 #---------------------------------------------------------------------------
62 64 # 'BaseFrontendMixin' abstract interface
63 65 #---------------------------------------------------------------------------
64 66
65 67 def _handle_pyout(self, msg):
66 68 """ Overridden to handle rich data types, like SVG.
67 69 """
68 70 if not self._hidden and self._is_from_this_session(msg):
69 71 content = msg['content']
70 72 prompt_number = content['execution_count']
71 73 data = content['data']
72 74 if data.has_key('image/svg+xml'):
73 75 self._append_plain_text(self.output_sep)
74 76 self._append_html(self._make_out_prompt(prompt_number))
75 77 # TODO: try/except this call.
76 78 self._append_svg(data['image/svg+xml'])
77 79 self._append_html(self.output_sep2)
78 80 elif data.has_key('image/png'):
79 81 self._append_plain_text(self.output_sep)
80 82 self._append_html(self._make_out_prompt(prompt_number))
81 83 # This helps the output to look nice.
82 84 self._append_plain_text('\n')
83 85 # TODO: try/except these calls
84 86 png = decodestring(data['image/png'])
85 87 self._append_png(png)
86 88 self._append_html(self.output_sep2)
87 89 else:
88 90 # Default back to the plain text representation.
89 91 return super(RichIPythonWidget, self)._handle_pyout(msg)
90 92
91 93 def _handle_display_data(self, msg):
92 94 """ Overridden to handle rich data types, like SVG.
93 95 """
94 96 if not self._hidden and self._is_from_this_session(msg):
95 97 source = msg['content']['source']
96 98 data = msg['content']['data']
97 99 metadata = msg['content']['metadata']
98 100 # Try to use the svg or html representations.
99 101 # FIXME: Is this the right ordering of things to try?
100 102 if data.has_key('image/svg+xml'):
101 103 svg = data['image/svg+xml']
102 104 # TODO: try/except this call.
103 105 self._append_svg(svg)
104 106 elif data.has_key('image/png'):
105 107 # TODO: try/except these calls
106 108 # PNG data is base64 encoded as it passes over the network
107 109 # in a JSON structure so we decode it.
108 110 png = decodestring(data['image/png'])
109 111 self._append_png(png)
110 112 else:
111 113 # Default back to the plain text representation.
112 114 return super(RichIPythonWidget, self)._handle_display_data(msg)
113 115
114 116 #---------------------------------------------------------------------------
115 117 # 'FrontendWidget' protected interface
116 118 #---------------------------------------------------------------------------
117 119
118 120 def _process_execute_payload(self, item):
119 121 """ Reimplemented to handle matplotlib plot payloads.
120 122 """
121 123 # TODO: remove this as all plot data is coming back through the
122 124 # display_data message type.
123 125 if item['source'] == self._payload_source_plot:
124 126 if item['format'] == 'svg':
125 127 svg = item['data']
126 128 self._append_svg(svg)
127 129 return True
128 130 else:
129 131 # Add other plot formats here!
130 132 return False
131 133 else:
132 134 return super(RichIPythonWidget, self)._process_execute_payload(item)
133 135
134 136 #---------------------------------------------------------------------------
135 137 # 'RichIPythonWidget' protected interface
136 138 #---------------------------------------------------------------------------
137 139
138 140 def _append_svg(self, svg):
139 141 """ Append raw svg data to the widget.
140 142 """
141 143 try:
142 144 image = svg_to_image(svg)
143 145 except ValueError:
144 146 self._append_plain_text('Received invalid plot data.')
145 147 else:
146 148 format = self._add_image(image)
147 149 self._name_to_svg[str(format.name())] = svg
148 150 format.setProperty(self._svg_text_format_property, svg)
149 151 cursor = self._get_end_cursor()
150 152 cursor.insertBlock()
151 153 cursor.insertImage(format)
152 154 cursor.insertBlock()
153 155
154 156 def _append_png(self, png):
155 157 """ Append raw svg data to the widget.
156 158 """
157 159 try:
158 160 image = QtGui.QImage()
159 161 image.loadFromData(png, 'PNG')
160 162 except ValueError:
161 163 self._append_plain_text('Received invalid plot data.')
162 164 else:
163 165 format = self._add_image(image)
164 166 cursor = self._get_end_cursor()
165 167 cursor.insertBlock()
166 168 cursor.insertImage(format)
167 169 cursor.insertBlock()
168 170
169 171 def _add_image(self, image):
170 172 """ Adds the specified QImage to the document and returns a
171 173 QTextImageFormat that references it.
172 174 """
173 175 document = self._control.document()
174 name = QtCore.QString.number(image.cacheKey())
176 name = str(image.cacheKey())
175 177 document.addResource(QtGui.QTextDocument.ImageResource,
176 178 QtCore.QUrl(name), image)
177 179 format = QtGui.QTextImageFormat()
178 180 format.setName(name)
179 181 return format
180 182
181 183 def _copy_image(self, name):
182 184 """ Copies the ImageResource with 'name' to the clipboard.
183 185 """
184 186 image = self._get_image(name)
185 187 QtGui.QApplication.clipboard().setImage(image)
186 188
187 189 def _get_image(self, name):
188 190 """ Returns the QImage stored as the ImageResource with 'name'.
189 191 """
190 192 document = self._control.document()
191 193 variant = document.resource(QtGui.QTextDocument.ImageResource,
192 194 QtCore.QUrl(name))
193 195 return variant.toPyObject()
194 196
195 197 def _save_image(self, name, format='PNG'):
196 198 """ Shows a save dialog for the ImageResource with 'name'.
197 199 """
198 200 dialog = QtGui.QFileDialog(self._control, 'Save Image')
199 201 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
200 202 dialog.setDefaultSuffix(format.lower())
201 203 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
202 204 if dialog.exec_():
203 205 filename = dialog.selectedFiles()[0]
204 206 image = self._get_image(name)
205 207 image.save(filename, format)
206 208
207 209 def image_tag(self, match, path = None, format = "png"):
208 210 """ Return (X)HTML mark-up for the image-tag given by match.
209 211
210 212 Parameters
211 213 ----------
212 214 match : re.SRE_Match
213 215 A match to an HTML image tag as exported by Qt, with
214 216 match.group("Name") containing the matched image ID.
215 217
216 218 path : string|None, optional [default None]
217 219 If not None, specifies a path to which supporting files
218 220 may be written (e.g., for linked images).
219 221 If None, all images are to be included inline.
220 222
221 223 format : "png"|"svg", optional [default "png"]
222 224 Format for returned or referenced images.
223 225
224 226 Subclasses supporting image display should override this
225 227 method.
226 228 """
227 229
228 230 if(format == "png"):
229 231 try:
230 232 image = self._get_image(match.group("name"))
231 233 except KeyError:
232 234 return "<b>Couldn't find image %s</b>" % match.group("name")
233 235
234 236 if(path is not None):
235 237 if not os.path.exists(path):
236 238 os.mkdir(path)
237 239 relpath = os.path.basename(path)
238 240 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
239 241 "PNG")):
240 242 return '<img src="%s/qt_img%s.png">' % (relpath,
241 243 match.group("name"))
242 244 else:
243 245 return "<b>Couldn't save image!</b>"
244 246 else:
245 247 ba = QtCore.QByteArray()
246 248 buffer_ = QtCore.QBuffer(ba)
247 249 buffer_.open(QtCore.QIODevice.WriteOnly)
248 250 image.save(buffer_, "PNG")
249 251 buffer_.close()
250 252 return '<img src="data:image/png;base64,\n%s\n" />' % (
251 253 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
252 254
253 255 elif(format == "svg"):
254 256 try:
255 257 svg = str(self._name_to_svg[match.group("name")])
256 258 except KeyError:
257 259 return "<b>Couldn't find image %s</b>" % match.group("name")
258 260
259 261 # Not currently checking path, because it's tricky to find a
260 262 # cross-browser way to embed external SVG images (e.g., via
261 263 # object or embed tags).
262 264
263 265 # Chop stand-alone header from matplotlib SVG
264 266 offset = svg.find("<svg")
265 267 assert(offset > -1)
266 268
267 269 return svg[offset:]
268 270
269 271 else:
270 272 return '<b>Unrecognized image format</b>'
271 273
@@ -1,242 +1,242 b''
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 from PyQt4 import QtCore
5 from IPython.external.qt import QtCore
6 6
7 7 # IPython imports.
8 8 from IPython.utils.traitlets import Type
9 9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
10 10 XReqSocketChannel, RepSocketChannel, HBSocketChannel
11 11 from util import MetaQObjectHasTraits, SuperQObject
12 12
13 13
14 14 class SocketChannelQObject(SuperQObject):
15 15
16 16 # Emitted when the channel is started.
17 started = QtCore.pyqtSignal()
17 started = QtCore.Signal()
18 18
19 19 # Emitted when the channel is stopped.
20 stopped = QtCore.pyqtSignal()
20 stopped = QtCore.Signal()
21 21
22 22 #---------------------------------------------------------------------------
23 23 # 'ZmqSocketChannel' interface
24 24 #---------------------------------------------------------------------------
25 25
26 26 def start(self):
27 27 """ Reimplemented to emit signal.
28 28 """
29 29 super(SocketChannelQObject, self).start()
30 30 self.started.emit()
31 31
32 32 def stop(self):
33 33 """ Reimplemented to emit signal.
34 34 """
35 35 super(SocketChannelQObject, self).stop()
36 36 self.stopped.emit()
37 37
38 38
39 39 class QtXReqSocketChannel(SocketChannelQObject, XReqSocketChannel):
40 40
41 41 # Emitted when any message is received.
42 message_received = QtCore.pyqtSignal(object)
42 message_received = QtCore.Signal(object)
43 43
44 44 # Emitted when a reply has been received for the corresponding request
45 45 # type.
46 execute_reply = QtCore.pyqtSignal(object)
47 complete_reply = QtCore.pyqtSignal(object)
48 object_info_reply = QtCore.pyqtSignal(object)
46 execute_reply = QtCore.Signal(object)
47 complete_reply = QtCore.Signal(object)
48 object_info_reply = QtCore.Signal(object)
49 49
50 50 # Emitted when the first reply comes back.
51 first_reply = QtCore.pyqtSignal()
51 first_reply = QtCore.Signal()
52 52
53 53 # Used by the first_reply signal logic to determine if a reply is the
54 54 # first.
55 55 _handlers_called = False
56 56
57 57 #---------------------------------------------------------------------------
58 58 # 'XReqSocketChannel' interface
59 59 #---------------------------------------------------------------------------
60 60
61 61 def call_handlers(self, msg):
62 62 """ Reimplemented to emit signals instead of making callbacks.
63 63 """
64 64 # Emit the generic signal.
65 65 self.message_received.emit(msg)
66 66
67 67 # Emit signals for specialized message types.
68 68 msg_type = msg['msg_type']
69 69 signal = getattr(self, msg_type, None)
70 70 if signal:
71 71 signal.emit(msg)
72 72
73 73 if not self._handlers_called:
74 74 self.first_reply.emit()
75 75 self._handlers_called = True
76 76
77 77 #---------------------------------------------------------------------------
78 78 # 'QtXReqSocketChannel' interface
79 79 #---------------------------------------------------------------------------
80 80
81 81 def reset_first_reply(self):
82 82 """ Reset the first_reply signal to fire again on the next reply.
83 83 """
84 84 self._handlers_called = False
85 85
86 86
87 87 class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel):
88 88
89 89 # Emitted when any message is received.
90 message_received = QtCore.pyqtSignal(object)
90 message_received = QtCore.Signal(object)
91 91
92 92 # Emitted when a message of type 'stream' is received.
93 stream_received = QtCore.pyqtSignal(object)
93 stream_received = QtCore.Signal(object)
94 94
95 95 # Emitted when a message of type 'pyin' is received.
96 pyin_received = QtCore.pyqtSignal(object)
96 pyin_received = QtCore.Signal(object)
97 97
98 98 # Emitted when a message of type 'pyout' is received.
99 pyout_received = QtCore.pyqtSignal(object)
99 pyout_received = QtCore.Signal(object)
100 100
101 101 # Emitted when a message of type 'pyerr' is received.
102 pyerr_received = QtCore.pyqtSignal(object)
102 pyerr_received = QtCore.Signal(object)
103 103
104 104 # Emitted when a message of type 'display_data' is received
105 105 display_data_received = QtCore.pyqtSignal(object)
106 106
107 107 # Emitted when a crash report message is received from the kernel's
108 108 # last-resort sys.excepthook.
109 crash_received = QtCore.pyqtSignal(object)
109 crash_received = QtCore.Signal(object)
110 110
111 111 # Emitted when a shutdown is noticed.
112 shutdown_reply_received = QtCore.pyqtSignal(object)
112 shutdown_reply_received = QtCore.Signal(object)
113 113
114 114 #---------------------------------------------------------------------------
115 115 # 'SubSocketChannel' interface
116 116 #---------------------------------------------------------------------------
117 117
118 118 def call_handlers(self, msg):
119 119 """ Reimplemented to emit signals instead of making callbacks.
120 120 """
121 121 # Emit the generic signal.
122 122 self.message_received.emit(msg)
123 123 # Emit signals for specialized message types.
124 124 msg_type = msg['msg_type']
125 125 signal = getattr(self, msg_type + '_received', None)
126 126 if signal:
127 127 signal.emit(msg)
128 128 elif msg_type in ('stdout', 'stderr'):
129 129 self.stream_received.emit(msg)
130 130
131 131 def flush(self):
132 132 """ Reimplemented to ensure that signals are dispatched immediately.
133 133 """
134 134 super(QtSubSocketChannel, self).flush()
135 135 QtCore.QCoreApplication.instance().processEvents()
136 136
137 137
138 138 class QtRepSocketChannel(SocketChannelQObject, RepSocketChannel):
139 139
140 140 # Emitted when any message is received.
141 message_received = QtCore.pyqtSignal(object)
141 message_received = QtCore.Signal(object)
142 142
143 143 # Emitted when an input request is received.
144 input_requested = QtCore.pyqtSignal(object)
144 input_requested = QtCore.Signal(object)
145 145
146 146 #---------------------------------------------------------------------------
147 147 # 'RepSocketChannel' interface
148 148 #---------------------------------------------------------------------------
149 149
150 150 def call_handlers(self, msg):
151 151 """ Reimplemented to emit signals instead of making callbacks.
152 152 """
153 153 # Emit the generic signal.
154 154 self.message_received.emit(msg)
155 155
156 156 # Emit signals for specialized message types.
157 157 msg_type = msg['msg_type']
158 158 if msg_type == 'input_request':
159 159 self.input_requested.emit(msg)
160 160
161 161
162 162 class QtHBSocketChannel(SocketChannelQObject, HBSocketChannel):
163 163
164 164 # Emitted when the kernel has died.
165 kernel_died = QtCore.pyqtSignal(object)
165 kernel_died = QtCore.Signal(object)
166 166
167 167 #---------------------------------------------------------------------------
168 168 # 'HBSocketChannel' interface
169 169 #---------------------------------------------------------------------------
170 170
171 171 def call_handlers(self, since_last_heartbeat):
172 172 """ Reimplemented to emit signals instead of making callbacks.
173 173 """
174 174 # Emit the generic signal.
175 175 self.kernel_died.emit(since_last_heartbeat)
176 176
177 177
178 178 class QtKernelManager(KernelManager, SuperQObject):
179 179 """ A KernelManager that provides signals and slots.
180 180 """
181 181
182 182 __metaclass__ = MetaQObjectHasTraits
183 183
184 184 # Emitted when the kernel manager has started listening.
185 started_channels = QtCore.pyqtSignal()
185 started_channels = QtCore.Signal()
186 186
187 187 # Emitted when the kernel manager has stopped listening.
188 stopped_channels = QtCore.pyqtSignal()
188 stopped_channels = QtCore.Signal()
189 189
190 190 # Use Qt-specific channel classes that emit signals.
191 191 sub_channel_class = Type(QtSubSocketChannel)
192 192 xreq_channel_class = Type(QtXReqSocketChannel)
193 193 rep_channel_class = Type(QtRepSocketChannel)
194 194 hb_channel_class = Type(QtHBSocketChannel)
195 195
196 196 #---------------------------------------------------------------------------
197 197 # 'KernelManager' interface
198 198 #---------------------------------------------------------------------------
199 199
200 200 #------ Kernel process management ------------------------------------------
201 201
202 202 def start_kernel(self, *args, **kw):
203 203 """ Reimplemented for proper heartbeat management.
204 204 """
205 205 if self._xreq_channel is not None:
206 206 self._xreq_channel.reset_first_reply()
207 207 super(QtKernelManager, self).start_kernel(*args, **kw)
208 208
209 209 #------ Channel management -------------------------------------------------
210 210
211 211 def start_channels(self, *args, **kw):
212 212 """ Reimplemented to emit signal.
213 213 """
214 214 super(QtKernelManager, self).start_channels(*args, **kw)
215 215 self.started_channels.emit()
216 216
217 217 def stop_channels(self):
218 218 """ Reimplemented to emit signal.
219 219 """
220 220 super(QtKernelManager, self).stop_channels()
221 221 self.stopped_channels.emit()
222 222
223 223 @property
224 224 def xreq_channel(self):
225 225 """ Reimplemented for proper heartbeat management.
226 226 """
227 227 if self._xreq_channel is None:
228 228 self._xreq_channel = super(QtKernelManager, self).xreq_channel
229 229 self._xreq_channel.first_reply.connect(self._first_reply)
230 230 return self._xreq_channel
231 231
232 232 #---------------------------------------------------------------------------
233 233 # Protected interface
234 234 #---------------------------------------------------------------------------
235 235
236 236 def _first_reply(self):
237 237 """ Unpauses the heartbeat channel when the first reply is received on
238 238 the execute channel. Note that this will *not* start the heartbeat
239 239 channel if it is not already running!
240 240 """
241 241 if self._hb_channel is not None:
242 242 self._hb_channel.unpause()
@@ -1,89 +1,80 b''
1 1 """ Defines utility functions for working with SVG documents in Qt.
2 2 """
3 3
4 4 # System library imports.
5 from PyQt4 import QtCore, QtGui, QtSvg
5 from IPython.external.qt import QtCore, QtGui, QtSvg
6 6
7 7
8 8 def save_svg(string, parent=None):
9 9 """ Prompts the user to save an SVG document to disk.
10 10
11 11 Parameters:
12 12 -----------
13 13 string : str
14 A Python string or QString containing a SVG document.
14 A Python string containing a SVG document.
15 15
16 16 parent : QWidget, optional
17 17 The parent to use for the file dialog.
18 18
19 19 Returns:
20 20 --------
21 21 The name of the file to which the document was saved, or None if the save
22 22 was cancelled.
23 23 """
24 24 dialog = QtGui.QFileDialog(parent, 'Save SVG Document')
25 25 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
26 26 dialog.setDefaultSuffix('svg')
27 27 dialog.setNameFilter('SVG document (*.svg)')
28 28 if dialog.exec_():
29 29 filename = dialog.selectedFiles()[0]
30 30 f = open(filename, 'w')
31 31 try:
32 32 f.write(string)
33 33 finally:
34 34 f.close()
35 35 return filename
36 36 return None
37 37
38 38 def svg_to_clipboard(string):
39 39 """ Copy a SVG document to the clipboard.
40 40
41 41 Parameters:
42 42 -----------
43 43 string : str
44 A Python string or QString containing a SVG document.
44 A Python string containing a SVG document.
45 45 """
46 if isinstance(string, basestring):
47 bytes = QtCore.QByteArray(string)
48 else:
49 bytes = string.toAscii()
50 46 mime_data = QtCore.QMimeData()
51 mime_data.setData('image/svg+xml', bytes)
47 mime_data.setData('image/svg+xml', string)
52 48 QtGui.QApplication.clipboard().setMimeData(mime_data)
53 49
54 50 def svg_to_image(string, size=None):
55 51 """ Convert a SVG document to a QImage.
56 52
57 53 Parameters:
58 54 -----------
59 55 string : str
60 A Python string or QString containing a SVG document.
56 A Python string containing a SVG document.
61 57
62 58 size : QSize, optional
63 59 The size of the image that is produced. If not specified, the SVG
64 60 document's default size is used.
65 61
66 62 Raises:
67 63 -------
68 64 ValueError
69 65 If an invalid SVG string is provided.
70 66
71 67 Returns:
72 68 --------
73 69 A QImage of format QImage.Format_ARGB32.
74 70 """
75 if isinstance(string, basestring):
76 bytes = QtCore.QByteArray.fromRawData(string) # shallow copy
77 else:
78 bytes = string.toAscii()
79
80 renderer = QtSvg.QSvgRenderer(bytes)
71 renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(string))
81 72 if not renderer.isValid():
82 73 raise ValueError('Invalid SVG data.')
83 74
84 75 if size is None:
85 76 size = renderer.defaultSize()
86 77 image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32)
87 78 painter = QtGui.QPainter(image)
88 79 renderer.render(painter)
89 80 return image
@@ -1,106 +1,106 b''
1 1 """ Defines miscellaneous Qt-related helper classes and functions.
2 2 """
3 3
4 4 # Standard library imports.
5 5 import inspect
6 6
7 7 # System library imports.
8 from PyQt4 import QtCore, QtGui
8 from IPython.external.qt import QtCore, QtGui
9 9
10 10 # IPython imports.
11 11 from IPython.utils.traitlets import HasTraits, TraitType
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Metaclasses
15 15 #-----------------------------------------------------------------------------
16 16
17 17 MetaHasTraits = type(HasTraits)
18 18 MetaQObject = type(QtCore.QObject)
19 19
20 20 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
21 21 """ A metaclass that inherits from the metaclasses of HasTraits and QObject.
22 22
23 23 Using this metaclass allows a class to inherit from both HasTraits and
24 24 QObject. Using SuperQObject instead of QObject is highly recommended. See
25 25 QtKernelManager for an example.
26 26 """
27 27 def __new__(mcls, name, bases, classdict):
28 28 # FIXME: this duplicates the code from MetaHasTraits.
29 29 # I don't think a super() call will help me here.
30 30 for k,v in classdict.iteritems():
31 31 if isinstance(v, TraitType):
32 32 v.name = k
33 33 elif inspect.isclass(v):
34 34 if issubclass(v, TraitType):
35 35 vinst = v()
36 36 vinst.name = k
37 37 classdict[k] = vinst
38 38 cls = MetaQObject.__new__(mcls, name, bases, classdict)
39 39 return cls
40 40
41 41 def __init__(mcls, name, bases, classdict):
42 42 # Note: super() did not work, so we explicitly call these.
43 43 MetaQObject.__init__(mcls, name, bases, classdict)
44 44 MetaHasTraits.__init__(mcls, name, bases, classdict)
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # Classes
48 48 #-----------------------------------------------------------------------------
49 49
50 50 class SuperQObject(QtCore.QObject):
51 51 """ Permits the use of super() in class hierarchies that contain QObject.
52 52
53 53 Unlike QObject, SuperQObject does not accept a QObject parent. If it did,
54 54 super could not be emulated properly (all other classes in the heierarchy
55 55 would have to accept the parent argument--they don't, of course, because
56 56 they don't inherit QObject.)
57 57
58 58 This class is primarily useful for attaching signals to existing non-Qt
59 59 classes. See QtKernelManager for an example.
60 60 """
61 61
62 62 def __new__(cls, *args, **kw):
63 63 # We initialize QObject as early as possible. Without this, Qt complains
64 64 # if SuperQObject is not the first class in the super class list.
65 65 inst = QtCore.QObject.__new__(cls)
66 66 QtCore.QObject.__init__(inst)
67 67 return inst
68 68
69 69 def __init__(self, *args, **kw):
70 70 # Emulate super by calling the next method in the MRO, if there is one.
71 71 mro = self.__class__.mro()
72 72 for qt_class in QtCore.QObject.mro():
73 73 mro.remove(qt_class)
74 74 next_index = mro.index(SuperQObject) + 1
75 75 if next_index < len(mro):
76 76 mro[next_index].__init__(self, *args, **kw)
77 77
78 78 #-----------------------------------------------------------------------------
79 79 # Functions
80 80 #-----------------------------------------------------------------------------
81 81
82 82 def get_font(family, fallback=None):
83 83 """Return a font of the requested family, using fallback as alternative.
84 84
85 85 If a fallback is provided, it is used in case the requested family isn't
86 86 found. If no fallback is given, no alternative is chosen and Qt's internal
87 87 algorithms may automatically choose a fallback font.
88 88
89 89 Parameters
90 90 ----------
91 91 family : str
92 92 A font name.
93 93 fallback : str
94 94 A font name.
95 95
96 96 Returns
97 97 -------
98 98 font : QFont object
99 99 """
100 100 font = QtGui.QFont(family)
101 101 # Check whether we got what we wanted using QFontInfo, since exactMatch()
102 102 # is overly strict and returns false in too many cases.
103 103 font_info = QtGui.QFontInfo(font)
104 104 if fallback is not None and font_info.family() != family:
105 105 font = QtGui.QFont(fallback)
106 106 return font
General Comments 0
You need to be logged in to leave comments. Login now