##// 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 """ Utilities for processing ANSI escape codes and special ASCII characters.
1 """ Utilities for processing ANSI escape codes and special ASCII characters.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 from collections import namedtuple
8 from collections import namedtuple
9 import re
9 import re
10
10
11 # System library imports
11 # System library imports
12 from PyQt4 import QtCore, QtGui
12 from IPython.external.qt import QtCore, QtGui
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Constants and datatypes
15 # Constants and datatypes
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 # An action for erase requests (ED and EL commands).
18 # An action for erase requests (ED and EL commands).
19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
20
20
21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
22 # and HVP commands).
22 # and HVP commands).
23 # FIXME: Not implemented in AnsiCodeProcessor.
23 # FIXME: Not implemented in AnsiCodeProcessor.
24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
25
25
26 # An action for scroll requests (SU and ST) and form feeds.
26 # An action for scroll requests (SU and ST) and form feeds.
27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Classes
30 # Classes
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 class AnsiCodeProcessor(object):
33 class AnsiCodeProcessor(object):
34 """ Translates special ASCII characters and ANSI escape codes into readable
34 """ Translates special ASCII characters and ANSI escape codes into readable
35 attributes.
35 attributes.
36 """
36 """
37
37
38 # Whether to increase intensity or set boldness for SGR code 1.
38 # Whether to increase intensity or set boldness for SGR code 1.
39 # (Different terminals handle this in different ways.)
39 # (Different terminals handle this in different ways.)
40 bold_text_enabled = False
40 bold_text_enabled = False
41
41
42 # Protected class variables.
42 # Protected class variables.
43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
45 _special_pattern = re.compile('([\f])')
45 _special_pattern = re.compile('([\f])')
46
46
47 #---------------------------------------------------------------------------
47 #---------------------------------------------------------------------------
48 # AnsiCodeProcessor interface
48 # AnsiCodeProcessor interface
49 #---------------------------------------------------------------------------
49 #---------------------------------------------------------------------------
50
50
51 def __init__(self):
51 def __init__(self):
52 self.actions = []
52 self.actions = []
53 self.reset_sgr()
53 self.reset_sgr()
54
54
55 def reset_sgr(self):
55 def reset_sgr(self):
56 """ Reset graphics attributs to their default values.
56 """ Reset graphics attributs to their default values.
57 """
57 """
58 self.intensity = 0
58 self.intensity = 0
59 self.italic = False
59 self.italic = False
60 self.bold = False
60 self.bold = False
61 self.underline = False
61 self.underline = False
62 self.foreground_color = None
62 self.foreground_color = None
63 self.background_color = None
63 self.background_color = None
64
64
65 def split_string(self, string):
65 def split_string(self, string):
66 """ Yields substrings for which the same escape code applies.
66 """ Yields substrings for which the same escape code applies.
67 """
67 """
68 self.actions = []
68 self.actions = []
69 start = 0
69 start = 0
70
70
71 for match in self._ansi_pattern.finditer(string):
71 for match in self._ansi_pattern.finditer(string):
72 raw = string[start:match.start()]
72 raw = string[start:match.start()]
73 substring = self._special_pattern.sub(self._replace_special, raw)
73 substring = self._special_pattern.sub(self._replace_special, raw)
74 if substring or self.actions:
74 if substring or self.actions:
75 yield substring
75 yield substring
76 start = match.end()
76 start = match.end()
77
77
78 self.actions = []
78 self.actions = []
79 try:
79 try:
80 params = []
80 params = []
81 for param in match.group(1).split(';'):
81 for param in match.group(1).split(';'):
82 if param:
82 if param:
83 params.append(int(param))
83 params.append(int(param))
84 except ValueError:
84 except ValueError:
85 # Silently discard badly formed escape codes.
85 # Silently discard badly formed escape codes.
86 pass
86 pass
87 else:
87 else:
88 self.set_csi_code(match.group(2), params)
88 self.set_csi_code(match.group(2), params)
89
89
90 raw = string[start:]
90 raw = string[start:]
91 substring = self._special_pattern.sub(self._replace_special, raw)
91 substring = self._special_pattern.sub(self._replace_special, raw)
92 if substring or self.actions:
92 if substring or self.actions:
93 yield substring
93 yield substring
94
94
95 def set_csi_code(self, command, params=[]):
95 def set_csi_code(self, command, params=[]):
96 """ Set attributes based on CSI (Control Sequence Introducer) code.
96 """ Set attributes based on CSI (Control Sequence Introducer) code.
97
97
98 Parameters
98 Parameters
99 ----------
99 ----------
100 command : str
100 command : str
101 The code identifier, i.e. the final character in the sequence.
101 The code identifier, i.e. the final character in the sequence.
102
102
103 params : sequence of integers, optional
103 params : sequence of integers, optional
104 The parameter codes for the command.
104 The parameter codes for the command.
105 """
105 """
106 if command == 'm': # SGR - Select Graphic Rendition
106 if command == 'm': # SGR - Select Graphic Rendition
107 if params:
107 if params:
108 for code in params:
108 for code in params:
109 self.set_sgr_code(code)
109 self.set_sgr_code(code)
110 else:
110 else:
111 self.set_sgr_code(0)
111 self.set_sgr_code(0)
112
112
113 elif (command == 'J' or # ED - Erase Data
113 elif (command == 'J' or # ED - Erase Data
114 command == 'K'): # EL - Erase in Line
114 command == 'K'): # EL - Erase in Line
115 code = params[0] if params else 0
115 code = params[0] if params else 0
116 if 0 <= code <= 2:
116 if 0 <= code <= 2:
117 area = 'screen' if command == 'J' else 'line'
117 area = 'screen' if command == 'J' else 'line'
118 if code == 0:
118 if code == 0:
119 erase_to = 'end'
119 erase_to = 'end'
120 elif code == 1:
120 elif code == 1:
121 erase_to = 'start'
121 erase_to = 'start'
122 elif code == 2:
122 elif code == 2:
123 erase_to = 'all'
123 erase_to = 'all'
124 self.actions.append(EraseAction('erase', area, erase_to))
124 self.actions.append(EraseAction('erase', area, erase_to))
125
125
126 elif (command == 'S' or # SU - Scroll Up
126 elif (command == 'S' or # SU - Scroll Up
127 command == 'T'): # SD - Scroll Down
127 command == 'T'): # SD - Scroll Down
128 dir = 'up' if command == 'S' else 'down'
128 dir = 'up' if command == 'S' else 'down'
129 count = params[0] if params else 1
129 count = params[0] if params else 1
130 self.actions.append(ScrollAction('scroll', dir, 'line', count))
130 self.actions.append(ScrollAction('scroll', dir, 'line', count))
131
131
132 def set_sgr_code(self, code):
132 def set_sgr_code(self, code):
133 """ Set attributes based on SGR (Select Graphic Rendition) code.
133 """ Set attributes based on SGR (Select Graphic Rendition) code.
134 """
134 """
135 if code == 0:
135 if code == 0:
136 self.reset_sgr()
136 self.reset_sgr()
137 elif code == 1:
137 elif code == 1:
138 if self.bold_text_enabled:
138 if self.bold_text_enabled:
139 self.bold = True
139 self.bold = True
140 else:
140 else:
141 self.intensity = 1
141 self.intensity = 1
142 elif code == 2:
142 elif code == 2:
143 self.intensity = 0
143 self.intensity = 0
144 elif code == 3:
144 elif code == 3:
145 self.italic = True
145 self.italic = True
146 elif code == 4:
146 elif code == 4:
147 self.underline = True
147 self.underline = True
148 elif code == 22:
148 elif code == 22:
149 self.intensity = 0
149 self.intensity = 0
150 self.bold = False
150 self.bold = False
151 elif code == 23:
151 elif code == 23:
152 self.italic = False
152 self.italic = False
153 elif code == 24:
153 elif code == 24:
154 self.underline = False
154 self.underline = False
155 elif code >= 30 and code <= 37:
155 elif code >= 30 and code <= 37:
156 self.foreground_color = code - 30
156 self.foreground_color = code - 30
157 elif code == 39:
157 elif code == 39:
158 self.foreground_color = None
158 self.foreground_color = None
159 elif code >= 40 and code <= 47:
159 elif code >= 40 and code <= 47:
160 self.background_color = code - 40
160 self.background_color = code - 40
161 elif code == 49:
161 elif code == 49:
162 self.background_color = None
162 self.background_color = None
163
163
164 #---------------------------------------------------------------------------
164 #---------------------------------------------------------------------------
165 # Protected interface
165 # Protected interface
166 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
167
167
168 def _replace_special(self, match):
168 def _replace_special(self, match):
169 special = match.group(1)
169 special = match.group(1)
170 if special == '\f':
170 if special == '\f':
171 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
171 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
172 return ''
172 return ''
173
173
174
174
175 class QtAnsiCodeProcessor(AnsiCodeProcessor):
175 class QtAnsiCodeProcessor(AnsiCodeProcessor):
176 """ Translates ANSI escape codes into QTextCharFormats.
176 """ Translates ANSI escape codes into QTextCharFormats.
177 """
177 """
178
178
179 # A map from color codes to RGB colors.
179 # A map from color codes to RGB colors.
180 default_map = (# Normal, Bright/Light ANSI color code
180 default_map = (# Normal, Bright/Light ANSI color code
181 ('black', 'grey'), # 0: black
181 ('black', 'grey'), # 0: black
182 ('darkred', 'red'), # 1: red
182 ('darkred', 'red'), # 1: red
183 ('darkgreen', 'lime'), # 2: green
183 ('darkgreen', 'lime'), # 2: green
184 ('brown', 'yellow'), # 3: yellow
184 ('brown', 'yellow'), # 3: yellow
185 ('darkblue', 'deepskyblue'), # 4: blue
185 ('darkblue', 'deepskyblue'), # 4: blue
186 ('darkviolet', 'magenta'), # 5: magenta
186 ('darkviolet', 'magenta'), # 5: magenta
187 ('steelblue', 'cyan'), # 6: cyan
187 ('steelblue', 'cyan'), # 6: cyan
188 ('grey', 'white')) # 7: white
188 ('grey', 'white')) # 7: white
189
189
190 def __init__(self):
190 def __init__(self):
191 super(QtAnsiCodeProcessor, self).__init__()
191 super(QtAnsiCodeProcessor, self).__init__()
192 self.color_map = self.default_map
192 self.color_map = self.default_map
193
193
194 def get_format(self):
194 def get_format(self):
195 """ Returns a QTextCharFormat that encodes the current style attributes.
195 """ Returns a QTextCharFormat that encodes the current style attributes.
196 """
196 """
197 format = QtGui.QTextCharFormat()
197 format = QtGui.QTextCharFormat()
198
198
199 # Set foreground color
199 # Set foreground color
200 if self.foreground_color is not None:
200 if self.foreground_color is not None:
201 color = self.color_map[self.foreground_color][self.intensity]
201 color = self.color_map[self.foreground_color][self.intensity]
202 format.setForeground(QtGui.QColor(color))
202 format.setForeground(QtGui.QColor(color))
203
203
204 # Set background color
204 # Set background color
205 if self.background_color is not None:
205 if self.background_color is not None:
206 color = self.color_map[self.background_color][self.intensity]
206 color = self.color_map[self.background_color][self.intensity]
207 format.setBackground(QtGui.QColor(color))
207 format.setBackground(QtGui.QColor(color))
208
208
209 # Set font weight/style options
209 # Set font weight/style options
210 if self.bold:
210 if self.bold:
211 format.setFontWeight(QtGui.QFont.Bold)
211 format.setFontWeight(QtGui.QFont.Bold)
212 else:
212 else:
213 format.setFontWeight(QtGui.QFont.Normal)
213 format.setFontWeight(QtGui.QFont.Normal)
214 format.setFontItalic(self.italic)
214 format.setFontItalic(self.italic)
215 format.setFontUnderline(self.underline)
215 format.setFontUnderline(self.underline)
216
216
217 return format
217 return format
218
218
219 def set_background_color(self, color):
219 def set_background_color(self, color):
220 """ Given a background color (a QColor), attempt to set a color map
220 """ Given a background color (a QColor), attempt to set a color map
221 that will be aesthetically pleasing.
221 that will be aesthetically pleasing.
222 """
222 """
223 if color.value() < 127:
223 if color.value() < 127:
224 # Colors appropriate for a terminal with a dark background.
224 # Colors appropriate for a terminal with a dark background.
225 self.color_map = self.default_map
225 self.color_map = self.default_map
226
226
227 else:
227 else:
228 # Colors appropriate for a terminal with a light background. For
228 # Colors appropriate for a terminal with a light background. For
229 # now, only use non-bright colors...
229 # now, only use non-bright colors...
230 self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]
230 self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]
231
231
232 # ...and replace white with black.
232 # ...and replace white with black.
233 self.color_map[7] = ('black', 'black')
233 self.color_map[7] = ('black', 'black')
@@ -1,101 +1,100 b''
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
2 """
2 """
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from IPython.external.qt import QtCore, QtGui
6
6
7
7
8 class BracketMatcher(QtCore.QObject):
8 class BracketMatcher(QtCore.QObject):
9 """ Matches square brackets, braces, and parentheses based on cursor
9 """ Matches square brackets, braces, and parentheses based on cursor
10 position.
10 position.
11 """
11 """
12
12
13 # Protected class variables.
13 # Protected class variables.
14 _opening_map = { '(':')', '{':'}', '[':']' }
14 _opening_map = { '(':')', '{':'}', '[':']' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
16
16
17 #--------------------------------------------------------------------------
17 #--------------------------------------------------------------------------
18 # 'QObject' interface
18 # 'QObject' interface
19 #--------------------------------------------------------------------------
19 #--------------------------------------------------------------------------
20
20
21 def __init__(self, text_edit):
21 def __init__(self, text_edit):
22 """ Create a call tip manager that is attached to the specified Qt
22 """ Create a call tip manager that is attached to the specified Qt
23 text edit widget.
23 text edit widget.
24 """
24 """
25 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
25 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
26 super(BracketMatcher, self).__init__()
26 super(BracketMatcher, self).__init__()
27
27
28 # The format to apply to matching brackets.
28 # The format to apply to matching brackets.
29 self.format = QtGui.QTextCharFormat()
29 self.format = QtGui.QTextCharFormat()
30 self.format.setBackground(QtGui.QColor('silver'))
30 self.format.setBackground(QtGui.QColor('silver'))
31
31
32 self._text_edit = text_edit
32 self._text_edit = text_edit
33 text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
33 text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
34
34
35 #--------------------------------------------------------------------------
35 #--------------------------------------------------------------------------
36 # Protected interface
36 # Protected interface
37 #--------------------------------------------------------------------------
37 #--------------------------------------------------------------------------
38
38
39 def _find_match(self, position):
39 def _find_match(self, position):
40 """ Given a valid position in the text document, try to find the
40 """ Given a valid position in the text document, try to find the
41 position of the matching bracket. Returns -1 if unsuccessful.
41 position of the matching bracket. Returns -1 if unsuccessful.
42 """
42 """
43 # Decide what character to search for and what direction to search in.
43 # Decide what character to search for and what direction to search in.
44 document = self._text_edit.document()
44 document = self._text_edit.document()
45 qchar = document.characterAt(position)
45 start_char = document.characterAt(position)
46 start_char = qchar.toAscii()
47 search_char = self._opening_map.get(start_char)
46 search_char = self._opening_map.get(start_char)
48 if search_char:
47 if search_char:
49 increment = 1
48 increment = 1
50 else:
49 else:
51 search_char = self._closing_map.get(start_char)
50 search_char = self._closing_map.get(start_char)
52 if search_char:
51 if search_char:
53 increment = -1
52 increment = -1
54 else:
53 else:
55 return -1
54 return -1
56
55
57 # Search for the character.
56 # Search for the character.
57 char = start_char
58 depth = 0
58 depth = 0
59 while position >= 0 and position < document.characterCount():
59 while position >= 0 and position < document.characterCount():
60 char = qchar.toAscii()
61 if char == start_char:
60 if char == start_char:
62 depth += 1
61 depth += 1
63 elif char == search_char:
62 elif char == search_char:
64 depth -= 1
63 depth -= 1
65 if depth == 0:
64 if depth == 0:
66 break
65 break
67 position += increment
66 position += increment
68 qchar = document.characterAt(position)
67 char = document.characterAt(position)
69 else:
68 else:
70 position = -1
69 position = -1
71 return position
70 return position
72
71
73 def _selection_for_character(self, position):
72 def _selection_for_character(self, position):
74 """ Convenience method for selecting a character.
73 """ Convenience method for selecting a character.
75 """
74 """
76 selection = QtGui.QTextEdit.ExtraSelection()
75 selection = QtGui.QTextEdit.ExtraSelection()
77 cursor = self._text_edit.textCursor()
76 cursor = self._text_edit.textCursor()
78 cursor.setPosition(position)
77 cursor.setPosition(position)
79 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
78 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
80 QtGui.QTextCursor.KeepAnchor)
79 QtGui.QTextCursor.KeepAnchor)
81 selection.cursor = cursor
80 selection.cursor = cursor
82 selection.format = self.format
81 selection.format = self.format
83 return selection
82 return selection
84
83
85 #------ Signal handlers ----------------------------------------------------
84 #------ Signal handlers ----------------------------------------------------
86
85
87 def _cursor_position_changed(self):
86 def _cursor_position_changed(self):
88 """ Updates the document formatting based on the new cursor position.
87 """ Updates the document formatting based on the new cursor position.
89 """
88 """
90 # Clear out the old formatting.
89 # Clear out the old formatting.
91 self._text_edit.setExtraSelections([])
90 self._text_edit.setExtraSelections([])
92
91
93 # Attempt to match a bracket for the new cursor position.
92 # Attempt to match a bracket for the new cursor position.
94 cursor = self._text_edit.textCursor()
93 cursor = self._text_edit.textCursor()
95 if not cursor.hasSelection():
94 if not cursor.hasSelection():
96 position = cursor.position() - 1
95 position = cursor.position() - 1
97 match_position = self._find_match(position)
96 match_position = self._find_match(position)
98 if match_position != -1:
97 if match_position != -1:
99 extra_selections = [ self._selection_for_character(pos)
98 extra_selections = [ self._selection_for_character(pos)
100 for pos in (position, match_position) ]
99 for pos in (position, match_position) ]
101 self._text_edit.setExtraSelections(extra_selections)
100 self._text_edit.setExtraSelections(extra_selections)
@@ -1,225 +1,225 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 from textwrap import dedent
3 from textwrap import dedent
4 from unicodedata import category
4
5
5 # System library imports
6 # System library imports
6 from PyQt4 import QtCore, QtGui
7 from IPython.external.qt import QtCore, QtGui
7
8
8
9
9 class CallTipWidget(QtGui.QLabel):
10 class CallTipWidget(QtGui.QLabel):
10 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
11 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
11 """
12 """
12
13
13 #--------------------------------------------------------------------------
14 #--------------------------------------------------------------------------
14 # 'QObject' interface
15 # 'QObject' interface
15 #--------------------------------------------------------------------------
16 #--------------------------------------------------------------------------
16
17
17 def __init__(self, text_edit):
18 def __init__(self, text_edit):
18 """ Create a call tip manager that is attached to the specified Qt
19 """ Create a call tip manager that is attached to the specified Qt
19 text edit widget.
20 text edit widget.
20 """
21 """
21 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
23 super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
23
24
24 self._hide_timer = QtCore.QBasicTimer()
25 self._hide_timer = QtCore.QBasicTimer()
25 self._text_edit = text_edit
26 self._text_edit = text_edit
26
27
27 self.setFont(text_edit.document().defaultFont())
28 self.setFont(text_edit.document().defaultFont())
28 self.setForegroundRole(QtGui.QPalette.ToolTipText)
29 self.setForegroundRole(QtGui.QPalette.ToolTipText)
29 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
30 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
30 self.setPalette(QtGui.QToolTip.palette())
31 self.setPalette(QtGui.QToolTip.palette())
31
32
32 self.setAlignment(QtCore.Qt.AlignLeft)
33 self.setAlignment(QtCore.Qt.AlignLeft)
33 self.setIndent(1)
34 self.setIndent(1)
34 self.setFrameStyle(QtGui.QFrame.NoFrame)
35 self.setFrameStyle(QtGui.QFrame.NoFrame)
35 self.setMargin(1 + self.style().pixelMetric(
36 self.setMargin(1 + self.style().pixelMetric(
36 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
37 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
37 self.setWindowOpacity(self.style().styleHint(
38 self.setWindowOpacity(self.style().styleHint(
38 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
39 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
39
40
40 def eventFilter(self, obj, event):
41 def eventFilter(self, obj, event):
41 """ Reimplemented to hide on certain key presses and on text edit focus
42 """ Reimplemented to hide on certain key presses and on text edit focus
42 changes.
43 changes.
43 """
44 """
44 if obj == self._text_edit:
45 if obj == self._text_edit:
45 etype = event.type()
46 etype = event.type()
46
47
47 if etype == QtCore.QEvent.KeyPress:
48 if etype == QtCore.QEvent.KeyPress:
48 key = event.key()
49 key = event.key()
49 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
50 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
50 self.hide()
51 self.hide()
51 elif key == QtCore.Qt.Key_Escape:
52 elif key == QtCore.Qt.Key_Escape:
52 self.hide()
53 self.hide()
53 return True
54 return True
54
55
55 elif etype == QtCore.QEvent.FocusOut:
56 elif etype == QtCore.QEvent.FocusOut:
56 self.hide()
57 self.hide()
57
58
58 elif etype == QtCore.QEvent.Enter:
59 elif etype == QtCore.QEvent.Enter:
59 self._hide_timer.stop()
60 self._hide_timer.stop()
60
61
61 elif etype == QtCore.QEvent.Leave:
62 elif etype == QtCore.QEvent.Leave:
62 self._hide_later()
63 self._hide_later()
63
64
64 return super(CallTipWidget, self).eventFilter(obj, event)
65 return super(CallTipWidget, self).eventFilter(obj, event)
65
66
66 def timerEvent(self, event):
67 def timerEvent(self, event):
67 """ Reimplemented to hide the widget when the hide timer fires.
68 """ Reimplemented to hide the widget when the hide timer fires.
68 """
69 """
69 if event.timerId() == self._hide_timer.timerId():
70 if event.timerId() == self._hide_timer.timerId():
70 self._hide_timer.stop()
71 self._hide_timer.stop()
71 self.hide()
72 self.hide()
72
73
73 #--------------------------------------------------------------------------
74 #--------------------------------------------------------------------------
74 # 'QWidget' interface
75 # 'QWidget' interface
75 #--------------------------------------------------------------------------
76 #--------------------------------------------------------------------------
76
77
77 def enterEvent(self, event):
78 def enterEvent(self, event):
78 """ Reimplemented to cancel the hide timer.
79 """ Reimplemented to cancel the hide timer.
79 """
80 """
80 super(CallTipWidget, self).enterEvent(event)
81 super(CallTipWidget, self).enterEvent(event)
81 self._hide_timer.stop()
82 self._hide_timer.stop()
82
83
83 def hideEvent(self, event):
84 def hideEvent(self, event):
84 """ Reimplemented to disconnect signal handlers and event filter.
85 """ Reimplemented to disconnect signal handlers and event filter.
85 """
86 """
86 super(CallTipWidget, self).hideEvent(event)
87 super(CallTipWidget, self).hideEvent(event)
87 self._text_edit.cursorPositionChanged.disconnect(
88 self._text_edit.cursorPositionChanged.disconnect(
88 self._cursor_position_changed)
89 self._cursor_position_changed)
89 self._text_edit.removeEventFilter(self)
90 self._text_edit.removeEventFilter(self)
90
91
91 def leaveEvent(self, event):
92 def leaveEvent(self, event):
92 """ Reimplemented to start the hide timer.
93 """ Reimplemented to start the hide timer.
93 """
94 """
94 super(CallTipWidget, self).leaveEvent(event)
95 super(CallTipWidget, self).leaveEvent(event)
95 self._hide_later()
96 self._hide_later()
96
97
97 def paintEvent(self, event):
98 def paintEvent(self, event):
98 """ Reimplemented to paint the background panel.
99 """ Reimplemented to paint the background panel.
99 """
100 """
100 painter = QtGui.QStylePainter(self)
101 painter = QtGui.QStylePainter(self)
101 option = QtGui.QStyleOptionFrame()
102 option = QtGui.QStyleOptionFrame()
102 option.init(self)
103 option.init(self)
103 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
104 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
104 painter.end()
105 painter.end()
105
106
106 super(CallTipWidget, self).paintEvent(event)
107 super(CallTipWidget, self).paintEvent(event)
107
108
108 def setFont(self, font):
109 def setFont(self, font):
109 """ Reimplemented to allow use of this method as a slot.
110 """ Reimplemented to allow use of this method as a slot.
110 """
111 """
111 super(CallTipWidget, self).setFont(font)
112 super(CallTipWidget, self).setFont(font)
112
113
113 def showEvent(self, event):
114 def showEvent(self, event):
114 """ Reimplemented to connect signal handlers and event filter.
115 """ Reimplemented to connect signal handlers and event filter.
115 """
116 """
116 super(CallTipWidget, self).showEvent(event)
117 super(CallTipWidget, self).showEvent(event)
117 self._text_edit.cursorPositionChanged.connect(
118 self._text_edit.cursorPositionChanged.connect(
118 self._cursor_position_changed)
119 self._cursor_position_changed)
119 self._text_edit.installEventFilter(self)
120 self._text_edit.installEventFilter(self)
120
121
121 #--------------------------------------------------------------------------
122 #--------------------------------------------------------------------------
122 # 'CallTipWidget' interface
123 # 'CallTipWidget' interface
123 #--------------------------------------------------------------------------
124 #--------------------------------------------------------------------------
124
125
125 def show_call_info(self, call_line=None, doc=None, maxlines=20):
126 def show_call_info(self, call_line=None, doc=None, maxlines=20):
126 """ Attempts to show the specified call line and docstring at the
127 """ Attempts to show the specified call line and docstring at the
127 current cursor location. The docstring is possibly truncated for
128 current cursor location. The docstring is possibly truncated for
128 length.
129 length.
129 """
130 """
130 if doc:
131 if doc:
131 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
132 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
132 if match:
133 if match:
133 doc = doc[:match.end()] + '\n[Documentation continues...]'
134 doc = doc[:match.end()] + '\n[Documentation continues...]'
134 else:
135 else:
135 doc = ''
136 doc = ''
136
137
137 if call_line:
138 if call_line:
138 doc = '\n\n'.join([call_line, doc])
139 doc = '\n\n'.join([call_line, doc])
139 return self.show_tip(doc)
140 return self.show_tip(doc)
140
141
141 def show_tip(self, tip):
142 def show_tip(self, tip):
142 """ Attempts to show the specified tip at the current cursor location.
143 """ Attempts to show the specified tip at the current cursor location.
143 """
144 """
144 # Attempt to find the cursor position at which to show the call tip.
145 # Attempt to find the cursor position at which to show the call tip.
145 text_edit = self._text_edit
146 text_edit = self._text_edit
146 document = text_edit.document()
147 document = text_edit.document()
147 cursor = text_edit.textCursor()
148 cursor = text_edit.textCursor()
148 search_pos = cursor.position() - 1
149 search_pos = cursor.position() - 1
149 self._start_position, _ = self._find_parenthesis(search_pos,
150 self._start_position, _ = self._find_parenthesis(search_pos,
150 forward=False)
151 forward=False)
151 if self._start_position == -1:
152 if self._start_position == -1:
152 return False
153 return False
153
154
154 # Set the text and resize the widget accordingly.
155 # Set the text and resize the widget accordingly.
155 self.setText(tip)
156 self.setText(tip)
156 self.resize(self.sizeHint())
157 self.resize(self.sizeHint())
157
158
158 # Locate and show the widget. Place the tip below the current line
159 # Locate and show the widget. Place the tip below the current line
159 # unless it would be off the screen. In that case, place it above
160 # unless it would be off the screen. In that case, place it above
160 # the current line.
161 # the current line.
161 padding = 3 # Distance in pixels between cursor bounds and tip box.
162 padding = 3 # Distance in pixels between cursor bounds and tip box.
162 cursor_rect = text_edit.cursorRect(cursor)
163 cursor_rect = text_edit.cursorRect(cursor)
163 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
164 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
164 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
165 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
165 point.setY(point.y() + padding)
166 point.setY(point.y() + padding)
166 tip_height = self.size().height()
167 tip_height = self.size().height()
167 if point.y() + tip_height > screen_rect.height():
168 if point.y() + tip_height > screen_rect.height():
168 point = text_edit.mapToGlobal(cursor_rect.topRight())
169 point = text_edit.mapToGlobal(cursor_rect.topRight())
169 point.setY(point.y() - tip_height - padding)
170 point.setY(point.y() - tip_height - padding)
170 self.move(point)
171 self.move(point)
171 self.show()
172 self.show()
172 return True
173 return True
173
174
174 #--------------------------------------------------------------------------
175 #--------------------------------------------------------------------------
175 # Protected interface
176 # Protected interface
176 #--------------------------------------------------------------------------
177 #--------------------------------------------------------------------------
177
178
178 def _find_parenthesis(self, position, forward=True):
179 def _find_parenthesis(self, position, forward=True):
179 """ If 'forward' is True (resp. False), proceed forwards
180 """ If 'forward' is True (resp. False), proceed forwards
180 (resp. backwards) through the line that contains 'position' until an
181 (resp. backwards) through the line that contains 'position' until an
181 unmatched closing (resp. opening) parenthesis is found. Returns a
182 unmatched closing (resp. opening) parenthesis is found. Returns a
182 tuple containing the position of this parenthesis (or -1 if it is
183 tuple containing the position of this parenthesis (or -1 if it is
183 not found) and the number commas (at depth 0) found along the way.
184 not found) and the number commas (at depth 0) found along the way.
184 """
185 """
185 commas = depth = 0
186 commas = depth = 0
186 document = self._text_edit.document()
187 document = self._text_edit.document()
187 qchar = document.characterAt(position)
188 char = document.characterAt(position)
188 while (position > 0 and qchar.isPrint() and
189 # Search until a match is found or a non-printable character is
189 # Need to check explicitly for line/paragraph separators:
190 # encountered.
190 qchar.unicode() not in (0x2028, 0x2029)):
191 while category(char) != 'Cc' and position > 0:
191 char = qchar.toAscii()
192 if char == ',' and depth == 0:
192 if char == ',' and depth == 0:
193 commas += 1
193 commas += 1
194 elif char == ')':
194 elif char == ')':
195 if forward and depth == 0:
195 if forward and depth == 0:
196 break
196 break
197 depth += 1
197 depth += 1
198 elif char == '(':
198 elif char == '(':
199 if not forward and depth == 0:
199 if not forward and depth == 0:
200 break
200 break
201 depth -= 1
201 depth -= 1
202 position += 1 if forward else -1
202 position += 1 if forward else -1
203 qchar = document.characterAt(position)
203 char = document.characterAt(position)
204 else:
204 else:
205 position = -1
205 position = -1
206 return position, commas
206 return position, commas
207
207
208 def _hide_later(self):
208 def _hide_later(self):
209 """ Hides the tooltip after some time has passed.
209 """ Hides the tooltip after some time has passed.
210 """
210 """
211 if not self._hide_timer.isActive():
211 if not self._hide_timer.isActive():
212 self._hide_timer.start(300, self)
212 self._hide_timer.start(300, self)
213
213
214 #------ Signal handlers ----------------------------------------------------
214 #------ Signal handlers ----------------------------------------------------
215
215
216 def _cursor_position_changed(self):
216 def _cursor_position_changed(self):
217 """ Updates the tip based on user cursor movement.
217 """ Updates the tip based on user cursor movement.
218 """
218 """
219 cursor = self._text_edit.textCursor()
219 cursor = self._text_edit.textCursor()
220 if cursor.position() <= self._start_position:
220 if cursor.position() <= self._start_position:
221 self.hide()
221 self.hide()
222 else:
222 else:
223 position, commas = self._find_parenthesis(self._start_position + 1)
223 position, commas = self._find_parenthesis(self._start_position + 1)
224 if position != -1:
224 if position != -1:
225 self.hide()
225 self.hide()
@@ -1,133 +1,133 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from IPython.external.qt import QtCore, QtGui
3
3
4
4
5 class CompletionWidget(QtGui.QListWidget):
5 class CompletionWidget(QtGui.QListWidget):
6 """ A widget for GUI tab completion.
6 """ A widget for GUI tab completion.
7 """
7 """
8
8
9 #--------------------------------------------------------------------------
9 #--------------------------------------------------------------------------
10 # 'QObject' interface
10 # 'QObject' interface
11 #--------------------------------------------------------------------------
11 #--------------------------------------------------------------------------
12
12
13 def __init__(self, text_edit):
13 def __init__(self, text_edit):
14 """ Create a completion widget that is attached to the specified Qt
14 """ Create a completion widget that is attached to the specified Qt
15 text edit widget.
15 text edit widget.
16 """
16 """
17 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
17 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 super(CompletionWidget, self).__init__()
18 super(CompletionWidget, self).__init__()
19
19
20 self._text_edit = text_edit
20 self._text_edit = text_edit
21
21
22 self.setAttribute(QtCore.Qt.WA_StaticContents)
22 self.setAttribute(QtCore.Qt.WA_StaticContents)
23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
24
24
25 # Ensure that the text edit keeps focus when widget is displayed.
25 # Ensure that the text edit keeps focus when widget is displayed.
26 self.setFocusProxy(self._text_edit)
26 self.setFocusProxy(self._text_edit)
27
27
28 self.setFrameShadow(QtGui.QFrame.Plain)
28 self.setFrameShadow(QtGui.QFrame.Plain)
29 self.setFrameShape(QtGui.QFrame.StyledPanel)
29 self.setFrameShape(QtGui.QFrame.StyledPanel)
30
30
31 self.itemActivated.connect(self._complete_current)
31 self.itemActivated.connect(self._complete_current)
32
32
33 def eventFilter(self, obj, event):
33 def eventFilter(self, obj, event):
34 """ Reimplemented to handle keyboard input and to auto-hide when the
34 """ Reimplemented to handle keyboard input and to auto-hide when the
35 text edit loses focus.
35 text edit loses focus.
36 """
36 """
37 if obj == self._text_edit:
37 if obj == self._text_edit:
38 etype = event.type()
38 etype = event.type()
39
39
40 if etype == QtCore.QEvent.KeyPress:
40 if etype == QtCore.QEvent.KeyPress:
41 key, text = event.key(), event.text()
41 key, text = event.key(), event.text()
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
43 QtCore.Qt.Key_Tab):
43 QtCore.Qt.Key_Tab):
44 self._complete_current()
44 self._complete_current()
45 return True
45 return True
46 elif key == QtCore.Qt.Key_Escape:
46 elif key == QtCore.Qt.Key_Escape:
47 self.hide()
47 self.hide()
48 return True
48 return True
49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
52 self.keyPressEvent(event)
52 self.keyPressEvent(event)
53 return True
53 return True
54
54
55 elif etype == QtCore.QEvent.FocusOut:
55 elif etype == QtCore.QEvent.FocusOut:
56 self.hide()
56 self.hide()
57
57
58 return super(CompletionWidget, self).eventFilter(obj, event)
58 return super(CompletionWidget, self).eventFilter(obj, event)
59
59
60 #--------------------------------------------------------------------------
60 #--------------------------------------------------------------------------
61 # 'QWidget' interface
61 # 'QWidget' interface
62 #--------------------------------------------------------------------------
62 #--------------------------------------------------------------------------
63
63
64 def hideEvent(self, event):
64 def hideEvent(self, event):
65 """ Reimplemented to disconnect signal handlers and event filter.
65 """ Reimplemented to disconnect signal handlers and event filter.
66 """
66 """
67 super(CompletionWidget, self).hideEvent(event)
67 super(CompletionWidget, self).hideEvent(event)
68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
69 self._text_edit.removeEventFilter(self)
69 self._text_edit.removeEventFilter(self)
70
70
71 def showEvent(self, event):
71 def showEvent(self, event):
72 """ Reimplemented to connect signal handlers and event filter.
72 """ Reimplemented to connect signal handlers and event filter.
73 """
73 """
74 super(CompletionWidget, self).showEvent(event)
74 super(CompletionWidget, self).showEvent(event)
75 self._text_edit.cursorPositionChanged.connect(self._update_current)
75 self._text_edit.cursorPositionChanged.connect(self._update_current)
76 self._text_edit.installEventFilter(self)
76 self._text_edit.installEventFilter(self)
77
77
78 #--------------------------------------------------------------------------
78 #--------------------------------------------------------------------------
79 # 'CompletionWidget' interface
79 # 'CompletionWidget' interface
80 #--------------------------------------------------------------------------
80 #--------------------------------------------------------------------------
81
81
82 def show_items(self, cursor, items):
82 def show_items(self, cursor, items):
83 """ Shows the completion widget with 'items' at the position specified
83 """ Shows the completion widget with 'items' at the position specified
84 by 'cursor'.
84 by 'cursor'.
85 """
85 """
86 text_edit = self._text_edit
86 text_edit = self._text_edit
87 point = text_edit.cursorRect(cursor).bottomRight()
87 point = text_edit.cursorRect(cursor).bottomRight()
88 point = text_edit.mapToGlobal(point)
88 point = text_edit.mapToGlobal(point)
89 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
89 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
90 if screen_rect.size().height() - point.y() - self.height() < 0:
90 if screen_rect.size().height() - point.y() - self.height() < 0:
91 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
91 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
92 point.setY(point.y() - self.height())
92 point.setY(point.y() - self.height())
93 self.move(point)
93 self.move(point)
94
94
95 self._start_position = cursor.position()
95 self._start_position = cursor.position()
96 self.clear()
96 self.clear()
97 self.addItems(items)
97 self.addItems(items)
98 self.setCurrentRow(0)
98 self.setCurrentRow(0)
99 self.show()
99 self.show()
100
100
101 #--------------------------------------------------------------------------
101 #--------------------------------------------------------------------------
102 # Protected interface
102 # Protected interface
103 #--------------------------------------------------------------------------
103 #--------------------------------------------------------------------------
104
104
105 def _complete_current(self):
105 def _complete_current(self):
106 """ Perform the completion with the currently selected item.
106 """ Perform the completion with the currently selected item.
107 """
107 """
108 self._current_text_cursor().insertText(self.currentItem().text())
108 self._current_text_cursor().insertText(self.currentItem().text())
109 self.hide()
109 self.hide()
110
110
111 def _current_text_cursor(self):
111 def _current_text_cursor(self):
112 """ Returns a cursor with text between the start position and the
112 """ Returns a cursor with text between the start position and the
113 current position selected.
113 current position selected.
114 """
114 """
115 cursor = self._text_edit.textCursor()
115 cursor = self._text_edit.textCursor()
116 if cursor.position() >= self._start_position:
116 if cursor.position() >= self._start_position:
117 cursor.setPosition(self._start_position,
117 cursor.setPosition(self._start_position,
118 QtGui.QTextCursor.KeepAnchor)
118 QtGui.QTextCursor.KeepAnchor)
119 return cursor
119 return cursor
120
120
121 def _update_current(self):
121 def _update_current(self):
122 """ Updates the current item based on the current text.
122 """ Updates the current item based on the current text.
123 """
123 """
124 prefix = self._current_text_cursor().selection().toPlainText()
124 prefix = self._current_text_cursor().selection().toPlainText()
125 if prefix:
125 if prefix:
126 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
126 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
127 QtCore.Qt.MatchCaseSensitive))
127 QtCore.Qt.MatchCaseSensitive))
128 if items:
128 if items:
129 self.setCurrentItem(items[0])
129 self.setCurrentItem(items[0])
130 else:
130 else:
131 self.hide()
131 self.hide()
132 else:
132 else:
133 self.hide()
133 self.hide()
@@ -1,1895 +1,1906 b''
1 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os
8 from os.path import commonprefix
9 from os.path import commonprefix
9 import re
10 import re
10 import os
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13
14
14 # System library imports
15 # System library imports
15 from PyQt4 import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
16
17
17 # Local imports
18 # Local imports
18 from IPython.config.configurable import Configurable
19 from IPython.config.configurable import Configurable
19 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
20 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
20 from IPython.utils.traitlets import Bool, Enum, Int
21 from IPython.utils.traitlets import Bool, Enum, Int
21 from ansi_code_processor import QtAnsiCodeProcessor
22 from ansi_code_processor import QtAnsiCodeProcessor
22 from completion_widget import CompletionWidget
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 # Classes
36 # Classes
26 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
27
38
28 class ConsoleWidget(Configurable, QtGui.QWidget):
39 class ConsoleWidget(Configurable, QtGui.QWidget):
29 """ An abstract base class for console-type widgets. This class has
40 """ An abstract base class for console-type widgets. This class has
30 functionality for:
41 functionality for:
31
42
32 * Maintaining a prompt and editing region
43 * Maintaining a prompt and editing region
33 * Providing the traditional Unix-style console keyboard shortcuts
44 * Providing the traditional Unix-style console keyboard shortcuts
34 * Performing tab completion
45 * Performing tab completion
35 * Paging text
46 * Paging text
36 * Handling ANSI escape codes
47 * Handling ANSI escape codes
37
48
38 ConsoleWidget also provides a number of utility methods that will be
49 ConsoleWidget also provides a number of utility methods that will be
39 convenient to implementors of a console-style widget.
50 convenient to implementors of a console-style widget.
40 """
51 """
41 __metaclass__ = MetaQObjectHasTraits
52 __metaclass__ = MetaQObjectHasTraits
42
53
43 #------ Configuration ------------------------------------------------------
54 #------ Configuration ------------------------------------------------------
44
55
45 # Whether to process ANSI escape codes.
56 # Whether to process ANSI escape codes.
46 ansi_codes = Bool(True, config=True)
57 ansi_codes = Bool(True, config=True)
47
58
48 # The maximum number of lines of text before truncation. Specifying a
59 # The maximum number of lines of text before truncation. Specifying a
49 # non-positive number disables text truncation (not recommended).
60 # non-positive number disables text truncation (not recommended).
50 buffer_size = Int(500, config=True)
61 buffer_size = Int(500, config=True)
51
62
52 # Whether to use a list widget or plain text output for tab completion.
63 # Whether to use a list widget or plain text output for tab completion.
53 gui_completion = Bool(False, config=True)
64 gui_completion = Bool(False, config=True)
54
65
55 # The type of underlying text widget to use. Valid values are 'plain', which
66 # The type of underlying text widget to use. Valid values are 'plain', which
56 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
67 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
57 # NOTE: this value can only be specified during initialization.
68 # NOTE: this value can only be specified during initialization.
58 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
69 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
59
70
60 # The type of paging to use. Valid values are:
71 # The type of paging to use. Valid values are:
61 # 'inside' : The widget pages like a traditional terminal.
72 # 'inside' : The widget pages like a traditional terminal.
62 # 'hsplit' : When paging is requested, the widget is split
73 # 'hsplit' : When paging is requested, the widget is split
63 # horizontally. The top pane contains the console, and the
74 # horizontally. The top pane contains the console, and the
64 # bottom pane contains the paged text.
75 # bottom pane contains the paged text.
65 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
76 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
66 # 'custom' : No action is taken by the widget beyond emitting a
77 # 'custom' : No action is taken by the widget beyond emitting a
67 # 'custom_page_requested(str)' signal.
78 # 'custom_page_requested(str)' signal.
68 # 'none' : The text is written directly to the console.
79 # 'none' : The text is written directly to the console.
69 # NOTE: this value can only be specified during initialization.
80 # NOTE: this value can only be specified during initialization.
70 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
81 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
71 default_value='inside', config=True)
82 default_value='inside', config=True)
72
83
73 # Whether to override ShortcutEvents for the keybindings defined by this
84 # Whether to override ShortcutEvents for the keybindings defined by this
74 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
85 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
75 # priority (when it has focus) over, e.g., window-level menu shortcuts.
86 # priority (when it has focus) over, e.g., window-level menu shortcuts.
76 override_shortcuts = Bool(False)
87 override_shortcuts = Bool(False)
77
88
78 #------ Signals ------------------------------------------------------------
89 #------ Signals ------------------------------------------------------------
79
90
80 # Signals that indicate ConsoleWidget state.
91 # Signals that indicate ConsoleWidget state.
81 copy_available = QtCore.pyqtSignal(bool)
92 copy_available = QtCore.Signal(bool)
82 redo_available = QtCore.pyqtSignal(bool)
93 redo_available = QtCore.Signal(bool)
83 undo_available = QtCore.pyqtSignal(bool)
94 undo_available = QtCore.Signal(bool)
84
95
85 # Signal emitted when paging is needed and the paging style has been
96 # Signal emitted when paging is needed and the paging style has been
86 # specified as 'custom'.
97 # specified as 'custom'.
87 custom_page_requested = QtCore.pyqtSignal(object)
98 custom_page_requested = QtCore.Signal(object)
88
99
89 # Signal emitted when the font is changed.
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 #------ Protected class variables ------------------------------------------
103 #------ Protected class variables ------------------------------------------
93
104
94 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
105 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
95 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
106 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
96 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
107 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
97 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
108 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
98 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
109 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
99 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
110 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
100 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
111 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
101
112
102 _shortcuts = set(_ctrl_down_remap.keys() +
113 _shortcuts = set(_ctrl_down_remap.keys() +
103 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
114 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
104 QtCore.Qt.Key_V ])
115 QtCore.Qt.Key_V ])
105
116
106 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
107 # 'QObject' interface
118 # 'QObject' interface
108 #---------------------------------------------------------------------------
119 #---------------------------------------------------------------------------
109
120
110 def __init__(self, parent=None, **kw):
121 def __init__(self, parent=None, **kw):
111 """ Create a ConsoleWidget.
122 """ Create a ConsoleWidget.
112
123
113 Parameters:
124 Parameters:
114 -----------
125 -----------
115 parent : QWidget, optional [default None]
126 parent : QWidget, optional [default None]
116 The parent for this widget.
127 The parent for this widget.
117 """
128 """
118 QtGui.QWidget.__init__(self, parent)
129 QtGui.QWidget.__init__(self, parent)
119 Configurable.__init__(self, **kw)
130 Configurable.__init__(self, **kw)
120
131
121 # Create the layout and underlying text widget.
132 # Create the layout and underlying text widget.
122 layout = QtGui.QStackedLayout(self)
133 layout = QtGui.QStackedLayout(self)
123 layout.setContentsMargins(0, 0, 0, 0)
134 layout.setContentsMargins(0, 0, 0, 0)
124 self._control = self._create_control()
135 self._control = self._create_control()
125 self._page_control = None
136 self._page_control = None
126 self._splitter = None
137 self._splitter = None
127 if self.paging in ('hsplit', 'vsplit'):
138 if self.paging in ('hsplit', 'vsplit'):
128 self._splitter = QtGui.QSplitter()
139 self._splitter = QtGui.QSplitter()
129 if self.paging == 'hsplit':
140 if self.paging == 'hsplit':
130 self._splitter.setOrientation(QtCore.Qt.Horizontal)
141 self._splitter.setOrientation(QtCore.Qt.Horizontal)
131 else:
142 else:
132 self._splitter.setOrientation(QtCore.Qt.Vertical)
143 self._splitter.setOrientation(QtCore.Qt.Vertical)
133 self._splitter.addWidget(self._control)
144 self._splitter.addWidget(self._control)
134 layout.addWidget(self._splitter)
145 layout.addWidget(self._splitter)
135 else:
146 else:
136 layout.addWidget(self._control)
147 layout.addWidget(self._control)
137
148
138 # Create the paging widget, if necessary.
149 # Create the paging widget, if necessary.
139 if self.paging in ('inside', 'hsplit', 'vsplit'):
150 if self.paging in ('inside', 'hsplit', 'vsplit'):
140 self._page_control = self._create_page_control()
151 self._page_control = self._create_page_control()
141 if self._splitter:
152 if self._splitter:
142 self._page_control.hide()
153 self._page_control.hide()
143 self._splitter.addWidget(self._page_control)
154 self._splitter.addWidget(self._page_control)
144 else:
155 else:
145 layout.addWidget(self._page_control)
156 layout.addWidget(self._page_control)
146
157
147 # Initialize protected variables. Some variables contain useful state
158 # Initialize protected variables. Some variables contain useful state
148 # information for subclasses; they should be considered read-only.
159 # information for subclasses; they should be considered read-only.
149 self._ansi_processor = QtAnsiCodeProcessor()
160 self._ansi_processor = QtAnsiCodeProcessor()
150 self._completion_widget = CompletionWidget(self._control)
161 self._completion_widget = CompletionWidget(self._control)
151 self._continuation_prompt = '> '
162 self._continuation_prompt = '> '
152 self._continuation_prompt_html = None
163 self._continuation_prompt_html = None
153 self._executing = False
164 self._executing = False
154 self._filter_drag = False
165 self._filter_drag = False
155 self._filter_resize = False
166 self._filter_resize = False
156 self._prompt = ''
167 self._prompt = ''
157 self._prompt_html = None
168 self._prompt_html = None
158 self._prompt_pos = 0
169 self._prompt_pos = 0
159 self._prompt_sep = ''
170 self._prompt_sep = ''
160 self._reading = False
171 self._reading = False
161 self._reading_callback = None
172 self._reading_callback = None
162 self._tab_width = 8
173 self._tab_width = 8
163 self._text_completing_pos = 0
174 self._text_completing_pos = 0
164 self._filename = 'ipython.html'
175 self._filename = 'ipython.html'
165 self._png_mode=None
176 self._png_mode=None
166
177
167 # Set a monospaced font.
178 # Set a monospaced font.
168 self.reset_font()
179 self.reset_font()
169
180
170 # Configure actions.
181 # Configure actions.
171 action = QtGui.QAction('Print', None)
182 action = QtGui.QAction('Print', None)
172 action.setEnabled(True)
183 action.setEnabled(True)
173 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
184 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
174 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
185 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
175 # only override if there is a collision
186 # only override if there is a collision
176 # Qt ctrl = cmd on OSX, so the match gets a false positive on darwin
187 # Qt ctrl = cmd on OSX, so the match gets a false positive on darwin
177 printkey = "Ctrl+Shift+P"
188 printkey = "Ctrl+Shift+P"
178 action.setShortcut(printkey)
189 action.setShortcut(printkey)
179 action.triggered.connect(self.print_)
190 action.triggered.connect(self.print_)
180 self.addAction(action)
191 self.addAction(action)
181 self._print_action = action
192 self._print_action = action
182
193
183 action = QtGui.QAction('Save as HTML/XML', None)
194 action = QtGui.QAction('Save as HTML/XML', None)
184 action.setEnabled(self.can_export())
195 action.setEnabled(self.can_export())
185 action.setShortcut(QtGui.QKeySequence.Save)
196 action.setShortcut(QtGui.QKeySequence.Save)
186 action.triggered.connect(self.export)
197 action.triggered.connect(self.export)
187 self.addAction(action)
198 self.addAction(action)
188 self._export_action = action
199 self._export_action = action
189
200
190 action = QtGui.QAction('Select All', None)
201 action = QtGui.QAction('Select All', None)
191 action.setEnabled(True)
202 action.setEnabled(True)
192 action.setShortcut(QtGui.QKeySequence.SelectAll)
203 action.setShortcut(QtGui.QKeySequence.SelectAll)
193 action.triggered.connect(self.select_all)
204 action.triggered.connect(self.select_all)
194 self.addAction(action)
205 self.addAction(action)
195 self._select_all_action = action
206 self._select_all_action = action
196
207
197
208
198 def eventFilter(self, obj, event):
209 def eventFilter(self, obj, event):
199 """ Reimplemented to ensure a console-like behavior in the underlying
210 """ Reimplemented to ensure a console-like behavior in the underlying
200 text widgets.
211 text widgets.
201 """
212 """
202 etype = event.type()
213 etype = event.type()
203 if etype == QtCore.QEvent.KeyPress:
214 if etype == QtCore.QEvent.KeyPress:
204
215
205 # Re-map keys for all filtered widgets.
216 # Re-map keys for all filtered widgets.
206 key = event.key()
217 key = event.key()
207 if self._control_key_down(event.modifiers()) and \
218 if self._control_key_down(event.modifiers()) and \
208 key in self._ctrl_down_remap:
219 key in self._ctrl_down_remap:
209 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
220 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
210 self._ctrl_down_remap[key],
221 self._ctrl_down_remap[key],
211 QtCore.Qt.NoModifier)
222 QtCore.Qt.NoModifier)
212 QtGui.qApp.sendEvent(obj, new_event)
223 QtGui.qApp.sendEvent(obj, new_event)
213 return True
224 return True
214
225
215 elif obj == self._control:
226 elif obj == self._control:
216 return self._event_filter_console_keypress(event)
227 return self._event_filter_console_keypress(event)
217
228
218 elif obj == self._page_control:
229 elif obj == self._page_control:
219 return self._event_filter_page_keypress(event)
230 return self._event_filter_page_keypress(event)
220
231
221 # Make middle-click paste safe.
232 # Make middle-click paste safe.
222 elif etype == QtCore.QEvent.MouseButtonRelease and \
233 elif etype == QtCore.QEvent.MouseButtonRelease and \
223 event.button() == QtCore.Qt.MidButton and \
234 event.button() == QtCore.Qt.MidButton and \
224 obj == self._control.viewport():
235 obj == self._control.viewport():
225 cursor = self._control.cursorForPosition(event.pos())
236 cursor = self._control.cursorForPosition(event.pos())
226 self._control.setTextCursor(cursor)
237 self._control.setTextCursor(cursor)
227 self.paste(QtGui.QClipboard.Selection)
238 self.paste(QtGui.QClipboard.Selection)
228 return True
239 return True
229
240
230 # Manually adjust the scrollbars *after* a resize event is dispatched.
241 # Manually adjust the scrollbars *after* a resize event is dispatched.
231 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
242 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
232 self._filter_resize = True
243 self._filter_resize = True
233 QtGui.qApp.sendEvent(obj, event)
244 QtGui.qApp.sendEvent(obj, event)
234 self._adjust_scrollbars()
245 self._adjust_scrollbars()
235 self._filter_resize = False
246 self._filter_resize = False
236 return True
247 return True
237
248
238 # Override shortcuts for all filtered widgets.
249 # Override shortcuts for all filtered widgets.
239 elif etype == QtCore.QEvent.ShortcutOverride and \
250 elif etype == QtCore.QEvent.ShortcutOverride and \
240 self.override_shortcuts and \
251 self.override_shortcuts and \
241 self._control_key_down(event.modifiers()) and \
252 self._control_key_down(event.modifiers()) and \
242 event.key() in self._shortcuts:
253 event.key() in self._shortcuts:
243 event.accept()
254 event.accept()
244
255
245 # Ensure that drags are safe. The problem is that the drag starting
256 # Ensure that drags are safe. The problem is that the drag starting
246 # logic, which determines whether the drag is a Copy or Move, is locked
257 # logic, which determines whether the drag is a Copy or Move, is locked
247 # down in QTextControl. If the widget is editable, which it must be if
258 # down in QTextControl. If the widget is editable, which it must be if
248 # we're not executing, the drag will be a Move. The following hack
259 # we're not executing, the drag will be a Move. The following hack
249 # prevents QTextControl from deleting the text by clearing the selection
260 # prevents QTextControl from deleting the text by clearing the selection
250 # when a drag leave event originating from this widget is dispatched.
261 # when a drag leave event originating from this widget is dispatched.
251 # The fact that we have to clear the user's selection is unfortunate,
262 # The fact that we have to clear the user's selection is unfortunate,
252 # but the alternative--trying to prevent Qt from using its hardwired
263 # but the alternative--trying to prevent Qt from using its hardwired
253 # drag logic and writing our own--is worse.
264 # drag logic and writing our own--is worse.
254 elif etype == QtCore.QEvent.DragEnter and \
265 elif etype == QtCore.QEvent.DragEnter and \
255 obj == self._control.viewport() and \
266 obj == self._control.viewport() and \
256 event.source() == self._control.viewport():
267 event.source() == self._control.viewport():
257 self._filter_drag = True
268 self._filter_drag = True
258 elif etype == QtCore.QEvent.DragLeave and \
269 elif etype == QtCore.QEvent.DragLeave and \
259 obj == self._control.viewport() and \
270 obj == self._control.viewport() and \
260 self._filter_drag:
271 self._filter_drag:
261 cursor = self._control.textCursor()
272 cursor = self._control.textCursor()
262 cursor.clearSelection()
273 cursor.clearSelection()
263 self._control.setTextCursor(cursor)
274 self._control.setTextCursor(cursor)
264 self._filter_drag = False
275 self._filter_drag = False
265
276
266 # Ensure that drops are safe.
277 # Ensure that drops are safe.
267 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
278 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
268 cursor = self._control.cursorForPosition(event.pos())
279 cursor = self._control.cursorForPosition(event.pos())
269 if self._in_buffer(cursor.position()):
280 if self._in_buffer(cursor.position()):
270 text = unicode(event.mimeData().text())
281 text = event.mimeData().text()
271 self._insert_plain_text_into_buffer(cursor, text)
282 self._insert_plain_text_into_buffer(cursor, text)
272
283
273 # Qt is expecting to get something here--drag and drop occurs in its
284 # Qt is expecting to get something here--drag and drop occurs in its
274 # own event loop. Send a DragLeave event to end it.
285 # own event loop. Send a DragLeave event to end it.
275 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
286 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
276 return True
287 return True
277
288
278 return super(ConsoleWidget, self).eventFilter(obj, event)
289 return super(ConsoleWidget, self).eventFilter(obj, event)
279
290
280 #---------------------------------------------------------------------------
291 #---------------------------------------------------------------------------
281 # 'QWidget' interface
292 # 'QWidget' interface
282 #---------------------------------------------------------------------------
293 #---------------------------------------------------------------------------
283
294
284 def sizeHint(self):
295 def sizeHint(self):
285 """ Reimplemented to suggest a size that is 80 characters wide and
296 """ Reimplemented to suggest a size that is 80 characters wide and
286 25 lines high.
297 25 lines high.
287 """
298 """
288 font_metrics = QtGui.QFontMetrics(self.font)
299 font_metrics = QtGui.QFontMetrics(self.font)
289 margin = (self._control.frameWidth() +
300 margin = (self._control.frameWidth() +
290 self._control.document().documentMargin()) * 2
301 self._control.document().documentMargin()) * 2
291 style = self.style()
302 style = self.style()
292 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
303 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
293
304
294 # Note 1: Despite my best efforts to take the various margins into
305 # Note 1: Despite my best efforts to take the various margins into
295 # account, the width is still coming out a bit too small, so we include
306 # account, the width is still coming out a bit too small, so we include
296 # a fudge factor of one character here.
307 # a fudge factor of one character here.
297 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
308 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
298 # to a Qt bug on certain Mac OS systems where it returns 0.
309 # to a Qt bug on certain Mac OS systems where it returns 0.
299 width = font_metrics.width(' ') * 81 + margin
310 width = font_metrics.width(' ') * 81 + margin
300 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
311 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
301 if self.paging == 'hsplit':
312 if self.paging == 'hsplit':
302 width = width * 2 + splitwidth
313 width = width * 2 + splitwidth
303
314
304 height = font_metrics.height() * 25 + margin
315 height = font_metrics.height() * 25 + margin
305 if self.paging == 'vsplit':
316 if self.paging == 'vsplit':
306 height = height * 2 + splitwidth
317 height = height * 2 + splitwidth
307
318
308 return QtCore.QSize(width, height)
319 return QtCore.QSize(width, height)
309
320
310 #---------------------------------------------------------------------------
321 #---------------------------------------------------------------------------
311 # 'ConsoleWidget' public interface
322 # 'ConsoleWidget' public interface
312 #---------------------------------------------------------------------------
323 #---------------------------------------------------------------------------
313
324
314 def can_copy(self):
325 def can_copy(self):
315 """ Returns whether text can be copied to the clipboard.
326 """ Returns whether text can be copied to the clipboard.
316 """
327 """
317 return self._control.textCursor().hasSelection()
328 return self._control.textCursor().hasSelection()
318
329
319 def can_cut(self):
330 def can_cut(self):
320 """ Returns whether text can be cut to the clipboard.
331 """ Returns whether text can be cut to the clipboard.
321 """
332 """
322 cursor = self._control.textCursor()
333 cursor = self._control.textCursor()
323 return (cursor.hasSelection() and
334 return (cursor.hasSelection() and
324 self._in_buffer(cursor.anchor()) and
335 self._in_buffer(cursor.anchor()) and
325 self._in_buffer(cursor.position()))
336 self._in_buffer(cursor.position()))
326
337
327 def can_paste(self):
338 def can_paste(self):
328 """ Returns whether text can be pasted from the clipboard.
339 """ Returns whether text can be pasted from the clipboard.
329 """
340 """
330 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
341 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
331 return not QtGui.QApplication.clipboard().text().isEmpty()
342 return bool(QtGui.QApplication.clipboard().text())
332 return False
343 return False
333
344
334 def can_export(self):
345 def can_export(self):
335 """Returns whether we can export. Currently only rich widgets
346 """Returns whether we can export. Currently only rich widgets
336 can export html.
347 can export html.
337 """
348 """
338 return self.kind == "rich"
349 return self.kind == "rich"
339
350
340 def clear(self, keep_input=True):
351 def clear(self, keep_input=True):
341 """ Clear the console.
352 """ Clear the console.
342
353
343 Parameters:
354 Parameters:
344 -----------
355 -----------
345 keep_input : bool, optional (default True)
356 keep_input : bool, optional (default True)
346 If set, restores the old input buffer if a new prompt is written.
357 If set, restores the old input buffer if a new prompt is written.
347 """
358 """
348 if self._executing:
359 if self._executing:
349 self._control.clear()
360 self._control.clear()
350 else:
361 else:
351 if keep_input:
362 if keep_input:
352 input_buffer = self.input_buffer
363 input_buffer = self.input_buffer
353 self._control.clear()
364 self._control.clear()
354 self._show_prompt()
365 self._show_prompt()
355 if keep_input:
366 if keep_input:
356 self.input_buffer = input_buffer
367 self.input_buffer = input_buffer
357
368
358 def copy(self):
369 def copy(self):
359 """ Copy the currently selected text to the clipboard.
370 """ Copy the currently selected text to the clipboard.
360 """
371 """
361 self._control.copy()
372 self._control.copy()
362
373
363 def cut(self):
374 def cut(self):
364 """ Copy the currently selected text to the clipboard and delete it
375 """ Copy the currently selected text to the clipboard and delete it
365 if it's inside the input buffer.
376 if it's inside the input buffer.
366 """
377 """
367 self.copy()
378 self.copy()
368 if self.can_cut():
379 if self.can_cut():
369 self._control.textCursor().removeSelectedText()
380 self._control.textCursor().removeSelectedText()
370
381
371 def execute(self, source=None, hidden=False, interactive=False):
382 def execute(self, source=None, hidden=False, interactive=False):
372 """ Executes source or the input buffer, possibly prompting for more
383 """ Executes source or the input buffer, possibly prompting for more
373 input.
384 input.
374
385
375 Parameters:
386 Parameters:
376 -----------
387 -----------
377 source : str, optional
388 source : str, optional
378
389
379 The source to execute. If not specified, the input buffer will be
390 The source to execute. If not specified, the input buffer will be
380 used. If specified and 'hidden' is False, the input buffer will be
391 used. If specified and 'hidden' is False, the input buffer will be
381 replaced with the source before execution.
392 replaced with the source before execution.
382
393
383 hidden : bool, optional (default False)
394 hidden : bool, optional (default False)
384
395
385 If set, no output will be shown and the prompt will not be modified.
396 If set, no output will be shown and the prompt will not be modified.
386 In other words, it will be completely invisible to the user that
397 In other words, it will be completely invisible to the user that
387 an execution has occurred.
398 an execution has occurred.
388
399
389 interactive : bool, optional (default False)
400 interactive : bool, optional (default False)
390
401
391 Whether the console is to treat the source as having been manually
402 Whether the console is to treat the source as having been manually
392 entered by the user. The effect of this parameter depends on the
403 entered by the user. The effect of this parameter depends on the
393 subclass implementation.
404 subclass implementation.
394
405
395 Raises:
406 Raises:
396 -------
407 -------
397 RuntimeError
408 RuntimeError
398 If incomplete input is given and 'hidden' is True. In this case,
409 If incomplete input is given and 'hidden' is True. In this case,
399 it is not possible to prompt for more input.
410 it is not possible to prompt for more input.
400
411
401 Returns:
412 Returns:
402 --------
413 --------
403 A boolean indicating whether the source was executed.
414 A boolean indicating whether the source was executed.
404 """
415 """
405 # WARNING: The order in which things happen here is very particular, in
416 # WARNING: The order in which things happen here is very particular, in
406 # large part because our syntax highlighting is fragile. If you change
417 # large part because our syntax highlighting is fragile. If you change
407 # something, test carefully!
418 # something, test carefully!
408
419
409 # Decide what to execute.
420 # Decide what to execute.
410 if source is None:
421 if source is None:
411 source = self.input_buffer
422 source = self.input_buffer
412 if not hidden:
423 if not hidden:
413 # A newline is appended later, but it should be considered part
424 # A newline is appended later, but it should be considered part
414 # of the input buffer.
425 # of the input buffer.
415 source += '\n'
426 source += '\n'
416 elif not hidden:
427 elif not hidden:
417 self.input_buffer = source
428 self.input_buffer = source
418
429
419 # Execute the source or show a continuation prompt if it is incomplete.
430 # Execute the source or show a continuation prompt if it is incomplete.
420 complete = self._is_complete(source, interactive)
431 complete = self._is_complete(source, interactive)
421 if hidden:
432 if hidden:
422 if complete:
433 if complete:
423 self._execute(source, hidden)
434 self._execute(source, hidden)
424 else:
435 else:
425 error = 'Incomplete noninteractive input: "%s"'
436 error = 'Incomplete noninteractive input: "%s"'
426 raise RuntimeError(error % source)
437 raise RuntimeError(error % source)
427 else:
438 else:
428 if complete:
439 if complete:
429 self._append_plain_text('\n')
440 self._append_plain_text('\n')
430 self._executing_input_buffer = self.input_buffer
441 self._executing_input_buffer = self.input_buffer
431 self._executing = True
442 self._executing = True
432 self._prompt_finished()
443 self._prompt_finished()
433
444
434 # The maximum block count is only in effect during execution.
445 # The maximum block count is only in effect during execution.
435 # This ensures that _prompt_pos does not become invalid due to
446 # This ensures that _prompt_pos does not become invalid due to
436 # text truncation.
447 # text truncation.
437 self._control.document().setMaximumBlockCount(self.buffer_size)
448 self._control.document().setMaximumBlockCount(self.buffer_size)
438
449
439 # Setting a positive maximum block count will automatically
450 # Setting a positive maximum block count will automatically
440 # disable the undo/redo history, but just to be safe:
451 # disable the undo/redo history, but just to be safe:
441 self._control.setUndoRedoEnabled(False)
452 self._control.setUndoRedoEnabled(False)
442
453
443 # Perform actual execution.
454 # Perform actual execution.
444 self._execute(source, hidden)
455 self._execute(source, hidden)
445
456
446 else:
457 else:
447 # Do this inside an edit block so continuation prompts are
458 # Do this inside an edit block so continuation prompts are
448 # removed seamlessly via undo/redo.
459 # removed seamlessly via undo/redo.
449 cursor = self._get_end_cursor()
460 cursor = self._get_end_cursor()
450 cursor.beginEditBlock()
461 cursor.beginEditBlock()
451 cursor.insertText('\n')
462 cursor.insertText('\n')
452 self._insert_continuation_prompt(cursor)
463 self._insert_continuation_prompt(cursor)
453 cursor.endEditBlock()
464 cursor.endEditBlock()
454
465
455 # Do not do this inside the edit block. It works as expected
466 # Do not do this inside the edit block. It works as expected
456 # when using a QPlainTextEdit control, but does not have an
467 # when using a QPlainTextEdit control, but does not have an
457 # effect when using a QTextEdit. I believe this is a Qt bug.
468 # effect when using a QTextEdit. I believe this is a Qt bug.
458 self._control.moveCursor(QtGui.QTextCursor.End)
469 self._control.moveCursor(QtGui.QTextCursor.End)
459
470
460 return complete
471 return complete
461
472
462 def _get_input_buffer(self):
473 def _get_input_buffer(self):
463 """ The text that the user has entered entered at the current prompt.
474 """ The text that the user has entered entered at the current prompt.
464 """
475 """
465 # If we're executing, the input buffer may not even exist anymore due to
476 # If we're executing, the input buffer may not even exist anymore due to
466 # the limit imposed by 'buffer_size'. Therefore, we store it.
477 # the limit imposed by 'buffer_size'. Therefore, we store it.
467 if self._executing:
478 if self._executing:
468 return self._executing_input_buffer
479 return self._executing_input_buffer
469
480
470 cursor = self._get_end_cursor()
481 cursor = self._get_end_cursor()
471 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
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 # Strip out continuation prompts.
485 # Strip out continuation prompts.
475 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
486 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
476
487
477 def _set_input_buffer(self, string):
488 def _set_input_buffer(self, string):
478 """ Replaces the text in the input buffer with 'string'.
489 """ Replaces the text in the input buffer with 'string'.
479 """
490 """
480 # For now, it is an error to modify the input buffer during execution.
491 # For now, it is an error to modify the input buffer during execution.
481 if self._executing:
492 if self._executing:
482 raise RuntimeError("Cannot change input buffer during execution.")
493 raise RuntimeError("Cannot change input buffer during execution.")
483
494
484 # Remove old text.
495 # Remove old text.
485 cursor = self._get_end_cursor()
496 cursor = self._get_end_cursor()
486 cursor.beginEditBlock()
497 cursor.beginEditBlock()
487 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
498 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
488 cursor.removeSelectedText()
499 cursor.removeSelectedText()
489
500
490 # Insert new text with continuation prompts.
501 # Insert new text with continuation prompts.
491 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
502 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
492 cursor.endEditBlock()
503 cursor.endEditBlock()
493 self._control.moveCursor(QtGui.QTextCursor.End)
504 self._control.moveCursor(QtGui.QTextCursor.End)
494
505
495 input_buffer = property(_get_input_buffer, _set_input_buffer)
506 input_buffer = property(_get_input_buffer, _set_input_buffer)
496
507
497 def _get_font(self):
508 def _get_font(self):
498 """ The base font being used by the ConsoleWidget.
509 """ The base font being used by the ConsoleWidget.
499 """
510 """
500 return self._control.document().defaultFont()
511 return self._control.document().defaultFont()
501
512
502 def _set_font(self, font):
513 def _set_font(self, font):
503 """ Sets the base font for the ConsoleWidget to the specified QFont.
514 """ Sets the base font for the ConsoleWidget to the specified QFont.
504 """
515 """
505 font_metrics = QtGui.QFontMetrics(font)
516 font_metrics = QtGui.QFontMetrics(font)
506 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
517 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
507
518
508 self._completion_widget.setFont(font)
519 self._completion_widget.setFont(font)
509 self._control.document().setDefaultFont(font)
520 self._control.document().setDefaultFont(font)
510 if self._page_control:
521 if self._page_control:
511 self._page_control.document().setDefaultFont(font)
522 self._page_control.document().setDefaultFont(font)
512
523
513 self.font_changed.emit(font)
524 self.font_changed.emit(font)
514
525
515 font = property(_get_font, _set_font)
526 font = property(_get_font, _set_font)
516
527
517 def paste(self, mode=QtGui.QClipboard.Clipboard):
528 def paste(self, mode=QtGui.QClipboard.Clipboard):
518 """ Paste the contents of the clipboard into the input region.
529 """ Paste the contents of the clipboard into the input region.
519
530
520 Parameters:
531 Parameters:
521 -----------
532 -----------
522 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
533 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
523
534
524 Controls which part of the system clipboard is used. This can be
535 Controls which part of the system clipboard is used. This can be
525 used to access the selection clipboard in X11 and the Find buffer
536 used to access the selection clipboard in X11 and the Find buffer
526 in Mac OS. By default, the regular clipboard is used.
537 in Mac OS. By default, the regular clipboard is used.
527 """
538 """
528 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
539 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
529 # Make sure the paste is safe.
540 # Make sure the paste is safe.
530 self._keep_cursor_in_buffer()
541 self._keep_cursor_in_buffer()
531 cursor = self._control.textCursor()
542 cursor = self._control.textCursor()
532
543
533 # Remove any trailing newline, which confuses the GUI and forces the
544 # Remove any trailing newline, which confuses the GUI and forces the
534 # user to backspace.
545 # user to backspace.
535 text = unicode(QtGui.QApplication.clipboard().text(mode)).rstrip()
546 text = QtGui.QApplication.clipboard().text(mode).rstrip()
536 self._insert_plain_text_into_buffer(cursor, dedent(text))
547 self._insert_plain_text_into_buffer(cursor, dedent(text))
537
548
538 def print_(self, printer = None):
549 def print_(self, printer = None):
539 """ Print the contents of the ConsoleWidget to the specified QPrinter.
550 """ Print the contents of the ConsoleWidget to the specified QPrinter.
540 """
551 """
541 if (not printer):
552 if (not printer):
542 printer = QtGui.QPrinter()
553 printer = QtGui.QPrinter()
543 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
554 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
544 return
555 return
545 self._control.print_(printer)
556 self._control.print_(printer)
546
557
547 def export(self, parent = None):
558 def export(self, parent = None):
548 """Export HTML/XML in various modes from one Dialog."""
559 """Export HTML/XML in various modes from one Dialog."""
549 parent = parent or None # sometimes parent is False
560 parent = parent or None # sometimes parent is False
550 dialog = QtGui.QFileDialog(parent, 'Save Console as...')
561 dialog = QtGui.QFileDialog(parent, 'Save Console as...')
551 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
562 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
552 filters = [
563 filters = [
553 'HTML with PNG figures (*.html *.htm)',
564 'HTML with PNG figures (*.html *.htm)',
554 'XHTML with inline SVG figures (*.xhtml *.xml)'
565 'XHTML with inline SVG figures (*.xhtml *.xml)'
555 ]
566 ]
556 dialog.setNameFilters(filters)
567 dialog.setNameFilters(filters)
557 if self._filename:
568 if self._filename:
558 dialog.selectFile(self._filename)
569 dialog.selectFile(self._filename)
559 root,ext = os.path.splitext(self._filename)
570 root,ext = os.path.splitext(self._filename)
560 if ext.lower() in ('.xml', '.xhtml'):
571 if ext.lower() in ('.xml', '.xhtml'):
561 dialog.selectNameFilter(filters[-1])
572 dialog.selectNameFilter(filters[-1])
562 if dialog.exec_():
573 if dialog.exec_():
563 filename = str(dialog.selectedFiles()[0])
574 filename = str(dialog.selectedFiles()[0])
564 self._filename = filename
575 self._filename = filename
565 choice = str(dialog.selectedNameFilter())
576 choice = str(dialog.selectedNameFilter())
566
577
567 if choice.startswith('XHTML'):
578 if choice.startswith('XHTML'):
568 exporter = self.export_xhtml
579 exporter = self.export_xhtml
569 else:
580 else:
570 exporter = self.export_html
581 exporter = self.export_html
571
582
572 try:
583 try:
573 return exporter(filename)
584 return exporter(filename)
574 except Exception, e:
585 except Exception, e:
575 title = self.window().windowTitle()
586 title = self.window().windowTitle()
576 msg = "Error while saving to: %s\n"%filename+str(e)
587 msg = "Error while saving to: %s\n"%filename+str(e)
577 reply = QtGui.QMessageBox.warning(self, title, msg,
588 reply = QtGui.QMessageBox.warning(self, title, msg,
578 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
589 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
579 return None
590 return None
580
591
581 def export_html(self, filename):
592 def export_html(self, filename):
582 """ Export the contents of the ConsoleWidget as HTML.
593 """ Export the contents of the ConsoleWidget as HTML.
583
594
584 Parameters:
595 Parameters:
585 -----------
596 -----------
586 filename : str
597 filename : str
587 The file to be saved.
598 The file to be saved.
588 inline : bool, optional [default True]
599 inline : bool, optional [default True]
589 If True, include images as inline PNGs. Otherwise,
600 If True, include images as inline PNGs. Otherwise,
590 include them as links to external PNG files, mimicking
601 include them as links to external PNG files, mimicking
591 web browsers' "Web Page, Complete" behavior.
602 web browsers' "Web Page, Complete" behavior.
592 """
603 """
593 # N.B. this is overly restrictive, but Qt's output is
604 # N.B. this is overly restrictive, but Qt's output is
594 # predictable...
605 # predictable...
595 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
606 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
596 html = self.fix_html_encoding(
607 html = self.fix_html_encoding(
597 str(self._control.toHtml().toUtf8()))
608 str(self._control.toHtml().toUtf8()))
598 if self._png_mode:
609 if self._png_mode:
599 # preference saved, don't ask again
610 # preference saved, don't ask again
600 if img_re.search(html):
611 if img_re.search(html):
601 inline = (self._png_mode == 'inline')
612 inline = (self._png_mode == 'inline')
602 else:
613 else:
603 inline = True
614 inline = True
604 elif img_re.search(html):
615 elif img_re.search(html):
605 # there are images
616 # there are images
606 widget = QtGui.QWidget()
617 widget = QtGui.QWidget()
607 layout = QtGui.QVBoxLayout(widget)
618 layout = QtGui.QVBoxLayout(widget)
608 title = self.window().windowTitle()
619 title = self.window().windowTitle()
609 msg = "Exporting HTML with PNGs"
620 msg = "Exporting HTML with PNGs"
610 info = "Would you like inline PNGs (single large html file) or "+\
621 info = "Would you like inline PNGs (single large html file) or "+\
611 "external image files?"
622 "external image files?"
612 checkbox = QtGui.QCheckBox("&Don't ask again")
623 checkbox = QtGui.QCheckBox("&Don't ask again")
613 checkbox.setShortcut('D')
624 checkbox.setShortcut('D')
614 ib = QtGui.QPushButton("&Inline", self)
625 ib = QtGui.QPushButton("&Inline", self)
615 ib.setShortcut('I')
626 ib.setShortcut('I')
616 eb = QtGui.QPushButton("&External", self)
627 eb = QtGui.QPushButton("&External", self)
617 eb.setShortcut('E')
628 eb.setShortcut('E')
618 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
629 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
619 box.setInformativeText(info)
630 box.setInformativeText(info)
620 box.addButton(ib,QtGui.QMessageBox.NoRole)
631 box.addButton(ib,QtGui.QMessageBox.NoRole)
621 box.addButton(eb,QtGui.QMessageBox.YesRole)
632 box.addButton(eb,QtGui.QMessageBox.YesRole)
622 box.setDefaultButton(ib)
633 box.setDefaultButton(ib)
623 layout.setSpacing(0)
634 layout.setSpacing(0)
624 layout.addWidget(box)
635 layout.addWidget(box)
625 layout.addWidget(checkbox)
636 layout.addWidget(checkbox)
626 widget.setLayout(layout)
637 widget.setLayout(layout)
627 widget.show()
638 widget.show()
628 reply = box.exec_()
639 reply = box.exec_()
629 inline = (reply == 0)
640 inline = (reply == 0)
630 if checkbox.checkState():
641 if checkbox.checkState():
631 # don't ask anymore, always use this choice
642 # don't ask anymore, always use this choice
632 if inline:
643 if inline:
633 self._png_mode='inline'
644 self._png_mode='inline'
634 else:
645 else:
635 self._png_mode='external'
646 self._png_mode='external'
636 else:
647 else:
637 # no images
648 # no images
638 inline = True
649 inline = True
639
650
640 if inline:
651 if inline:
641 path = None
652 path = None
642 else:
653 else:
643 root,ext = os.path.splitext(filename)
654 root,ext = os.path.splitext(filename)
644 path = root+"_files"
655 path = root+"_files"
645 if os.path.isfile(path):
656 if os.path.isfile(path):
646 raise OSError("%s exists, but is not a directory."%path)
657 raise OSError("%s exists, but is not a directory."%path)
647
658
648 f = open(filename, 'w')
659 f = open(filename, 'w')
649 try:
660 try:
650 f.write(img_re.sub(
661 f.write(img_re.sub(
651 lambda x: self.image_tag(x, path = path, format = "png"),
662 lambda x: self.image_tag(x, path = path, format = "png"),
652 html))
663 html))
653 except Exception, e:
664 except Exception, e:
654 f.close()
665 f.close()
655 raise e
666 raise e
656 else:
667 else:
657 f.close()
668 f.close()
658 return filename
669 return filename
659
670
660
671
661 def export_xhtml(self, filename):
672 def export_xhtml(self, filename):
662 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
673 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
663 """
674 """
664 f = open(filename, 'w')
675 f = open(filename, 'w')
665 try:
676 try:
666 # N.B. this is overly restrictive, but Qt's output is
677 # N.B. this is overly restrictive, but Qt's output is
667 # predictable...
678 # predictable...
668 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
679 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
669 html = str(self._control.toHtml().toUtf8())
680 html = str(self._control.toHtml().toUtf8())
670 # Hack to make xhtml header -- note that we are not doing
681 # Hack to make xhtml header -- note that we are not doing
671 # any check for valid xml
682 # any check for valid xml
672 offset = html.find("<html>")
683 offset = html.find("<html>")
673 assert(offset > -1)
684 assert(offset > -1)
674 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
685 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
675 html[offset+6:])
686 html[offset+6:])
676 # And now declare UTF-8 encoding
687 # And now declare UTF-8 encoding
677 html = self.fix_html_encoding(html)
688 html = self.fix_html_encoding(html)
678 f.write(img_re.sub(
689 f.write(img_re.sub(
679 lambda x: self.image_tag(x, path = None, format = "svg"),
690 lambda x: self.image_tag(x, path = None, format = "svg"),
680 html))
691 html))
681 except Exception, e:
692 except Exception, e:
682 f.close()
693 f.close()
683 raise e
694 raise e
684 else:
695 else:
685 f.close()
696 f.close()
686 return filename
697 return filename
687
698
688 def fix_html_encoding(self, html):
699 def fix_html_encoding(self, html):
689 """ Return html string, with a UTF-8 declaration added to <HEAD>.
700 """ Return html string, with a UTF-8 declaration added to <HEAD>.
690
701
691 Assumes that html is Qt generated and has already been UTF-8 encoded
702 Assumes that html is Qt generated and has already been UTF-8 encoded
692 and coerced to a python string. If the expected head element is
703 and coerced to a python string. If the expected head element is
693 not found, the given object is returned unmodified.
704 not found, the given object is returned unmodified.
694
705
695 This patching is needed for proper rendering of some characters
706 This patching is needed for proper rendering of some characters
696 (e.g., indented commands) when viewing exported HTML on a local
707 (e.g., indented commands) when viewing exported HTML on a local
697 system (i.e., without seeing an encoding declaration in an HTTP
708 system (i.e., without seeing an encoding declaration in an HTTP
698 header).
709 header).
699
710
700 C.f. http://www.w3.org/International/O-charset for details.
711 C.f. http://www.w3.org/International/O-charset for details.
701 """
712 """
702 offset = html.find("<head>")
713 offset = html.find("<head>")
703 if(offset > -1):
714 if(offset > -1):
704 html = (html[:offset+6]+
715 html = (html[:offset+6]+
705 '\n<meta http-equiv="Content-Type" '+
716 '\n<meta http-equiv="Content-Type" '+
706 'content="text/html; charset=utf-8" />\n'+
717 'content="text/html; charset=utf-8" />\n'+
707 html[offset+6:])
718 html[offset+6:])
708
719
709 return html
720 return html
710
721
711 def image_tag(self, match, path = None, format = "png"):
722 def image_tag(self, match, path = None, format = "png"):
712 """ Return (X)HTML mark-up for the image-tag given by match.
723 """ Return (X)HTML mark-up for the image-tag given by match.
713
724
714 Parameters
725 Parameters
715 ----------
726 ----------
716 match : re.SRE_Match
727 match : re.SRE_Match
717 A match to an HTML image tag as exported by Qt, with
728 A match to an HTML image tag as exported by Qt, with
718 match.group("Name") containing the matched image ID.
729 match.group("Name") containing the matched image ID.
719
730
720 path : string|None, optional [default None]
731 path : string|None, optional [default None]
721 If not None, specifies a path to which supporting files
732 If not None, specifies a path to which supporting files
722 may be written (e.g., for linked images).
733 may be written (e.g., for linked images).
723 If None, all images are to be included inline.
734 If None, all images are to be included inline.
724
735
725 format : "png"|"svg", optional [default "png"]
736 format : "png"|"svg", optional [default "png"]
726 Format for returned or referenced images.
737 Format for returned or referenced images.
727
738
728 Subclasses supporting image display should override this
739 Subclasses supporting image display should override this
729 method.
740 method.
730 """
741 """
731
742
732 # Default case -- not enough information to generate tag
743 # Default case -- not enough information to generate tag
733 return ""
744 return ""
734
745
735 def prompt_to_top(self):
746 def prompt_to_top(self):
736 """ Moves the prompt to the top of the viewport.
747 """ Moves the prompt to the top of the viewport.
737 """
748 """
738 if not self._executing:
749 if not self._executing:
739 prompt_cursor = self._get_prompt_cursor()
750 prompt_cursor = self._get_prompt_cursor()
740 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
751 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
741 self._set_cursor(prompt_cursor)
752 self._set_cursor(prompt_cursor)
742 self._set_top_cursor(prompt_cursor)
753 self._set_top_cursor(prompt_cursor)
743
754
744 def redo(self):
755 def redo(self):
745 """ Redo the last operation. If there is no operation to redo, nothing
756 """ Redo the last operation. If there is no operation to redo, nothing
746 happens.
757 happens.
747 """
758 """
748 self._control.redo()
759 self._control.redo()
749
760
750 def reset_font(self):
761 def reset_font(self):
751 """ Sets the font to the default fixed-width font for this platform.
762 """ Sets the font to the default fixed-width font for this platform.
752 """
763 """
753 if sys.platform == 'win32':
764 if sys.platform == 'win32':
754 # Consolas ships with Vista/Win7, fallback to Courier if needed
765 # Consolas ships with Vista/Win7, fallback to Courier if needed
755 family, fallback = 'Consolas', 'Courier'
766 family, fallback = 'Consolas', 'Courier'
756 elif sys.platform == 'darwin':
767 elif sys.platform == 'darwin':
757 # OSX always has Monaco, no need for a fallback
768 # OSX always has Monaco, no need for a fallback
758 family, fallback = 'Monaco', None
769 family, fallback = 'Monaco', None
759 else:
770 else:
760 # FIXME: remove Consolas as a default on Linux once our font
771 # FIXME: remove Consolas as a default on Linux once our font
761 # selections are configurable by the user.
772 # selections are configurable by the user.
762 family, fallback = 'Consolas', 'Monospace'
773 family, fallback = 'Consolas', 'Monospace'
763 font = get_font(family, fallback)
774 font = get_font(family, fallback)
764 font.setPointSize(QtGui.qApp.font().pointSize())
775 font.setPointSize(QtGui.qApp.font().pointSize())
765 font.setStyleHint(QtGui.QFont.TypeWriter)
776 font.setStyleHint(QtGui.QFont.TypeWriter)
766 self._set_font(font)
777 self._set_font(font)
767
778
768 def change_font_size(self, delta):
779 def change_font_size(self, delta):
769 """Change the font size by the specified amount (in points).
780 """Change the font size by the specified amount (in points).
770 """
781 """
771 font = self.font
782 font = self.font
772 font.setPointSize(font.pointSize() + delta)
783 font.setPointSize(font.pointSize() + delta)
773 self._set_font(font)
784 self._set_font(font)
774
785
775 def select_all(self):
786 def select_all(self):
776 """ Selects all the text in the buffer.
787 """ Selects all the text in the buffer.
777 """
788 """
778 self._control.selectAll()
789 self._control.selectAll()
779
790
780 def _get_tab_width(self):
791 def _get_tab_width(self):
781 """ The width (in terms of space characters) for tab characters.
792 """ The width (in terms of space characters) for tab characters.
782 """
793 """
783 return self._tab_width
794 return self._tab_width
784
795
785 def _set_tab_width(self, tab_width):
796 def _set_tab_width(self, tab_width):
786 """ Sets the width (in terms of space characters) for tab characters.
797 """ Sets the width (in terms of space characters) for tab characters.
787 """
798 """
788 font_metrics = QtGui.QFontMetrics(self.font)
799 font_metrics = QtGui.QFontMetrics(self.font)
789 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
800 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
790
801
791 self._tab_width = tab_width
802 self._tab_width = tab_width
792
803
793 tab_width = property(_get_tab_width, _set_tab_width)
804 tab_width = property(_get_tab_width, _set_tab_width)
794
805
795 def undo(self):
806 def undo(self):
796 """ Undo the last operation. If there is no operation to undo, nothing
807 """ Undo the last operation. If there is no operation to undo, nothing
797 happens.
808 happens.
798 """
809 """
799 self._control.undo()
810 self._control.undo()
800
811
801 #---------------------------------------------------------------------------
812 #---------------------------------------------------------------------------
802 # 'ConsoleWidget' abstract interface
813 # 'ConsoleWidget' abstract interface
803 #---------------------------------------------------------------------------
814 #---------------------------------------------------------------------------
804
815
805 def _is_complete(self, source, interactive):
816 def _is_complete(self, source, interactive):
806 """ Returns whether 'source' can be executed. When triggered by an
817 """ Returns whether 'source' can be executed. When triggered by an
807 Enter/Return key press, 'interactive' is True; otherwise, it is
818 Enter/Return key press, 'interactive' is True; otherwise, it is
808 False.
819 False.
809 """
820 """
810 raise NotImplementedError
821 raise NotImplementedError
811
822
812 def _execute(self, source, hidden):
823 def _execute(self, source, hidden):
813 """ Execute 'source'. If 'hidden', do not show any output.
824 """ Execute 'source'. If 'hidden', do not show any output.
814 """
825 """
815 raise NotImplementedError
826 raise NotImplementedError
816
827
817 def _prompt_started_hook(self):
828 def _prompt_started_hook(self):
818 """ Called immediately after a new prompt is displayed.
829 """ Called immediately after a new prompt is displayed.
819 """
830 """
820 pass
831 pass
821
832
822 def _prompt_finished_hook(self):
833 def _prompt_finished_hook(self):
823 """ Called immediately after a prompt is finished, i.e. when some input
834 """ Called immediately after a prompt is finished, i.e. when some input
824 will be processed and a new prompt displayed.
835 will be processed and a new prompt displayed.
825 """
836 """
826 pass
837 pass
827
838
828 def _up_pressed(self):
839 def _up_pressed(self):
829 """ Called when the up key is pressed. Returns whether to continue
840 """ Called when the up key is pressed. Returns whether to continue
830 processing the event.
841 processing the event.
831 """
842 """
832 return True
843 return True
833
844
834 def _down_pressed(self):
845 def _down_pressed(self):
835 """ Called when the down key is pressed. Returns whether to continue
846 """ Called when the down key is pressed. Returns whether to continue
836 processing the event.
847 processing the event.
837 """
848 """
838 return True
849 return True
839
850
840 def _tab_pressed(self):
851 def _tab_pressed(self):
841 """ Called when the tab key is pressed. Returns whether to continue
852 """ Called when the tab key is pressed. Returns whether to continue
842 processing the event.
853 processing the event.
843 """
854 """
844 return False
855 return False
845
856
846 #--------------------------------------------------------------------------
857 #--------------------------------------------------------------------------
847 # 'ConsoleWidget' protected interface
858 # 'ConsoleWidget' protected interface
848 #--------------------------------------------------------------------------
859 #--------------------------------------------------------------------------
849
860
850 def _append_html(self, html):
861 def _append_html(self, html):
851 """ Appends html at the end of the console buffer.
862 """ Appends html at the end of the console buffer.
852 """
863 """
853 cursor = self._get_end_cursor()
864 cursor = self._get_end_cursor()
854 self._insert_html(cursor, html)
865 self._insert_html(cursor, html)
855
866
856 def _append_html_fetching_plain_text(self, html):
867 def _append_html_fetching_plain_text(self, html):
857 """ Appends 'html', then returns the plain text version of it.
868 """ Appends 'html', then returns the plain text version of it.
858 """
869 """
859 cursor = self._get_end_cursor()
870 cursor = self._get_end_cursor()
860 return self._insert_html_fetching_plain_text(cursor, html)
871 return self._insert_html_fetching_plain_text(cursor, html)
861
872
862 def _append_plain_text(self, text):
873 def _append_plain_text(self, text):
863 """ Appends plain text at the end of the console buffer, processing
874 """ Appends plain text at the end of the console buffer, processing
864 ANSI codes if enabled.
875 ANSI codes if enabled.
865 """
876 """
866 cursor = self._get_end_cursor()
877 cursor = self._get_end_cursor()
867 self._insert_plain_text(cursor, text)
878 self._insert_plain_text(cursor, text)
868
879
869 def _append_plain_text_keeping_prompt(self, text):
880 def _append_plain_text_keeping_prompt(self, text):
870 """ Writes 'text' after the current prompt, then restores the old prompt
881 """ Writes 'text' after the current prompt, then restores the old prompt
871 with its old input buffer.
882 with its old input buffer.
872 """
883 """
873 input_buffer = self.input_buffer
884 input_buffer = self.input_buffer
874 self._append_plain_text('\n')
885 self._append_plain_text('\n')
875 self._prompt_finished()
886 self._prompt_finished()
876
887
877 self._append_plain_text(text)
888 self._append_plain_text(text)
878 self._show_prompt()
889 self._show_prompt()
879 self.input_buffer = input_buffer
890 self.input_buffer = input_buffer
880
891
881 def _cancel_text_completion(self):
892 def _cancel_text_completion(self):
882 """ If text completion is progress, cancel it.
893 """ If text completion is progress, cancel it.
883 """
894 """
884 if self._text_completing_pos:
895 if self._text_completing_pos:
885 self._clear_temporary_buffer()
896 self._clear_temporary_buffer()
886 self._text_completing_pos = 0
897 self._text_completing_pos = 0
887
898
888 def _clear_temporary_buffer(self):
899 def _clear_temporary_buffer(self):
889 """ Clears the "temporary text" buffer, i.e. all the text following
900 """ Clears the "temporary text" buffer, i.e. all the text following
890 the prompt region.
901 the prompt region.
891 """
902 """
892 # Select and remove all text below the input buffer.
903 # Select and remove all text below the input buffer.
893 cursor = self._get_prompt_cursor()
904 cursor = self._get_prompt_cursor()
894 prompt = self._continuation_prompt.lstrip()
905 prompt = self._continuation_prompt.lstrip()
895 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
906 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
896 temp_cursor = QtGui.QTextCursor(cursor)
907 temp_cursor = QtGui.QTextCursor(cursor)
897 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
908 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
898 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
909 text = temp_cursor.selection().toPlainText().lstrip()
899 if not text.startswith(prompt):
910 if not text.startswith(prompt):
900 break
911 break
901 else:
912 else:
902 # We've reached the end of the input buffer and no text follows.
913 # We've reached the end of the input buffer and no text follows.
903 return
914 return
904 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
915 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
905 cursor.movePosition(QtGui.QTextCursor.End,
916 cursor.movePosition(QtGui.QTextCursor.End,
906 QtGui.QTextCursor.KeepAnchor)
917 QtGui.QTextCursor.KeepAnchor)
907 cursor.removeSelectedText()
918 cursor.removeSelectedText()
908
919
909 # After doing this, we have no choice but to clear the undo/redo
920 # After doing this, we have no choice but to clear the undo/redo
910 # history. Otherwise, the text is not "temporary" at all, because it
921 # history. Otherwise, the text is not "temporary" at all, because it
911 # can be recalled with undo/redo. Unfortunately, Qt does not expose
922 # can be recalled with undo/redo. Unfortunately, Qt does not expose
912 # fine-grained control to the undo/redo system.
923 # fine-grained control to the undo/redo system.
913 if self._control.isUndoRedoEnabled():
924 if self._control.isUndoRedoEnabled():
914 self._control.setUndoRedoEnabled(False)
925 self._control.setUndoRedoEnabled(False)
915 self._control.setUndoRedoEnabled(True)
926 self._control.setUndoRedoEnabled(True)
916
927
917 def _complete_with_items(self, cursor, items):
928 def _complete_with_items(self, cursor, items):
918 """ Performs completion with 'items' at the specified cursor location.
929 """ Performs completion with 'items' at the specified cursor location.
919 """
930 """
920 self._cancel_text_completion()
931 self._cancel_text_completion()
921
932
922 if len(items) == 1:
933 if len(items) == 1:
923 cursor.setPosition(self._control.textCursor().position(),
934 cursor.setPosition(self._control.textCursor().position(),
924 QtGui.QTextCursor.KeepAnchor)
935 QtGui.QTextCursor.KeepAnchor)
925 cursor.insertText(items[0])
936 cursor.insertText(items[0])
926
937
927 elif len(items) > 1:
938 elif len(items) > 1:
928 current_pos = self._control.textCursor().position()
939 current_pos = self._control.textCursor().position()
929 prefix = commonprefix(items)
940 prefix = commonprefix(items)
930 if prefix:
941 if prefix:
931 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
942 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
932 cursor.insertText(prefix)
943 cursor.insertText(prefix)
933 current_pos = cursor.position()
944 current_pos = cursor.position()
934
945
935 if self.gui_completion:
946 if self.gui_completion:
936 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
947 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
937 self._completion_widget.show_items(cursor, items)
948 self._completion_widget.show_items(cursor, items)
938 else:
949 else:
939 cursor.beginEditBlock()
950 cursor.beginEditBlock()
940 self._append_plain_text('\n')
951 self._append_plain_text('\n')
941 self._page(self._format_as_columns(items))
952 self._page(self._format_as_columns(items))
942 cursor.endEditBlock()
953 cursor.endEditBlock()
943
954
944 cursor.setPosition(current_pos)
955 cursor.setPosition(current_pos)
945 self._control.moveCursor(QtGui.QTextCursor.End)
956 self._control.moveCursor(QtGui.QTextCursor.End)
946 self._control.setTextCursor(cursor)
957 self._control.setTextCursor(cursor)
947 self._text_completing_pos = current_pos
958 self._text_completing_pos = current_pos
948
959
949 def _context_menu_make(self, pos):
960 def _context_menu_make(self, pos):
950 """ Creates a context menu for the given QPoint (in widget coordinates).
961 """ Creates a context menu for the given QPoint (in widget coordinates).
951 """
962 """
952 menu = QtGui.QMenu(self)
963 menu = QtGui.QMenu(self)
953
964
954 cut_action = menu.addAction('Cut', self.cut)
965 cut_action = menu.addAction('Cut', self.cut)
955 cut_action.setEnabled(self.can_cut())
966 cut_action.setEnabled(self.can_cut())
956 cut_action.setShortcut(QtGui.QKeySequence.Cut)
967 cut_action.setShortcut(QtGui.QKeySequence.Cut)
957
968
958 copy_action = menu.addAction('Copy', self.copy)
969 copy_action = menu.addAction('Copy', self.copy)
959 copy_action.setEnabled(self.can_copy())
970 copy_action.setEnabled(self.can_copy())
960 copy_action.setShortcut(QtGui.QKeySequence.Copy)
971 copy_action.setShortcut(QtGui.QKeySequence.Copy)
961
972
962 paste_action = menu.addAction('Paste', self.paste)
973 paste_action = menu.addAction('Paste', self.paste)
963 paste_action.setEnabled(self.can_paste())
974 paste_action.setEnabled(self.can_paste())
964 paste_action.setShortcut(QtGui.QKeySequence.Paste)
975 paste_action.setShortcut(QtGui.QKeySequence.Paste)
965
976
966 menu.addSeparator()
977 menu.addSeparator()
967 menu.addAction(self._select_all_action)
978 menu.addAction(self._select_all_action)
968
979
969 menu.addSeparator()
980 menu.addSeparator()
970 menu.addAction(self._export_action)
981 menu.addAction(self._export_action)
971 menu.addAction(self._print_action)
982 menu.addAction(self._print_action)
972
983
973 return menu
984 return menu
974
985
975 def _control_key_down(self, modifiers, include_command=False):
986 def _control_key_down(self, modifiers, include_command=False):
976 """ Given a KeyboardModifiers flags object, return whether the Control
987 """ Given a KeyboardModifiers flags object, return whether the Control
977 key is down.
988 key is down.
978
989
979 Parameters:
990 Parameters:
980 -----------
991 -----------
981 include_command : bool, optional (default True)
992 include_command : bool, optional (default True)
982 Whether to treat the Command key as a (mutually exclusive) synonym
993 Whether to treat the Command key as a (mutually exclusive) synonym
983 for Control when in Mac OS.
994 for Control when in Mac OS.
984 """
995 """
985 # Note that on Mac OS, ControlModifier corresponds to the Command key
996 # Note that on Mac OS, ControlModifier corresponds to the Command key
986 # while MetaModifier corresponds to the Control key.
997 # while MetaModifier corresponds to the Control key.
987 if sys.platform == 'darwin':
998 if sys.platform == 'darwin':
988 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
999 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
989 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1000 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
990 else:
1001 else:
991 return bool(modifiers & QtCore.Qt.ControlModifier)
1002 return bool(modifiers & QtCore.Qt.ControlModifier)
992
1003
993 def _create_control(self):
1004 def _create_control(self):
994 """ Creates and connects the underlying text widget.
1005 """ Creates and connects the underlying text widget.
995 """
1006 """
996 # Create the underlying control.
1007 # Create the underlying control.
997 if self.kind == 'plain':
1008 if self.kind == 'plain':
998 control = QtGui.QPlainTextEdit()
1009 control = QtGui.QPlainTextEdit()
999 elif self.kind == 'rich':
1010 elif self.kind == 'rich':
1000 control = QtGui.QTextEdit()
1011 control = QtGui.QTextEdit()
1001 control.setAcceptRichText(False)
1012 control.setAcceptRichText(False)
1002
1013
1003 # Install event filters. The filter on the viewport is needed for
1014 # Install event filters. The filter on the viewport is needed for
1004 # mouse events and drag events.
1015 # mouse events and drag events.
1005 control.installEventFilter(self)
1016 control.installEventFilter(self)
1006 control.viewport().installEventFilter(self)
1017 control.viewport().installEventFilter(self)
1007
1018
1008 # Connect signals.
1019 # Connect signals.
1009 control.cursorPositionChanged.connect(self._cursor_position_changed)
1020 control.cursorPositionChanged.connect(self._cursor_position_changed)
1010 control.customContextMenuRequested.connect(
1021 control.customContextMenuRequested.connect(
1011 self._custom_context_menu_requested)
1022 self._custom_context_menu_requested)
1012 control.copyAvailable.connect(self.copy_available)
1023 control.copyAvailable.connect(self.copy_available)
1013 control.redoAvailable.connect(self.redo_available)
1024 control.redoAvailable.connect(self.redo_available)
1014 control.undoAvailable.connect(self.undo_available)
1025 control.undoAvailable.connect(self.undo_available)
1015
1026
1016 # Hijack the document size change signal to prevent Qt from adjusting
1027 # Hijack the document size change signal to prevent Qt from adjusting
1017 # the viewport's scrollbar. We are relying on an implementation detail
1028 # the viewport's scrollbar. We are relying on an implementation detail
1018 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1029 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1019 # this functionality we cannot create a nice terminal interface.
1030 # this functionality we cannot create a nice terminal interface.
1020 layout = control.document().documentLayout()
1031 layout = control.document().documentLayout()
1021 layout.documentSizeChanged.disconnect()
1032 layout.documentSizeChanged.disconnect()
1022 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1033 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1023
1034
1024 # Configure the control.
1035 # Configure the control.
1025 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1036 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1026 control.setReadOnly(True)
1037 control.setReadOnly(True)
1027 control.setUndoRedoEnabled(False)
1038 control.setUndoRedoEnabled(False)
1028 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1039 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1029 return control
1040 return control
1030
1041
1031 def _create_page_control(self):
1042 def _create_page_control(self):
1032 """ Creates and connects the underlying paging widget.
1043 """ Creates and connects the underlying paging widget.
1033 """
1044 """
1034 if self.kind == 'plain':
1045 if self.kind == 'plain':
1035 control = QtGui.QPlainTextEdit()
1046 control = QtGui.QPlainTextEdit()
1036 elif self.kind == 'rich':
1047 elif self.kind == 'rich':
1037 control = QtGui.QTextEdit()
1048 control = QtGui.QTextEdit()
1038 control.installEventFilter(self)
1049 control.installEventFilter(self)
1039 control.setReadOnly(True)
1050 control.setReadOnly(True)
1040 control.setUndoRedoEnabled(False)
1051 control.setUndoRedoEnabled(False)
1041 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1052 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1042 return control
1053 return control
1043
1054
1044 def _event_filter_console_keypress(self, event):
1055 def _event_filter_console_keypress(self, event):
1045 """ Filter key events for the underlying text widget to create a
1056 """ Filter key events for the underlying text widget to create a
1046 console-like interface.
1057 console-like interface.
1047 """
1058 """
1048 intercepted = False
1059 intercepted = False
1049 cursor = self._control.textCursor()
1060 cursor = self._control.textCursor()
1050 position = cursor.position()
1061 position = cursor.position()
1051 key = event.key()
1062 key = event.key()
1052 ctrl_down = self._control_key_down(event.modifiers())
1063 ctrl_down = self._control_key_down(event.modifiers())
1053 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1064 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1054 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1065 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1055
1066
1056 #------ Special sequences ----------------------------------------------
1067 #------ Special sequences ----------------------------------------------
1057
1068
1058 if event.matches(QtGui.QKeySequence.Copy):
1069 if event.matches(QtGui.QKeySequence.Copy):
1059 self.copy()
1070 self.copy()
1060 intercepted = True
1071 intercepted = True
1061
1072
1062 elif event.matches(QtGui.QKeySequence.Cut):
1073 elif event.matches(QtGui.QKeySequence.Cut):
1063 self.cut()
1074 self.cut()
1064 intercepted = True
1075 intercepted = True
1065
1076
1066 elif event.matches(QtGui.QKeySequence.Paste):
1077 elif event.matches(QtGui.QKeySequence.Paste):
1067 self.paste()
1078 self.paste()
1068 intercepted = True
1079 intercepted = True
1069
1080
1070 #------ Special modifier logic -----------------------------------------
1081 #------ Special modifier logic -----------------------------------------
1071
1082
1072 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1083 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1073 intercepted = True
1084 intercepted = True
1074
1085
1075 # Special handling when tab completing in text mode.
1086 # Special handling when tab completing in text mode.
1076 self._cancel_text_completion()
1087 self._cancel_text_completion()
1077
1088
1078 if self._in_buffer(position):
1089 if self._in_buffer(position):
1079 if self._reading:
1090 if self._reading:
1080 self._append_plain_text('\n')
1091 self._append_plain_text('\n')
1081 self._reading = False
1092 self._reading = False
1082 if self._reading_callback:
1093 if self._reading_callback:
1083 self._reading_callback()
1094 self._reading_callback()
1084
1095
1085 # If the input buffer is a single line or there is only
1096 # If the input buffer is a single line or there is only
1086 # whitespace after the cursor, execute. Otherwise, split the
1097 # whitespace after the cursor, execute. Otherwise, split the
1087 # line with a continuation prompt.
1098 # line with a continuation prompt.
1088 elif not self._executing:
1099 elif not self._executing:
1089 cursor.movePosition(QtGui.QTextCursor.End,
1100 cursor.movePosition(QtGui.QTextCursor.End,
1090 QtGui.QTextCursor.KeepAnchor)
1101 QtGui.QTextCursor.KeepAnchor)
1091 at_end = cursor.selectedText().trimmed().isEmpty()
1102 at_end = len(cursor.selectedText().strip()) == 0
1092 single_line = (self._get_end_cursor().blockNumber() ==
1103 single_line = (self._get_end_cursor().blockNumber() ==
1093 self._get_prompt_cursor().blockNumber())
1104 self._get_prompt_cursor().blockNumber())
1094 if (at_end or shift_down or single_line) and not ctrl_down:
1105 if (at_end or shift_down or single_line) and not ctrl_down:
1095 self.execute(interactive = not shift_down)
1106 self.execute(interactive = not shift_down)
1096 else:
1107 else:
1097 # Do this inside an edit block for clean undo/redo.
1108 # Do this inside an edit block for clean undo/redo.
1098 cursor.beginEditBlock()
1109 cursor.beginEditBlock()
1099 cursor.setPosition(position)
1110 cursor.setPosition(position)
1100 cursor.insertText('\n')
1111 cursor.insertText('\n')
1101 self._insert_continuation_prompt(cursor)
1112 self._insert_continuation_prompt(cursor)
1102 cursor.endEditBlock()
1113 cursor.endEditBlock()
1103
1114
1104 # Ensure that the whole input buffer is visible.
1115 # Ensure that the whole input buffer is visible.
1105 # FIXME: This will not be usable if the input buffer is
1116 # FIXME: This will not be usable if the input buffer is
1106 # taller than the console widget.
1117 # taller than the console widget.
1107 self._control.moveCursor(QtGui.QTextCursor.End)
1118 self._control.moveCursor(QtGui.QTextCursor.End)
1108 self._control.setTextCursor(cursor)
1119 self._control.setTextCursor(cursor)
1109
1120
1110 #------ Control/Cmd modifier -------------------------------------------
1121 #------ Control/Cmd modifier -------------------------------------------
1111
1122
1112 elif ctrl_down:
1123 elif ctrl_down:
1113 if key == QtCore.Qt.Key_G:
1124 if key == QtCore.Qt.Key_G:
1114 self._keyboard_quit()
1125 self._keyboard_quit()
1115 intercepted = True
1126 intercepted = True
1116
1127
1117 elif key == QtCore.Qt.Key_K:
1128 elif key == QtCore.Qt.Key_K:
1118 if self._in_buffer(position):
1129 if self._in_buffer(position):
1119 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1130 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1120 QtGui.QTextCursor.KeepAnchor)
1131 QtGui.QTextCursor.KeepAnchor)
1121 if not cursor.hasSelection():
1132 if not cursor.hasSelection():
1122 # Line deletion (remove continuation prompt)
1133 # Line deletion (remove continuation prompt)
1123 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1134 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1124 QtGui.QTextCursor.KeepAnchor)
1135 QtGui.QTextCursor.KeepAnchor)
1125 cursor.movePosition(QtGui.QTextCursor.Right,
1136 cursor.movePosition(QtGui.QTextCursor.Right,
1126 QtGui.QTextCursor.KeepAnchor,
1137 QtGui.QTextCursor.KeepAnchor,
1127 len(self._continuation_prompt))
1138 len(self._continuation_prompt))
1128 cursor.removeSelectedText()
1139 cursor.removeSelectedText()
1129 intercepted = True
1140 intercepted = True
1130
1141
1131 elif key == QtCore.Qt.Key_L:
1142 elif key == QtCore.Qt.Key_L:
1132 self.prompt_to_top()
1143 self.prompt_to_top()
1133 intercepted = True
1144 intercepted = True
1134
1145
1135 elif key == QtCore.Qt.Key_O:
1146 elif key == QtCore.Qt.Key_O:
1136 if self._page_control and self._page_control.isVisible():
1147 if self._page_control and self._page_control.isVisible():
1137 self._page_control.setFocus()
1148 self._page_control.setFocus()
1138 intercepted = True
1149 intercepted = True
1139
1150
1140 elif key == QtCore.Qt.Key_Y:
1151 elif key == QtCore.Qt.Key_Y:
1141 self.paste()
1152 self.paste()
1142 intercepted = True
1153 intercepted = True
1143
1154
1144 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1155 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1145 intercepted = True
1156 intercepted = True
1146
1157
1147 elif key == QtCore.Qt.Key_Plus:
1158 elif key == QtCore.Qt.Key_Plus:
1148 self.change_font_size(1)
1159 self.change_font_size(1)
1149 intercepted = True
1160 intercepted = True
1150
1161
1151 elif key == QtCore.Qt.Key_Minus:
1162 elif key == QtCore.Qt.Key_Minus:
1152 self.change_font_size(-1)
1163 self.change_font_size(-1)
1153 intercepted = True
1164 intercepted = True
1154
1165
1155 #------ Alt modifier ---------------------------------------------------
1166 #------ Alt modifier ---------------------------------------------------
1156
1167
1157 elif alt_down:
1168 elif alt_down:
1158 if key == QtCore.Qt.Key_B:
1169 if key == QtCore.Qt.Key_B:
1159 self._set_cursor(self._get_word_start_cursor(position))
1170 self._set_cursor(self._get_word_start_cursor(position))
1160 intercepted = True
1171 intercepted = True
1161
1172
1162 elif key == QtCore.Qt.Key_F:
1173 elif key == QtCore.Qt.Key_F:
1163 self._set_cursor(self._get_word_end_cursor(position))
1174 self._set_cursor(self._get_word_end_cursor(position))
1164 intercepted = True
1175 intercepted = True
1165
1176
1166 elif key == QtCore.Qt.Key_Backspace:
1177 elif key == QtCore.Qt.Key_Backspace:
1167 cursor = self._get_word_start_cursor(position)
1178 cursor = self._get_word_start_cursor(position)
1168 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1179 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1169 cursor.removeSelectedText()
1180 cursor.removeSelectedText()
1170 intercepted = True
1181 intercepted = True
1171
1182
1172 elif key == QtCore.Qt.Key_D:
1183 elif key == QtCore.Qt.Key_D:
1173 cursor = self._get_word_end_cursor(position)
1184 cursor = self._get_word_end_cursor(position)
1174 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1185 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1175 cursor.removeSelectedText()
1186 cursor.removeSelectedText()
1176 intercepted = True
1187 intercepted = True
1177
1188
1178 elif key == QtCore.Qt.Key_Delete:
1189 elif key == QtCore.Qt.Key_Delete:
1179 intercepted = True
1190 intercepted = True
1180
1191
1181 elif key == QtCore.Qt.Key_Greater:
1192 elif key == QtCore.Qt.Key_Greater:
1182 self._control.moveCursor(QtGui.QTextCursor.End)
1193 self._control.moveCursor(QtGui.QTextCursor.End)
1183 intercepted = True
1194 intercepted = True
1184
1195
1185 elif key == QtCore.Qt.Key_Less:
1196 elif key == QtCore.Qt.Key_Less:
1186 self._control.setTextCursor(self._get_prompt_cursor())
1197 self._control.setTextCursor(self._get_prompt_cursor())
1187 intercepted = True
1198 intercepted = True
1188
1199
1189 #------ No modifiers ---------------------------------------------------
1200 #------ No modifiers ---------------------------------------------------
1190
1201
1191 else:
1202 else:
1192 if shift_down:
1203 if shift_down:
1193 anchormode=QtGui.QTextCursor.KeepAnchor
1204 anchormode=QtGui.QTextCursor.KeepAnchor
1194 else:
1205 else:
1195 anchormode=QtGui.QTextCursor.MoveAnchor
1206 anchormode=QtGui.QTextCursor.MoveAnchor
1196
1207
1197 if key == QtCore.Qt.Key_Escape:
1208 if key == QtCore.Qt.Key_Escape:
1198 self._keyboard_quit()
1209 self._keyboard_quit()
1199 intercepted = True
1210 intercepted = True
1200
1211
1201 elif key == QtCore.Qt.Key_Up:
1212 elif key == QtCore.Qt.Key_Up:
1202 if self._reading or not self._up_pressed():
1213 if self._reading or not self._up_pressed():
1203 intercepted = True
1214 intercepted = True
1204 else:
1215 else:
1205 prompt_line = self._get_prompt_cursor().blockNumber()
1216 prompt_line = self._get_prompt_cursor().blockNumber()
1206 intercepted = cursor.blockNumber() <= prompt_line
1217 intercepted = cursor.blockNumber() <= prompt_line
1207
1218
1208 elif key == QtCore.Qt.Key_Down:
1219 elif key == QtCore.Qt.Key_Down:
1209 if self._reading or not self._down_pressed():
1220 if self._reading or not self._down_pressed():
1210 intercepted = True
1221 intercepted = True
1211 else:
1222 else:
1212 end_line = self._get_end_cursor().blockNumber()
1223 end_line = self._get_end_cursor().blockNumber()
1213 intercepted = cursor.blockNumber() == end_line
1224 intercepted = cursor.blockNumber() == end_line
1214
1225
1215 elif key == QtCore.Qt.Key_Tab:
1226 elif key == QtCore.Qt.Key_Tab:
1216 if not self._reading:
1227 if not self._reading:
1217 intercepted = not self._tab_pressed()
1228 intercepted = not self._tab_pressed()
1218
1229
1219 elif key == QtCore.Qt.Key_Left:
1230 elif key == QtCore.Qt.Key_Left:
1220
1231
1221 # Move to the previous line
1232 # Move to the previous line
1222 line, col = cursor.blockNumber(), cursor.columnNumber()
1233 line, col = cursor.blockNumber(), cursor.columnNumber()
1223 if line > self._get_prompt_cursor().blockNumber() and \
1234 if line > self._get_prompt_cursor().blockNumber() and \
1224 col == len(self._continuation_prompt):
1235 col == len(self._continuation_prompt):
1225 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1236 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1226 mode=anchormode)
1237 mode=anchormode)
1227 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1238 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1228 mode=anchormode)
1239 mode=anchormode)
1229 intercepted = True
1240 intercepted = True
1230
1241
1231 # Regular left movement
1242 # Regular left movement
1232 else:
1243 else:
1233 intercepted = not self._in_buffer(position - 1)
1244 intercepted = not self._in_buffer(position - 1)
1234
1245
1235 elif key == QtCore.Qt.Key_Right:
1246 elif key == QtCore.Qt.Key_Right:
1236 original_block_number = cursor.blockNumber()
1247 original_block_number = cursor.blockNumber()
1237 cursor.movePosition(QtGui.QTextCursor.Right,
1248 cursor.movePosition(QtGui.QTextCursor.Right,
1238 mode=anchormode)
1249 mode=anchormode)
1239 if cursor.blockNumber() != original_block_number:
1250 if cursor.blockNumber() != original_block_number:
1240 cursor.movePosition(QtGui.QTextCursor.Right,
1251 cursor.movePosition(QtGui.QTextCursor.Right,
1241 n=len(self._continuation_prompt),
1252 n=len(self._continuation_prompt),
1242 mode=anchormode)
1253 mode=anchormode)
1243 self._set_cursor(cursor)
1254 self._set_cursor(cursor)
1244 intercepted = True
1255 intercepted = True
1245
1256
1246 elif key == QtCore.Qt.Key_Home:
1257 elif key == QtCore.Qt.Key_Home:
1247 start_line = cursor.blockNumber()
1258 start_line = cursor.blockNumber()
1248 if start_line == self._get_prompt_cursor().blockNumber():
1259 if start_line == self._get_prompt_cursor().blockNumber():
1249 start_pos = self._prompt_pos
1260 start_pos = self._prompt_pos
1250 else:
1261 else:
1251 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1262 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1252 QtGui.QTextCursor.KeepAnchor)
1263 QtGui.QTextCursor.KeepAnchor)
1253 start_pos = cursor.position()
1264 start_pos = cursor.position()
1254 start_pos += len(self._continuation_prompt)
1265 start_pos += len(self._continuation_prompt)
1255 cursor.setPosition(position)
1266 cursor.setPosition(position)
1256 if shift_down and self._in_buffer(position):
1267 if shift_down and self._in_buffer(position):
1257 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1268 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1258 else:
1269 else:
1259 cursor.setPosition(start_pos)
1270 cursor.setPosition(start_pos)
1260 self._set_cursor(cursor)
1271 self._set_cursor(cursor)
1261 intercepted = True
1272 intercepted = True
1262
1273
1263 elif key == QtCore.Qt.Key_Backspace:
1274 elif key == QtCore.Qt.Key_Backspace:
1264
1275
1265 # Line deletion (remove continuation prompt)
1276 # Line deletion (remove continuation prompt)
1266 line, col = cursor.blockNumber(), cursor.columnNumber()
1277 line, col = cursor.blockNumber(), cursor.columnNumber()
1267 if not self._reading and \
1278 if not self._reading and \
1268 col == len(self._continuation_prompt) and \
1279 col == len(self._continuation_prompt) and \
1269 line > self._get_prompt_cursor().blockNumber():
1280 line > self._get_prompt_cursor().blockNumber():
1270 cursor.beginEditBlock()
1281 cursor.beginEditBlock()
1271 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1282 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1272 QtGui.QTextCursor.KeepAnchor)
1283 QtGui.QTextCursor.KeepAnchor)
1273 cursor.removeSelectedText()
1284 cursor.removeSelectedText()
1274 cursor.deletePreviousChar()
1285 cursor.deletePreviousChar()
1275 cursor.endEditBlock()
1286 cursor.endEditBlock()
1276 intercepted = True
1287 intercepted = True
1277
1288
1278 # Regular backwards deletion
1289 # Regular backwards deletion
1279 else:
1290 else:
1280 anchor = cursor.anchor()
1291 anchor = cursor.anchor()
1281 if anchor == position:
1292 if anchor == position:
1282 intercepted = not self._in_buffer(position - 1)
1293 intercepted = not self._in_buffer(position - 1)
1283 else:
1294 else:
1284 intercepted = not self._in_buffer(min(anchor, position))
1295 intercepted = not self._in_buffer(min(anchor, position))
1285
1296
1286 elif key == QtCore.Qt.Key_Delete:
1297 elif key == QtCore.Qt.Key_Delete:
1287
1298
1288 # Line deletion (remove continuation prompt)
1299 # Line deletion (remove continuation prompt)
1289 if not self._reading and self._in_buffer(position) and \
1300 if not self._reading and self._in_buffer(position) and \
1290 cursor.atBlockEnd() and not cursor.hasSelection():
1301 cursor.atBlockEnd() and not cursor.hasSelection():
1291 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1302 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1292 QtGui.QTextCursor.KeepAnchor)
1303 QtGui.QTextCursor.KeepAnchor)
1293 cursor.movePosition(QtGui.QTextCursor.Right,
1304 cursor.movePosition(QtGui.QTextCursor.Right,
1294 QtGui.QTextCursor.KeepAnchor,
1305 QtGui.QTextCursor.KeepAnchor,
1295 len(self._continuation_prompt))
1306 len(self._continuation_prompt))
1296 cursor.removeSelectedText()
1307 cursor.removeSelectedText()
1297 intercepted = True
1308 intercepted = True
1298
1309
1299 # Regular forwards deletion:
1310 # Regular forwards deletion:
1300 else:
1311 else:
1301 anchor = cursor.anchor()
1312 anchor = cursor.anchor()
1302 intercepted = (not self._in_buffer(anchor) or
1313 intercepted = (not self._in_buffer(anchor) or
1303 not self._in_buffer(position))
1314 not self._in_buffer(position))
1304
1315
1305 # Don't move the cursor if control is down to allow copy-paste using
1316 # Don't move the cursor if control is down to allow copy-paste using
1306 # the keyboard in any part of the buffer.
1317 # the keyboard in any part of the buffer.
1307 if not ctrl_down:
1318 if not ctrl_down:
1308 self._keep_cursor_in_buffer()
1319 self._keep_cursor_in_buffer()
1309
1320
1310 return intercepted
1321 return intercepted
1311
1322
1312 def _event_filter_page_keypress(self, event):
1323 def _event_filter_page_keypress(self, event):
1313 """ Filter key events for the paging widget to create console-like
1324 """ Filter key events for the paging widget to create console-like
1314 interface.
1325 interface.
1315 """
1326 """
1316 key = event.key()
1327 key = event.key()
1317 ctrl_down = self._control_key_down(event.modifiers())
1328 ctrl_down = self._control_key_down(event.modifiers())
1318 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1329 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1319
1330
1320 if ctrl_down:
1331 if ctrl_down:
1321 if key == QtCore.Qt.Key_O:
1332 if key == QtCore.Qt.Key_O:
1322 self._control.setFocus()
1333 self._control.setFocus()
1323 intercept = True
1334 intercept = True
1324
1335
1325 elif alt_down:
1336 elif alt_down:
1326 if key == QtCore.Qt.Key_Greater:
1337 if key == QtCore.Qt.Key_Greater:
1327 self._page_control.moveCursor(QtGui.QTextCursor.End)
1338 self._page_control.moveCursor(QtGui.QTextCursor.End)
1328 intercepted = True
1339 intercepted = True
1329
1340
1330 elif key == QtCore.Qt.Key_Less:
1341 elif key == QtCore.Qt.Key_Less:
1331 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1342 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1332 intercepted = True
1343 intercepted = True
1333
1344
1334 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1345 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1335 if self._splitter:
1346 if self._splitter:
1336 self._page_control.hide()
1347 self._page_control.hide()
1337 else:
1348 else:
1338 self.layout().setCurrentWidget(self._control)
1349 self.layout().setCurrentWidget(self._control)
1339 return True
1350 return True
1340
1351
1341 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1352 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1342 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1353 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1343 QtCore.Qt.Key_PageDown,
1354 QtCore.Qt.Key_PageDown,
1344 QtCore.Qt.NoModifier)
1355 QtCore.Qt.NoModifier)
1345 QtGui.qApp.sendEvent(self._page_control, new_event)
1356 QtGui.qApp.sendEvent(self._page_control, new_event)
1346 return True
1357 return True
1347
1358
1348 elif key == QtCore.Qt.Key_Backspace:
1359 elif key == QtCore.Qt.Key_Backspace:
1349 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1360 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1350 QtCore.Qt.Key_PageUp,
1361 QtCore.Qt.Key_PageUp,
1351 QtCore.Qt.NoModifier)
1362 QtCore.Qt.NoModifier)
1352 QtGui.qApp.sendEvent(self._page_control, new_event)
1363 QtGui.qApp.sendEvent(self._page_control, new_event)
1353 return True
1364 return True
1354
1365
1355 return False
1366 return False
1356
1367
1357 def _format_as_columns(self, items, separator=' '):
1368 def _format_as_columns(self, items, separator=' '):
1358 """ Transform a list of strings into a single string with columns.
1369 """ Transform a list of strings into a single string with columns.
1359
1370
1360 Parameters
1371 Parameters
1361 ----------
1372 ----------
1362 items : sequence of strings
1373 items : sequence of strings
1363 The strings to process.
1374 The strings to process.
1364
1375
1365 separator : str, optional [default is two spaces]
1376 separator : str, optional [default is two spaces]
1366 The string that separates columns.
1377 The string that separates columns.
1367
1378
1368 Returns
1379 Returns
1369 -------
1380 -------
1370 The formatted string.
1381 The formatted string.
1371 """
1382 """
1372 # Note: this code is adapted from columnize 0.3.2.
1383 # Note: this code is adapted from columnize 0.3.2.
1373 # See http://code.google.com/p/pycolumnize/
1384 # See http://code.google.com/p/pycolumnize/
1374
1385
1375 # Calculate the number of characters available.
1386 # Calculate the number of characters available.
1376 width = self._control.viewport().width()
1387 width = self._control.viewport().width()
1377 char_width = QtGui.QFontMetrics(self.font).width(' ')
1388 char_width = QtGui.QFontMetrics(self.font).width(' ')
1378 displaywidth = max(10, (width / char_width) - 1)
1389 displaywidth = max(10, (width / char_width) - 1)
1379
1390
1380 # Some degenerate cases.
1391 # Some degenerate cases.
1381 size = len(items)
1392 size = len(items)
1382 if size == 0:
1393 if size == 0:
1383 return '\n'
1394 return '\n'
1384 elif size == 1:
1395 elif size == 1:
1385 return '%s\n' % items[0]
1396 return '%s\n' % items[0]
1386
1397
1387 # Try every row count from 1 upwards
1398 # Try every row count from 1 upwards
1388 array_index = lambda nrows, row, col: nrows*col + row
1399 array_index = lambda nrows, row, col: nrows*col + row
1389 for nrows in range(1, size):
1400 for nrows in range(1, size):
1390 ncols = (size + nrows - 1) // nrows
1401 ncols = (size + nrows - 1) // nrows
1391 colwidths = []
1402 colwidths = []
1392 totwidth = -len(separator)
1403 totwidth = -len(separator)
1393 for col in range(ncols):
1404 for col in range(ncols):
1394 # Get max column width for this column
1405 # Get max column width for this column
1395 colwidth = 0
1406 colwidth = 0
1396 for row in range(nrows):
1407 for row in range(nrows):
1397 i = array_index(nrows, row, col)
1408 i = array_index(nrows, row, col)
1398 if i >= size: break
1409 if i >= size: break
1399 x = items[i]
1410 x = items[i]
1400 colwidth = max(colwidth, len(x))
1411 colwidth = max(colwidth, len(x))
1401 colwidths.append(colwidth)
1412 colwidths.append(colwidth)
1402 totwidth += colwidth + len(separator)
1413 totwidth += colwidth + len(separator)
1403 if totwidth > displaywidth:
1414 if totwidth > displaywidth:
1404 break
1415 break
1405 if totwidth <= displaywidth:
1416 if totwidth <= displaywidth:
1406 break
1417 break
1407
1418
1408 # The smallest number of rows computed and the max widths for each
1419 # The smallest number of rows computed and the max widths for each
1409 # column has been obtained. Now we just have to format each of the rows.
1420 # column has been obtained. Now we just have to format each of the rows.
1410 string = ''
1421 string = ''
1411 for row in range(nrows):
1422 for row in range(nrows):
1412 texts = []
1423 texts = []
1413 for col in range(ncols):
1424 for col in range(ncols):
1414 i = row + nrows*col
1425 i = row + nrows*col
1415 if i >= size:
1426 if i >= size:
1416 texts.append('')
1427 texts.append('')
1417 else:
1428 else:
1418 texts.append(items[i])
1429 texts.append(items[i])
1419 while texts and not texts[-1]:
1430 while texts and not texts[-1]:
1420 del texts[-1]
1431 del texts[-1]
1421 for col in range(len(texts)):
1432 for col in range(len(texts)):
1422 texts[col] = texts[col].ljust(colwidths[col])
1433 texts[col] = texts[col].ljust(colwidths[col])
1423 string += '%s\n' % separator.join(texts)
1434 string += '%s\n' % separator.join(texts)
1424 return string
1435 return string
1425
1436
1426 def _get_block_plain_text(self, block):
1437 def _get_block_plain_text(self, block):
1427 """ Given a QTextBlock, return its unformatted text.
1438 """ Given a QTextBlock, return its unformatted text.
1428 """
1439 """
1429 cursor = QtGui.QTextCursor(block)
1440 cursor = QtGui.QTextCursor(block)
1430 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1441 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1431 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1442 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1432 QtGui.QTextCursor.KeepAnchor)
1443 QtGui.QTextCursor.KeepAnchor)
1433 return unicode(cursor.selection().toPlainText())
1444 return cursor.selection().toPlainText()
1434
1445
1435 def _get_cursor(self):
1446 def _get_cursor(self):
1436 """ Convenience method that returns a cursor for the current position.
1447 """ Convenience method that returns a cursor for the current position.
1437 """
1448 """
1438 return self._control.textCursor()
1449 return self._control.textCursor()
1439
1450
1440 def _get_end_cursor(self):
1451 def _get_end_cursor(self):
1441 """ Convenience method that returns a cursor for the last character.
1452 """ Convenience method that returns a cursor for the last character.
1442 """
1453 """
1443 cursor = self._control.textCursor()
1454 cursor = self._control.textCursor()
1444 cursor.movePosition(QtGui.QTextCursor.End)
1455 cursor.movePosition(QtGui.QTextCursor.End)
1445 return cursor
1456 return cursor
1446
1457
1447 def _get_input_buffer_cursor_column(self):
1458 def _get_input_buffer_cursor_column(self):
1448 """ Returns the column of the cursor in the input buffer, excluding the
1459 """ Returns the column of the cursor in the input buffer, excluding the
1449 contribution by the prompt, or -1 if there is no such column.
1460 contribution by the prompt, or -1 if there is no such column.
1450 """
1461 """
1451 prompt = self._get_input_buffer_cursor_prompt()
1462 prompt = self._get_input_buffer_cursor_prompt()
1452 if prompt is None:
1463 if prompt is None:
1453 return -1
1464 return -1
1454 else:
1465 else:
1455 cursor = self._control.textCursor()
1466 cursor = self._control.textCursor()
1456 return cursor.columnNumber() - len(prompt)
1467 return cursor.columnNumber() - len(prompt)
1457
1468
1458 def _get_input_buffer_cursor_line(self):
1469 def _get_input_buffer_cursor_line(self):
1459 """ Returns the text of the line of the input buffer that contains the
1470 """ Returns the text of the line of the input buffer that contains the
1460 cursor, or None if there is no such line.
1471 cursor, or None if there is no such line.
1461 """
1472 """
1462 prompt = self._get_input_buffer_cursor_prompt()
1473 prompt = self._get_input_buffer_cursor_prompt()
1463 if prompt is None:
1474 if prompt is None:
1464 return None
1475 return None
1465 else:
1476 else:
1466 cursor = self._control.textCursor()
1477 cursor = self._control.textCursor()
1467 text = self._get_block_plain_text(cursor.block())
1478 text = self._get_block_plain_text(cursor.block())
1468 return text[len(prompt):]
1479 return text[len(prompt):]
1469
1480
1470 def _get_input_buffer_cursor_prompt(self):
1481 def _get_input_buffer_cursor_prompt(self):
1471 """ Returns the (plain text) prompt for line of the input buffer that
1482 """ Returns the (plain text) prompt for line of the input buffer that
1472 contains the cursor, or None if there is no such line.
1483 contains the cursor, or None if there is no such line.
1473 """
1484 """
1474 if self._executing:
1485 if self._executing:
1475 return None
1486 return None
1476 cursor = self._control.textCursor()
1487 cursor = self._control.textCursor()
1477 if cursor.position() >= self._prompt_pos:
1488 if cursor.position() >= self._prompt_pos:
1478 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1489 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1479 return self._prompt
1490 return self._prompt
1480 else:
1491 else:
1481 return self._continuation_prompt
1492 return self._continuation_prompt
1482 else:
1493 else:
1483 return None
1494 return None
1484
1495
1485 def _get_prompt_cursor(self):
1496 def _get_prompt_cursor(self):
1486 """ Convenience method that returns a cursor for the prompt position.
1497 """ Convenience method that returns a cursor for the prompt position.
1487 """
1498 """
1488 cursor = self._control.textCursor()
1499 cursor = self._control.textCursor()
1489 cursor.setPosition(self._prompt_pos)
1500 cursor.setPosition(self._prompt_pos)
1490 return cursor
1501 return cursor
1491
1502
1492 def _get_selection_cursor(self, start, end):
1503 def _get_selection_cursor(self, start, end):
1493 """ Convenience method that returns a cursor with text selected between
1504 """ Convenience method that returns a cursor with text selected between
1494 the positions 'start' and 'end'.
1505 the positions 'start' and 'end'.
1495 """
1506 """
1496 cursor = self._control.textCursor()
1507 cursor = self._control.textCursor()
1497 cursor.setPosition(start)
1508 cursor.setPosition(start)
1498 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1509 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1499 return cursor
1510 return cursor
1500
1511
1501 def _get_word_start_cursor(self, position):
1512 def _get_word_start_cursor(self, position):
1502 """ Find the start of the word to the left the given position. If a
1513 """ Find the start of the word to the left the given position. If a
1503 sequence of non-word characters precedes the first word, skip over
1514 sequence of non-word characters precedes the first word, skip over
1504 them. (This emulates the behavior of bash, emacs, etc.)
1515 them. (This emulates the behavior of bash, emacs, etc.)
1505 """
1516 """
1506 document = self._control.document()
1517 document = self._control.document()
1507 position -= 1
1518 position -= 1
1508 while position >= self._prompt_pos and \
1519 while position >= self._prompt_pos and \
1509 not document.characterAt(position).isLetterOrNumber():
1520 not is_letter_or_number(document.characterAt(position)):
1510 position -= 1
1521 position -= 1
1511 while position >= self._prompt_pos and \
1522 while position >= self._prompt_pos and \
1512 document.characterAt(position).isLetterOrNumber():
1523 is_letter_or_number(document.characterAt(position)):
1513 position -= 1
1524 position -= 1
1514 cursor = self._control.textCursor()
1525 cursor = self._control.textCursor()
1515 cursor.setPosition(position + 1)
1526 cursor.setPosition(position + 1)
1516 return cursor
1527 return cursor
1517
1528
1518 def _get_word_end_cursor(self, position):
1529 def _get_word_end_cursor(self, position):
1519 """ Find the end of the word to the right the given position. If a
1530 """ Find the end of the word to the right the given position. If a
1520 sequence of non-word characters precedes the first word, skip over
1531 sequence of non-word characters precedes the first word, skip over
1521 them. (This emulates the behavior of bash, emacs, etc.)
1532 them. (This emulates the behavior of bash, emacs, etc.)
1522 """
1533 """
1523 document = self._control.document()
1534 document = self._control.document()
1524 end = self._get_end_cursor().position()
1535 end = self._get_end_cursor().position()
1525 while position < end and \
1536 while position < end and \
1526 not document.characterAt(position).isLetterOrNumber():
1537 not is_letter_or_number(document.characterAt(position)):
1527 position += 1
1538 position += 1
1528 while position < end and \
1539 while position < end and \
1529 document.characterAt(position).isLetterOrNumber():
1540 is_letter_or_number(document.characterAt(position)):
1530 position += 1
1541 position += 1
1531 cursor = self._control.textCursor()
1542 cursor = self._control.textCursor()
1532 cursor.setPosition(position)
1543 cursor.setPosition(position)
1533 return cursor
1544 return cursor
1534
1545
1535 def _insert_continuation_prompt(self, cursor):
1546 def _insert_continuation_prompt(self, cursor):
1536 """ Inserts new continuation prompt using the specified cursor.
1547 """ Inserts new continuation prompt using the specified cursor.
1537 """
1548 """
1538 if self._continuation_prompt_html is None:
1549 if self._continuation_prompt_html is None:
1539 self._insert_plain_text(cursor, self._continuation_prompt)
1550 self._insert_plain_text(cursor, self._continuation_prompt)
1540 else:
1551 else:
1541 self._continuation_prompt = self._insert_html_fetching_plain_text(
1552 self._continuation_prompt = self._insert_html_fetching_plain_text(
1542 cursor, self._continuation_prompt_html)
1553 cursor, self._continuation_prompt_html)
1543
1554
1544 def _insert_html(self, cursor, html):
1555 def _insert_html(self, cursor, html):
1545 """ Inserts HTML using the specified cursor in such a way that future
1556 """ Inserts HTML using the specified cursor in such a way that future
1546 formatting is unaffected.
1557 formatting is unaffected.
1547 """
1558 """
1548 cursor.beginEditBlock()
1559 cursor.beginEditBlock()
1549 cursor.insertHtml(html)
1560 cursor.insertHtml(html)
1550
1561
1551 # After inserting HTML, the text document "remembers" it's in "html
1562 # After inserting HTML, the text document "remembers" it's in "html
1552 # mode", which means that subsequent calls adding plain text will result
1563 # mode", which means that subsequent calls adding plain text will result
1553 # in unwanted formatting, lost tab characters, etc. The following code
1564 # in unwanted formatting, lost tab characters, etc. The following code
1554 # hacks around this behavior, which I consider to be a bug in Qt, by
1565 # hacks around this behavior, which I consider to be a bug in Qt, by
1555 # (crudely) resetting the document's style state.
1566 # (crudely) resetting the document's style state.
1556 cursor.movePosition(QtGui.QTextCursor.Left,
1567 cursor.movePosition(QtGui.QTextCursor.Left,
1557 QtGui.QTextCursor.KeepAnchor)
1568 QtGui.QTextCursor.KeepAnchor)
1558 if cursor.selection().toPlainText() == ' ':
1569 if cursor.selection().toPlainText() == ' ':
1559 cursor.removeSelectedText()
1570 cursor.removeSelectedText()
1560 else:
1571 else:
1561 cursor.movePosition(QtGui.QTextCursor.Right)
1572 cursor.movePosition(QtGui.QTextCursor.Right)
1562 cursor.insertText(' ', QtGui.QTextCharFormat())
1573 cursor.insertText(' ', QtGui.QTextCharFormat())
1563 cursor.endEditBlock()
1574 cursor.endEditBlock()
1564
1575
1565 def _insert_html_fetching_plain_text(self, cursor, html):
1576 def _insert_html_fetching_plain_text(self, cursor, html):
1566 """ Inserts HTML using the specified cursor, then returns its plain text
1577 """ Inserts HTML using the specified cursor, then returns its plain text
1567 version.
1578 version.
1568 """
1579 """
1569 cursor.beginEditBlock()
1580 cursor.beginEditBlock()
1570 cursor.removeSelectedText()
1581 cursor.removeSelectedText()
1571
1582
1572 start = cursor.position()
1583 start = cursor.position()
1573 self._insert_html(cursor, html)
1584 self._insert_html(cursor, html)
1574 end = cursor.position()
1585 end = cursor.position()
1575 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1586 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1576 text = unicode(cursor.selection().toPlainText())
1587 text = cursor.selection().toPlainText()
1577
1588
1578 cursor.setPosition(end)
1589 cursor.setPosition(end)
1579 cursor.endEditBlock()
1590 cursor.endEditBlock()
1580 return text
1591 return text
1581
1592
1582 def _insert_plain_text(self, cursor, text):
1593 def _insert_plain_text(self, cursor, text):
1583 """ Inserts plain text using the specified cursor, processing ANSI codes
1594 """ Inserts plain text using the specified cursor, processing ANSI codes
1584 if enabled.
1595 if enabled.
1585 """
1596 """
1586 cursor.beginEditBlock()
1597 cursor.beginEditBlock()
1587 if self.ansi_codes:
1598 if self.ansi_codes:
1588 for substring in self._ansi_processor.split_string(text):
1599 for substring in self._ansi_processor.split_string(text):
1589 for act in self._ansi_processor.actions:
1600 for act in self._ansi_processor.actions:
1590
1601
1591 # Unlike real terminal emulators, we don't distinguish
1602 # Unlike real terminal emulators, we don't distinguish
1592 # between the screen and the scrollback buffer. A screen
1603 # between the screen and the scrollback buffer. A screen
1593 # erase request clears everything.
1604 # erase request clears everything.
1594 if act.action == 'erase' and act.area == 'screen':
1605 if act.action == 'erase' and act.area == 'screen':
1595 cursor.select(QtGui.QTextCursor.Document)
1606 cursor.select(QtGui.QTextCursor.Document)
1596 cursor.removeSelectedText()
1607 cursor.removeSelectedText()
1597
1608
1598 # Simulate a form feed by scrolling just past the last line.
1609 # Simulate a form feed by scrolling just past the last line.
1599 elif act.action == 'scroll' and act.unit == 'page':
1610 elif act.action == 'scroll' and act.unit == 'page':
1600 cursor.insertText('\n')
1611 cursor.insertText('\n')
1601 cursor.endEditBlock()
1612 cursor.endEditBlock()
1602 self._set_top_cursor(cursor)
1613 self._set_top_cursor(cursor)
1603 cursor.joinPreviousEditBlock()
1614 cursor.joinPreviousEditBlock()
1604 cursor.deletePreviousChar()
1615 cursor.deletePreviousChar()
1605
1616
1606 format = self._ansi_processor.get_format()
1617 format = self._ansi_processor.get_format()
1607 cursor.insertText(substring, format)
1618 cursor.insertText(substring, format)
1608 else:
1619 else:
1609 cursor.insertText(text)
1620 cursor.insertText(text)
1610 cursor.endEditBlock()
1621 cursor.endEditBlock()
1611
1622
1612 def _insert_plain_text_into_buffer(self, cursor, text):
1623 def _insert_plain_text_into_buffer(self, cursor, text):
1613 """ Inserts text into the input buffer using the specified cursor (which
1624 """ Inserts text into the input buffer using the specified cursor (which
1614 must be in the input buffer), ensuring that continuation prompts are
1625 must be in the input buffer), ensuring that continuation prompts are
1615 inserted as necessary.
1626 inserted as necessary.
1616 """
1627 """
1617 lines = unicode(text).splitlines(True)
1628 lines = text.splitlines(True)
1618 if lines:
1629 if lines:
1619 cursor.beginEditBlock()
1630 cursor.beginEditBlock()
1620 cursor.insertText(lines[0])
1631 cursor.insertText(lines[0])
1621 for line in lines[1:]:
1632 for line in lines[1:]:
1622 if self._continuation_prompt_html is None:
1633 if self._continuation_prompt_html is None:
1623 cursor.insertText(self._continuation_prompt)
1634 cursor.insertText(self._continuation_prompt)
1624 else:
1635 else:
1625 self._continuation_prompt = \
1636 self._continuation_prompt = \
1626 self._insert_html_fetching_plain_text(
1637 self._insert_html_fetching_plain_text(
1627 cursor, self._continuation_prompt_html)
1638 cursor, self._continuation_prompt_html)
1628 cursor.insertText(line)
1639 cursor.insertText(line)
1629 cursor.endEditBlock()
1640 cursor.endEditBlock()
1630
1641
1631 def _in_buffer(self, position=None):
1642 def _in_buffer(self, position=None):
1632 """ Returns whether the current cursor (or, if specified, a position) is
1643 """ Returns whether the current cursor (or, if specified, a position) is
1633 inside the editing region.
1644 inside the editing region.
1634 """
1645 """
1635 cursor = self._control.textCursor()
1646 cursor = self._control.textCursor()
1636 if position is None:
1647 if position is None:
1637 position = cursor.position()
1648 position = cursor.position()
1638 else:
1649 else:
1639 cursor.setPosition(position)
1650 cursor.setPosition(position)
1640 line = cursor.blockNumber()
1651 line = cursor.blockNumber()
1641 prompt_line = self._get_prompt_cursor().blockNumber()
1652 prompt_line = self._get_prompt_cursor().blockNumber()
1642 if line == prompt_line:
1653 if line == prompt_line:
1643 return position >= self._prompt_pos
1654 return position >= self._prompt_pos
1644 elif line > prompt_line:
1655 elif line > prompt_line:
1645 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1656 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1646 prompt_pos = cursor.position() + len(self._continuation_prompt)
1657 prompt_pos = cursor.position() + len(self._continuation_prompt)
1647 return position >= prompt_pos
1658 return position >= prompt_pos
1648 return False
1659 return False
1649
1660
1650 def _keep_cursor_in_buffer(self):
1661 def _keep_cursor_in_buffer(self):
1651 """ Ensures that the cursor is inside the editing region. Returns
1662 """ Ensures that the cursor is inside the editing region. Returns
1652 whether the cursor was moved.
1663 whether the cursor was moved.
1653 """
1664 """
1654 moved = not self._in_buffer()
1665 moved = not self._in_buffer()
1655 if moved:
1666 if moved:
1656 cursor = self._control.textCursor()
1667 cursor = self._control.textCursor()
1657 cursor.movePosition(QtGui.QTextCursor.End)
1668 cursor.movePosition(QtGui.QTextCursor.End)
1658 self._control.setTextCursor(cursor)
1669 self._control.setTextCursor(cursor)
1659 return moved
1670 return moved
1660
1671
1661 def _keyboard_quit(self):
1672 def _keyboard_quit(self):
1662 """ Cancels the current editing task ala Ctrl-G in Emacs.
1673 """ Cancels the current editing task ala Ctrl-G in Emacs.
1663 """
1674 """
1664 if self._text_completing_pos:
1675 if self._text_completing_pos:
1665 self._cancel_text_completion()
1676 self._cancel_text_completion()
1666 else:
1677 else:
1667 self.input_buffer = ''
1678 self.input_buffer = ''
1668
1679
1669 def _page(self, text, html=False):
1680 def _page(self, text, html=False):
1670 """ Displays text using the pager if it exceeds the height of the
1681 """ Displays text using the pager if it exceeds the height of the
1671 viewport.
1682 viewport.
1672
1683
1673 Parameters:
1684 Parameters:
1674 -----------
1685 -----------
1675 html : bool, optional (default False)
1686 html : bool, optional (default False)
1676 If set, the text will be interpreted as HTML instead of plain text.
1687 If set, the text will be interpreted as HTML instead of plain text.
1677 """
1688 """
1678 line_height = QtGui.QFontMetrics(self.font).height()
1689 line_height = QtGui.QFontMetrics(self.font).height()
1679 minlines = self._control.viewport().height() / line_height
1690 minlines = self._control.viewport().height() / line_height
1680 if self.paging != 'none' and \
1691 if self.paging != 'none' and \
1681 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1692 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1682 if self.paging == 'custom':
1693 if self.paging == 'custom':
1683 self.custom_page_requested.emit(text)
1694 self.custom_page_requested.emit(text)
1684 else:
1695 else:
1685 self._page_control.clear()
1696 self._page_control.clear()
1686 cursor = self._page_control.textCursor()
1697 cursor = self._page_control.textCursor()
1687 if html:
1698 if html:
1688 self._insert_html(cursor, text)
1699 self._insert_html(cursor, text)
1689 else:
1700 else:
1690 self._insert_plain_text(cursor, text)
1701 self._insert_plain_text(cursor, text)
1691 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1702 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1692
1703
1693 self._page_control.viewport().resize(self._control.size())
1704 self._page_control.viewport().resize(self._control.size())
1694 if self._splitter:
1705 if self._splitter:
1695 self._page_control.show()
1706 self._page_control.show()
1696 self._page_control.setFocus()
1707 self._page_control.setFocus()
1697 else:
1708 else:
1698 self.layout().setCurrentWidget(self._page_control)
1709 self.layout().setCurrentWidget(self._page_control)
1699 elif html:
1710 elif html:
1700 self._append_plain_html(text)
1711 self._append_plain_html(text)
1701 else:
1712 else:
1702 self._append_plain_text(text)
1713 self._append_plain_text(text)
1703
1714
1704 def _prompt_finished(self):
1715 def _prompt_finished(self):
1705 """ Called immediately after a prompt is finished, i.e. when some input
1716 """ Called immediately after a prompt is finished, i.e. when some input
1706 will be processed and a new prompt displayed.
1717 will be processed and a new prompt displayed.
1707 """
1718 """
1708 # Flush all state from the input splitter so the next round of
1719 # Flush all state from the input splitter so the next round of
1709 # reading input starts with a clean buffer.
1720 # reading input starts with a clean buffer.
1710 self._input_splitter.reset()
1721 self._input_splitter.reset()
1711
1722
1712 self._control.setReadOnly(True)
1723 self._control.setReadOnly(True)
1713 self._prompt_finished_hook()
1724 self._prompt_finished_hook()
1714
1725
1715 def _prompt_started(self):
1726 def _prompt_started(self):
1716 """ Called immediately after a new prompt is displayed.
1727 """ Called immediately after a new prompt is displayed.
1717 """
1728 """
1718 # Temporarily disable the maximum block count to permit undo/redo and
1729 # Temporarily disable the maximum block count to permit undo/redo and
1719 # to ensure that the prompt position does not change due to truncation.
1730 # to ensure that the prompt position does not change due to truncation.
1720 self._control.document().setMaximumBlockCount(0)
1731 self._control.document().setMaximumBlockCount(0)
1721 self._control.setUndoRedoEnabled(True)
1732 self._control.setUndoRedoEnabled(True)
1722
1733
1723 self._control.setReadOnly(False)
1734 self._control.setReadOnly(False)
1724 self._control.moveCursor(QtGui.QTextCursor.End)
1735 self._control.moveCursor(QtGui.QTextCursor.End)
1725 self._executing = False
1736 self._executing = False
1726 self._prompt_started_hook()
1737 self._prompt_started_hook()
1727
1738
1728 def _readline(self, prompt='', callback=None):
1739 def _readline(self, prompt='', callback=None):
1729 """ Reads one line of input from the user.
1740 """ Reads one line of input from the user.
1730
1741
1731 Parameters
1742 Parameters
1732 ----------
1743 ----------
1733 prompt : str, optional
1744 prompt : str, optional
1734 The prompt to print before reading the line.
1745 The prompt to print before reading the line.
1735
1746
1736 callback : callable, optional
1747 callback : callable, optional
1737 A callback to execute with the read line. If not specified, input is
1748 A callback to execute with the read line. If not specified, input is
1738 read *synchronously* and this method does not return until it has
1749 read *synchronously* and this method does not return until it has
1739 been read.
1750 been read.
1740
1751
1741 Returns
1752 Returns
1742 -------
1753 -------
1743 If a callback is specified, returns nothing. Otherwise, returns the
1754 If a callback is specified, returns nothing. Otherwise, returns the
1744 input string with the trailing newline stripped.
1755 input string with the trailing newline stripped.
1745 """
1756 """
1746 if self._reading:
1757 if self._reading:
1747 raise RuntimeError('Cannot read a line. Widget is already reading.')
1758 raise RuntimeError('Cannot read a line. Widget is already reading.')
1748
1759
1749 if not callback and not self.isVisible():
1760 if not callback and not self.isVisible():
1750 # If the user cannot see the widget, this function cannot return.
1761 # If the user cannot see the widget, this function cannot return.
1751 raise RuntimeError('Cannot synchronously read a line if the widget '
1762 raise RuntimeError('Cannot synchronously read a line if the widget '
1752 'is not visible!')
1763 'is not visible!')
1753
1764
1754 self._reading = True
1765 self._reading = True
1755 self._show_prompt(prompt, newline=False)
1766 self._show_prompt(prompt, newline=False)
1756
1767
1757 if callback is None:
1768 if callback is None:
1758 self._reading_callback = None
1769 self._reading_callback = None
1759 while self._reading:
1770 while self._reading:
1760 QtCore.QCoreApplication.processEvents()
1771 QtCore.QCoreApplication.processEvents()
1761 return self.input_buffer.rstrip('\n')
1772 return self.input_buffer.rstrip('\n')
1762
1773
1763 else:
1774 else:
1764 self._reading_callback = lambda: \
1775 self._reading_callback = lambda: \
1765 callback(self.input_buffer.rstrip('\n'))
1776 callback(self.input_buffer.rstrip('\n'))
1766
1777
1767 def _set_continuation_prompt(self, prompt, html=False):
1778 def _set_continuation_prompt(self, prompt, html=False):
1768 """ Sets the continuation prompt.
1779 """ Sets the continuation prompt.
1769
1780
1770 Parameters
1781 Parameters
1771 ----------
1782 ----------
1772 prompt : str
1783 prompt : str
1773 The prompt to show when more input is needed.
1784 The prompt to show when more input is needed.
1774
1785
1775 html : bool, optional (default False)
1786 html : bool, optional (default False)
1776 If set, the prompt will be inserted as formatted HTML. Otherwise,
1787 If set, the prompt will be inserted as formatted HTML. Otherwise,
1777 the prompt will be treated as plain text, though ANSI color codes
1788 the prompt will be treated as plain text, though ANSI color codes
1778 will be handled.
1789 will be handled.
1779 """
1790 """
1780 if html:
1791 if html:
1781 self._continuation_prompt_html = prompt
1792 self._continuation_prompt_html = prompt
1782 else:
1793 else:
1783 self._continuation_prompt = prompt
1794 self._continuation_prompt = prompt
1784 self._continuation_prompt_html = None
1795 self._continuation_prompt_html = None
1785
1796
1786 def _set_cursor(self, cursor):
1797 def _set_cursor(self, cursor):
1787 """ Convenience method to set the current cursor.
1798 """ Convenience method to set the current cursor.
1788 """
1799 """
1789 self._control.setTextCursor(cursor)
1800 self._control.setTextCursor(cursor)
1790
1801
1791 def _set_top_cursor(self, cursor):
1802 def _set_top_cursor(self, cursor):
1792 """ Scrolls the viewport so that the specified cursor is at the top.
1803 """ Scrolls the viewport so that the specified cursor is at the top.
1793 """
1804 """
1794 scrollbar = self._control.verticalScrollBar()
1805 scrollbar = self._control.verticalScrollBar()
1795 scrollbar.setValue(scrollbar.maximum())
1806 scrollbar.setValue(scrollbar.maximum())
1796 original_cursor = self._control.textCursor()
1807 original_cursor = self._control.textCursor()
1797 self._control.setTextCursor(cursor)
1808 self._control.setTextCursor(cursor)
1798 self._control.ensureCursorVisible()
1809 self._control.ensureCursorVisible()
1799 self._control.setTextCursor(original_cursor)
1810 self._control.setTextCursor(original_cursor)
1800
1811
1801 def _show_prompt(self, prompt=None, html=False, newline=True):
1812 def _show_prompt(self, prompt=None, html=False, newline=True):
1802 """ Writes a new prompt at the end of the buffer.
1813 """ Writes a new prompt at the end of the buffer.
1803
1814
1804 Parameters
1815 Parameters
1805 ----------
1816 ----------
1806 prompt : str, optional
1817 prompt : str, optional
1807 The prompt to show. If not specified, the previous prompt is used.
1818 The prompt to show. If not specified, the previous prompt is used.
1808
1819
1809 html : bool, optional (default False)
1820 html : bool, optional (default False)
1810 Only relevant when a prompt is specified. If set, the prompt will
1821 Only relevant when a prompt is specified. If set, the prompt will
1811 be inserted as formatted HTML. Otherwise, the prompt will be treated
1822 be inserted as formatted HTML. Otherwise, the prompt will be treated
1812 as plain text, though ANSI color codes will be handled.
1823 as plain text, though ANSI color codes will be handled.
1813
1824
1814 newline : bool, optional (default True)
1825 newline : bool, optional (default True)
1815 If set, a new line will be written before showing the prompt if
1826 If set, a new line will be written before showing the prompt if
1816 there is not already a newline at the end of the buffer.
1827 there is not already a newline at the end of the buffer.
1817 """
1828 """
1818 # Insert a preliminary newline, if necessary.
1829 # Insert a preliminary newline, if necessary.
1819 if newline:
1830 if newline:
1820 cursor = self._get_end_cursor()
1831 cursor = self._get_end_cursor()
1821 if cursor.position() > 0:
1832 if cursor.position() > 0:
1822 cursor.movePosition(QtGui.QTextCursor.Left,
1833 cursor.movePosition(QtGui.QTextCursor.Left,
1823 QtGui.QTextCursor.KeepAnchor)
1834 QtGui.QTextCursor.KeepAnchor)
1824 if unicode(cursor.selection().toPlainText()) != '\n':
1835 if cursor.selection().toPlainText() != '\n':
1825 self._append_plain_text('\n')
1836 self._append_plain_text('\n')
1826
1837
1827 # Write the prompt.
1838 # Write the prompt.
1828 self._append_plain_text(self._prompt_sep)
1839 self._append_plain_text(self._prompt_sep)
1829 if prompt is None:
1840 if prompt is None:
1830 if self._prompt_html is None:
1841 if self._prompt_html is None:
1831 self._append_plain_text(self._prompt)
1842 self._append_plain_text(self._prompt)
1832 else:
1843 else:
1833 self._append_html(self._prompt_html)
1844 self._append_html(self._prompt_html)
1834 else:
1845 else:
1835 if html:
1846 if html:
1836 self._prompt = self._append_html_fetching_plain_text(prompt)
1847 self._prompt = self._append_html_fetching_plain_text(prompt)
1837 self._prompt_html = prompt
1848 self._prompt_html = prompt
1838 else:
1849 else:
1839 self._append_plain_text(prompt)
1850 self._append_plain_text(prompt)
1840 self._prompt = prompt
1851 self._prompt = prompt
1841 self._prompt_html = None
1852 self._prompt_html = None
1842
1853
1843 self._prompt_pos = self._get_end_cursor().position()
1854 self._prompt_pos = self._get_end_cursor().position()
1844 self._prompt_started()
1855 self._prompt_started()
1845
1856
1846 #------ Signal handlers ----------------------------------------------------
1857 #------ Signal handlers ----------------------------------------------------
1847
1858
1848 def _adjust_scrollbars(self):
1859 def _adjust_scrollbars(self):
1849 """ Expands the vertical scrollbar beyond the range set by Qt.
1860 """ Expands the vertical scrollbar beyond the range set by Qt.
1850 """
1861 """
1851 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1862 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1852 # and qtextedit.cpp.
1863 # and qtextedit.cpp.
1853 document = self._control.document()
1864 document = self._control.document()
1854 scrollbar = self._control.verticalScrollBar()
1865 scrollbar = self._control.verticalScrollBar()
1855 viewport_height = self._control.viewport().height()
1866 viewport_height = self._control.viewport().height()
1856 if isinstance(self._control, QtGui.QPlainTextEdit):
1867 if isinstance(self._control, QtGui.QPlainTextEdit):
1857 maximum = max(0, document.lineCount() - 1)
1868 maximum = max(0, document.lineCount() - 1)
1858 step = viewport_height / self._control.fontMetrics().lineSpacing()
1869 step = viewport_height / self._control.fontMetrics().lineSpacing()
1859 else:
1870 else:
1860 # QTextEdit does not do line-based layout and blocks will not in
1871 # QTextEdit does not do line-based layout and blocks will not in
1861 # general have the same height. Therefore it does not make sense to
1872 # general have the same height. Therefore it does not make sense to
1862 # attempt to scroll in line height increments.
1873 # attempt to scroll in line height increments.
1863 maximum = document.size().height()
1874 maximum = document.size().height()
1864 step = viewport_height
1875 step = viewport_height
1865 diff = maximum - scrollbar.maximum()
1876 diff = maximum - scrollbar.maximum()
1866 scrollbar.setRange(0, maximum)
1877 scrollbar.setRange(0, maximum)
1867 scrollbar.setPageStep(step)
1878 scrollbar.setPageStep(step)
1868 # Compensate for undesirable scrolling that occurs automatically due to
1879 # Compensate for undesirable scrolling that occurs automatically due to
1869 # maximumBlockCount() text truncation.
1880 # maximumBlockCount() text truncation.
1870 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1881 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1871 scrollbar.setValue(scrollbar.value() + diff)
1882 scrollbar.setValue(scrollbar.value() + diff)
1872
1883
1873 def _cursor_position_changed(self):
1884 def _cursor_position_changed(self):
1874 """ Clears the temporary buffer based on the cursor position.
1885 """ Clears the temporary buffer based on the cursor position.
1875 """
1886 """
1876 if self._text_completing_pos:
1887 if self._text_completing_pos:
1877 document = self._control.document()
1888 document = self._control.document()
1878 if self._text_completing_pos < document.characterCount():
1889 if self._text_completing_pos < document.characterCount():
1879 cursor = self._control.textCursor()
1890 cursor = self._control.textCursor()
1880 pos = cursor.position()
1891 pos = cursor.position()
1881 text_cursor = self._control.textCursor()
1892 text_cursor = self._control.textCursor()
1882 text_cursor.setPosition(self._text_completing_pos)
1893 text_cursor.setPosition(self._text_completing_pos)
1883 if pos < self._text_completing_pos or \
1894 if pos < self._text_completing_pos or \
1884 cursor.blockNumber() > text_cursor.blockNumber():
1895 cursor.blockNumber() > text_cursor.blockNumber():
1885 self._clear_temporary_buffer()
1896 self._clear_temporary_buffer()
1886 self._text_completing_pos = 0
1897 self._text_completing_pos = 0
1887 else:
1898 else:
1888 self._clear_temporary_buffer()
1899 self._clear_temporary_buffer()
1889 self._text_completing_pos = 0
1900 self._text_completing_pos = 0
1890
1901
1891 def _custom_context_menu_requested(self, pos):
1902 def _custom_context_menu_requested(self, pos):
1892 """ Shows a context menu at the given QPoint (in widget coordinates).
1903 """ Shows a context menu at the given QPoint (in widget coordinates).
1893 """
1904 """
1894 menu = self._context_menu_make(pos)
1905 menu = self._context_menu_make(pos)
1895 menu.exec_(self._control.mapToGlobal(pos))
1906 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,598 +1,598 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from PyQt4 import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11
11
12 # Local imports
12 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
14 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool
16 from IPython.utils.traitlets import Bool
17 from bracket_matcher import BracketMatcher
17 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
19 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
20 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
21 from pygments_highlighter import PygmentsHighlighter
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 prompts.
26 prompts.
27 """
27 """
28
28
29 def __init__(self, frontend):
29 def __init__(self, frontend):
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 self._current_offset = 0
31 self._current_offset = 0
32 self._frontend = frontend
32 self._frontend = frontend
33 self.highlighting_on = False
33 self.highlighting_on = False
34
34
35 def highlightBlock(self, qstring):
35 def highlightBlock(self, string):
36 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """
37 """
38 if not self.highlighting_on:
38 if not self.highlighting_on:
39 return
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 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # the string as plain text so we can compare it.
43 # the string as plain text so we can compare it.
44 current_block = self.currentBlock()
44 current_block = self.currentBlock()
45 string = self._frontend._get_block_plain_text(current_block)
45 string = self._frontend._get_block_plain_text(current_block)
46
46
47 # Decide whether to check for the regular or continuation prompt.
47 # Decide whether to check for the regular or continuation prompt.
48 if current_block.contains(self._frontend._prompt_pos):
48 if current_block.contains(self._frontend._prompt_pos):
49 prompt = self._frontend._prompt
49 prompt = self._frontend._prompt
50 else:
50 else:
51 prompt = self._frontend._continuation_prompt
51 prompt = self._frontend._continuation_prompt
52
52
53 # Don't highlight the part of the string that contains the prompt.
53 # Don't highlight the part of the string that contains the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 qstring.remove(0, len(prompt))
56 string = string[len(prompt):]
57 else:
57 else:
58 self._current_offset = 0
58 self._current_offset = 0
59
59
60 PygmentsHighlighter.highlightBlock(self, qstring)
60 PygmentsHighlighter.highlightBlock(self, string)
61
61
62 def rehighlightBlock(self, block):
62 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
63 """ Reimplemented to temporarily enable highlighting if disabled.
64 """
64 """
65 old = self.highlighting_on
65 old = self.highlighting_on
66 self.highlighting_on = True
66 self.highlighting_on = True
67 super(FrontendHighlighter, self).rehighlightBlock(block)
67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 self.highlighting_on = old
68 self.highlighting_on = old
69
69
70 def setFormat(self, start, count, format):
70 def setFormat(self, start, count, format):
71 """ Reimplemented to highlight selectively.
71 """ Reimplemented to highlight selectively.
72 """
72 """
73 start += self._current_offset
73 start += self._current_offset
74 PygmentsHighlighter.setFormat(self, start, count, format)
74 PygmentsHighlighter.setFormat(self, start, count, format)
75
75
76
76
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 """ A Qt frontend for a generic Python kernel.
78 """ A Qt frontend for a generic Python kernel.
79 """
79 """
80
80
81 # An option and corresponding signal for overriding the default kernel
81 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
82 # interrupt behavior.
83 custom_interrupt = Bool(False)
83 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.pyqtSignal()
84 custom_interrupt_requested = QtCore.Signal()
85
85
86 # An option and corresponding signals for overriding the default kernel
86 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
87 # restart behavior.
88 custom_restart = Bool(False)
88 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.pyqtSignal(float)
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.pyqtSignal()
90 custom_restart_requested = QtCore.Signal()
91
91
92 # Emitted when an 'execute_reply' has been received from the kernel and
92 # Emitted when an 'execute_reply' has been received from the kernel and
93 # processed by the FrontendWidget.
93 # processed by the FrontendWidget.
94 executed = QtCore.pyqtSignal(object)
94 executed = QtCore.Signal(object)
95
95
96 # Emitted when an exit request has been received from the kernel.
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 # Protected class variables.
99 # Protected class variables.
100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 _input_splitter_class = InputSplitter
103 _input_splitter_class = InputSplitter
104 _local_kernel = False
104 _local_kernel = False
105
105
106 #---------------------------------------------------------------------------
106 #---------------------------------------------------------------------------
107 # 'object' interface
107 # 'object' interface
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109
109
110 def __init__(self, *args, **kw):
110 def __init__(self, *args, **kw):
111 super(FrontendWidget, self).__init__(*args, **kw)
111 super(FrontendWidget, self).__init__(*args, **kw)
112
112
113 # FrontendWidget protected variables.
113 # FrontendWidget protected variables.
114 self._bracket_matcher = BracketMatcher(self._control)
114 self._bracket_matcher = BracketMatcher(self._control)
115 self._call_tip_widget = CallTipWidget(self._control)
115 self._call_tip_widget = CallTipWidget(self._control)
116 self._completion_lexer = CompletionLexer(PythonLexer())
116 self._completion_lexer = CompletionLexer(PythonLexer())
117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
118 self._hidden = False
118 self._hidden = False
119 self._highlighter = FrontendHighlighter(self)
119 self._highlighter = FrontendHighlighter(self)
120 self._input_splitter = self._input_splitter_class(input_mode='cell')
120 self._input_splitter = self._input_splitter_class(input_mode='cell')
121 self._kernel_manager = None
121 self._kernel_manager = None
122 self._request_info = {}
122 self._request_info = {}
123
123
124 # Configure the ConsoleWidget.
124 # Configure the ConsoleWidget.
125 self.tab_width = 4
125 self.tab_width = 4
126 self._set_continuation_prompt('... ')
126 self._set_continuation_prompt('... ')
127
127
128 # Configure the CallTipWidget.
128 # Configure the CallTipWidget.
129 self._call_tip_widget.setFont(self.font)
129 self._call_tip_widget.setFont(self.font)
130 self.font_changed.connect(self._call_tip_widget.setFont)
130 self.font_changed.connect(self._call_tip_widget.setFont)
131
131
132 # Configure actions.
132 # Configure actions.
133 action = self._copy_raw_action
133 action = self._copy_raw_action
134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
135 action.setEnabled(False)
135 action.setEnabled(False)
136 action.setShortcut(QtGui.QKeySequence(key))
136 action.setShortcut(QtGui.QKeySequence(key))
137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
138 action.triggered.connect(self.copy_raw)
138 action.triggered.connect(self.copy_raw)
139 self.copy_available.connect(action.setEnabled)
139 self.copy_available.connect(action.setEnabled)
140 self.addAction(action)
140 self.addAction(action)
141
141
142 # Connect signal handlers.
142 # Connect signal handlers.
143 document = self._control.document()
143 document = self._control.document()
144 document.contentsChange.connect(self._document_contents_change)
144 document.contentsChange.connect(self._document_contents_change)
145
145
146 # set flag for whether we are connected via localhost
146 # set flag for whether we are connected via localhost
147 self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel)
147 self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel)
148
148
149 #---------------------------------------------------------------------------
149 #---------------------------------------------------------------------------
150 # 'ConsoleWidget' public interface
150 # 'ConsoleWidget' public interface
151 #---------------------------------------------------------------------------
151 #---------------------------------------------------------------------------
152
152
153 def copy(self):
153 def copy(self):
154 """ Copy the currently selected text to the clipboard, removing prompts.
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 if text:
157 if text:
158 lines = map(transform_classic_prompt, text.splitlines())
158 lines = map(transform_classic_prompt, text.splitlines())
159 text = '\n'.join(lines)
159 text = '\n'.join(lines)
160 QtGui.QApplication.clipboard().setText(text)
160 QtGui.QApplication.clipboard().setText(text)
161
161
162 #---------------------------------------------------------------------------
162 #---------------------------------------------------------------------------
163 # 'ConsoleWidget' abstract interface
163 # 'ConsoleWidget' abstract interface
164 #---------------------------------------------------------------------------
164 #---------------------------------------------------------------------------
165
165
166 def _is_complete(self, source, interactive):
166 def _is_complete(self, source, interactive):
167 """ Returns whether 'source' can be completely processed and a new
167 """ Returns whether 'source' can be completely processed and a new
168 prompt created. When triggered by an Enter/Return key press,
168 prompt created. When triggered by an Enter/Return key press,
169 'interactive' is True; otherwise, it is False.
169 'interactive' is True; otherwise, it is False.
170 """
170 """
171 complete = self._input_splitter.push(source)
171 complete = self._input_splitter.push(source)
172 if interactive:
172 if interactive:
173 complete = not self._input_splitter.push_accepts_more()
173 complete = not self._input_splitter.push_accepts_more()
174 return complete
174 return complete
175
175
176 def _execute(self, source, hidden):
176 def _execute(self, source, hidden):
177 """ Execute 'source'. If 'hidden', do not show any output.
177 """ Execute 'source'. If 'hidden', do not show any output.
178
178
179 See parent class :meth:`execute` docstring for full details.
179 See parent class :meth:`execute` docstring for full details.
180 """
180 """
181 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
181 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
182 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
182 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
183 self._hidden = hidden
183 self._hidden = hidden
184
184
185 def _prompt_started_hook(self):
185 def _prompt_started_hook(self):
186 """ Called immediately after a new prompt is displayed.
186 """ Called immediately after a new prompt is displayed.
187 """
187 """
188 if not self._reading:
188 if not self._reading:
189 self._highlighter.highlighting_on = True
189 self._highlighter.highlighting_on = True
190
190
191 def _prompt_finished_hook(self):
191 def _prompt_finished_hook(self):
192 """ Called immediately after a prompt is finished, i.e. when some input
192 """ Called immediately after a prompt is finished, i.e. when some input
193 will be processed and a new prompt displayed.
193 will be processed and a new prompt displayed.
194 """
194 """
195 if not self._reading:
195 if not self._reading:
196 self._highlighter.highlighting_on = False
196 self._highlighter.highlighting_on = False
197
197
198 def _tab_pressed(self):
198 def _tab_pressed(self):
199 """ Called when the tab key is pressed. Returns whether to continue
199 """ Called when the tab key is pressed. Returns whether to continue
200 processing the event.
200 processing the event.
201 """
201 """
202 # Perform tab completion if:
202 # Perform tab completion if:
203 # 1) The cursor is in the input buffer.
203 # 1) The cursor is in the input buffer.
204 # 2) There is a non-whitespace character before the cursor.
204 # 2) There is a non-whitespace character before the cursor.
205 text = self._get_input_buffer_cursor_line()
205 text = self._get_input_buffer_cursor_line()
206 if text is None:
206 if text is None:
207 return False
207 return False
208 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
208 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
209 if complete:
209 if complete:
210 self._complete()
210 self._complete()
211 return not complete
211 return not complete
212
212
213 #---------------------------------------------------------------------------
213 #---------------------------------------------------------------------------
214 # 'ConsoleWidget' protected interface
214 # 'ConsoleWidget' protected interface
215 #---------------------------------------------------------------------------
215 #---------------------------------------------------------------------------
216
216
217 def _context_menu_make(self, pos):
217 def _context_menu_make(self, pos):
218 """ Reimplemented to add an action for raw copy.
218 """ Reimplemented to add an action for raw copy.
219 """
219 """
220 menu = super(FrontendWidget, self)._context_menu_make(pos)
220 menu = super(FrontendWidget, self)._context_menu_make(pos)
221 for before_action in menu.actions():
221 for before_action in menu.actions():
222 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
222 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
223 QtGui.QKeySequence.ExactMatch:
223 QtGui.QKeySequence.ExactMatch:
224 menu.insertAction(before_action, self._copy_raw_action)
224 menu.insertAction(before_action, self._copy_raw_action)
225 break
225 break
226 return menu
226 return menu
227
227
228 def _event_filter_console_keypress(self, event):
228 def _event_filter_console_keypress(self, event):
229 """ Reimplemented for execution interruption and smart backspace.
229 """ Reimplemented for execution interruption and smart backspace.
230 """
230 """
231 key = event.key()
231 key = event.key()
232 if self._control_key_down(event.modifiers(), include_command=False):
232 if self._control_key_down(event.modifiers(), include_command=False):
233
233
234 if key == QtCore.Qt.Key_C and self._executing:
234 if key == QtCore.Qt.Key_C and self._executing:
235 self.interrupt_kernel()
235 self.interrupt_kernel()
236 return True
236 return True
237
237
238 elif key == QtCore.Qt.Key_Period:
238 elif key == QtCore.Qt.Key_Period:
239 message = 'Are you sure you want to restart the kernel?'
239 message = 'Are you sure you want to restart the kernel?'
240 self.restart_kernel(message, now=False)
240 self.restart_kernel(message, now=False)
241 return True
241 return True
242
242
243 elif not event.modifiers() & QtCore.Qt.AltModifier:
243 elif not event.modifiers() & QtCore.Qt.AltModifier:
244
244
245 # Smart backspace: remove four characters in one backspace if:
245 # Smart backspace: remove four characters in one backspace if:
246 # 1) everything left of the cursor is whitespace
246 # 1) everything left of the cursor is whitespace
247 # 2) the four characters immediately left of the cursor are spaces
247 # 2) the four characters immediately left of the cursor are spaces
248 if key == QtCore.Qt.Key_Backspace:
248 if key == QtCore.Qt.Key_Backspace:
249 col = self._get_input_buffer_cursor_column()
249 col = self._get_input_buffer_cursor_column()
250 cursor = self._control.textCursor()
250 cursor = self._control.textCursor()
251 if col > 3 and not cursor.hasSelection():
251 if col > 3 and not cursor.hasSelection():
252 text = self._get_input_buffer_cursor_line()[:col]
252 text = self._get_input_buffer_cursor_line()[:col]
253 if text.endswith(' ') and not text.strip():
253 if text.endswith(' ') and not text.strip():
254 cursor.movePosition(QtGui.QTextCursor.Left,
254 cursor.movePosition(QtGui.QTextCursor.Left,
255 QtGui.QTextCursor.KeepAnchor, 4)
255 QtGui.QTextCursor.KeepAnchor, 4)
256 cursor.removeSelectedText()
256 cursor.removeSelectedText()
257 return True
257 return True
258
258
259 return super(FrontendWidget, self)._event_filter_console_keypress(event)
259 return super(FrontendWidget, self)._event_filter_console_keypress(event)
260
260
261 def _insert_continuation_prompt(self, cursor):
261 def _insert_continuation_prompt(self, cursor):
262 """ Reimplemented for auto-indentation.
262 """ Reimplemented for auto-indentation.
263 """
263 """
264 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
264 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
265 cursor.insertText(' ' * self._input_splitter.indent_spaces)
265 cursor.insertText(' ' * self._input_splitter.indent_spaces)
266
266
267 #---------------------------------------------------------------------------
267 #---------------------------------------------------------------------------
268 # 'BaseFrontendMixin' abstract interface
268 # 'BaseFrontendMixin' abstract interface
269 #---------------------------------------------------------------------------
269 #---------------------------------------------------------------------------
270
270
271 def _handle_complete_reply(self, rep):
271 def _handle_complete_reply(self, rep):
272 """ Handle replies for tab completion.
272 """ Handle replies for tab completion.
273 """
273 """
274 cursor = self._get_cursor()
274 cursor = self._get_cursor()
275 info = self._request_info.get('complete')
275 info = self._request_info.get('complete')
276 if info and info.id == rep['parent_header']['msg_id'] and \
276 if info and info.id == rep['parent_header']['msg_id'] and \
277 info.pos == cursor.position():
277 info.pos == cursor.position():
278 text = '.'.join(self._get_context())
278 text = '.'.join(self._get_context())
279 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
279 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
280 self._complete_with_items(cursor, rep['content']['matches'])
280 self._complete_with_items(cursor, rep['content']['matches'])
281
281
282 def _handle_execute_reply(self, msg):
282 def _handle_execute_reply(self, msg):
283 """ Handles replies for code execution.
283 """ Handles replies for code execution.
284 """
284 """
285 info = self._request_info.get('execute')
285 info = self._request_info.get('execute')
286 if info and info.id == msg['parent_header']['msg_id'] and \
286 if info and info.id == msg['parent_header']['msg_id'] and \
287 info.kind == 'user' and not self._hidden:
287 info.kind == 'user' and not self._hidden:
288 # Make sure that all output from the SUB channel has been processed
288 # Make sure that all output from the SUB channel has been processed
289 # before writing a new prompt.
289 # before writing a new prompt.
290 self.kernel_manager.sub_channel.flush()
290 self.kernel_manager.sub_channel.flush()
291
291
292 # Reset the ANSI style information to prevent bad text in stdout
292 # Reset the ANSI style information to prevent bad text in stdout
293 # from messing up our colors. We're not a true terminal so we're
293 # from messing up our colors. We're not a true terminal so we're
294 # allowed to do this.
294 # allowed to do this.
295 if self.ansi_codes:
295 if self.ansi_codes:
296 self._ansi_processor.reset_sgr()
296 self._ansi_processor.reset_sgr()
297
297
298 content = msg['content']
298 content = msg['content']
299 status = content['status']
299 status = content['status']
300 if status == 'ok':
300 if status == 'ok':
301 self._process_execute_ok(msg)
301 self._process_execute_ok(msg)
302 elif status == 'error':
302 elif status == 'error':
303 self._process_execute_error(msg)
303 self._process_execute_error(msg)
304 elif status == 'abort':
304 elif status == 'abort':
305 self._process_execute_abort(msg)
305 self._process_execute_abort(msg)
306
306
307 self._show_interpreter_prompt_for_reply(msg)
307 self._show_interpreter_prompt_for_reply(msg)
308 self.executed.emit(msg)
308 self.executed.emit(msg)
309
309
310 def _handle_input_request(self, msg):
310 def _handle_input_request(self, msg):
311 """ Handle requests for raw_input.
311 """ Handle requests for raw_input.
312 """
312 """
313 if self._hidden:
313 if self._hidden:
314 raise RuntimeError('Request for raw input during hidden execution.')
314 raise RuntimeError('Request for raw input during hidden execution.')
315
315
316 # Make sure that all output from the SUB channel has been processed
316 # Make sure that all output from the SUB channel has been processed
317 # before entering readline mode.
317 # before entering readline mode.
318 self.kernel_manager.sub_channel.flush()
318 self.kernel_manager.sub_channel.flush()
319
319
320 def callback(line):
320 def callback(line):
321 self.kernel_manager.rep_channel.input(line)
321 self.kernel_manager.rep_channel.input(line)
322 self._readline(msg['content']['prompt'], callback=callback)
322 self._readline(msg['content']['prompt'], callback=callback)
323
323
324 def _handle_kernel_died(self, since_last_heartbeat):
324 def _handle_kernel_died(self, since_last_heartbeat):
325 """ Handle the kernel's death by asking if the user wants to restart.
325 """ Handle the kernel's death by asking if the user wants to restart.
326 """
326 """
327 if self.custom_restart:
327 if self.custom_restart:
328 self.custom_restart_kernel_died.emit(since_last_heartbeat)
328 self.custom_restart_kernel_died.emit(since_last_heartbeat)
329 else:
329 else:
330 message = 'The kernel heartbeat has been inactive for %.2f ' \
330 message = 'The kernel heartbeat has been inactive for %.2f ' \
331 'seconds. Do you want to restart the kernel? You may ' \
331 'seconds. Do you want to restart the kernel? You may ' \
332 'first want to check the network connection.' % \
332 'first want to check the network connection.' % \
333 since_last_heartbeat
333 since_last_heartbeat
334 self.restart_kernel(message, now=True)
334 self.restart_kernel(message, now=True)
335
335
336 def _handle_object_info_reply(self, rep):
336 def _handle_object_info_reply(self, rep):
337 """ Handle replies for call tips.
337 """ Handle replies for call tips.
338 """
338 """
339 cursor = self._get_cursor()
339 cursor = self._get_cursor()
340 info = self._request_info.get('call_tip')
340 info = self._request_info.get('call_tip')
341 if info and info.id == rep['parent_header']['msg_id'] and \
341 if info and info.id == rep['parent_header']['msg_id'] and \
342 info.pos == cursor.position():
342 info.pos == cursor.position():
343 # Get the information for a call tip. For now we format the call
343 # Get the information for a call tip. For now we format the call
344 # line as string, later we can pass False to format_call and
344 # line as string, later we can pass False to format_call and
345 # syntax-highlight it ourselves for nicer formatting in the
345 # syntax-highlight it ourselves for nicer formatting in the
346 # calltip.
346 # calltip.
347 call_info, doc = call_tip(rep['content'], format_call=True)
347 call_info, doc = call_tip(rep['content'], format_call=True)
348 if call_info or doc:
348 if call_info or doc:
349 self._call_tip_widget.show_call_info(call_info, doc)
349 self._call_tip_widget.show_call_info(call_info, doc)
350
350
351 def _handle_pyout(self, msg):
351 def _handle_pyout(self, msg):
352 """ Handle display hook output.
352 """ Handle display hook output.
353 """
353 """
354 if not self._hidden and self._is_from_this_session(msg):
354 if not self._hidden and self._is_from_this_session(msg):
355 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
355 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
356
356
357 def _handle_stream(self, msg):
357 def _handle_stream(self, msg):
358 """ Handle stdout, stderr, and stdin.
358 """ Handle stdout, stderr, and stdin.
359 """
359 """
360 if not self._hidden and self._is_from_this_session(msg):
360 if not self._hidden and self._is_from_this_session(msg):
361 # Most consoles treat tabs as being 8 space characters. Convert tabs
361 # Most consoles treat tabs as being 8 space characters. Convert tabs
362 # to spaces so that output looks as expected regardless of this
362 # to spaces so that output looks as expected regardless of this
363 # widget's tab width.
363 # widget's tab width.
364 text = msg['content']['data'].expandtabs(8)
364 text = msg['content']['data'].expandtabs(8)
365
365
366 self._append_plain_text(text)
366 self._append_plain_text(text)
367 self._control.moveCursor(QtGui.QTextCursor.End)
367 self._control.moveCursor(QtGui.QTextCursor.End)
368
368
369 def _handle_shutdown_reply(self, msg):
369 def _handle_shutdown_reply(self, msg):
370 """ Handle shutdown signal, only if from other console.
370 """ Handle shutdown signal, only if from other console.
371 """
371 """
372 if not self._hidden and not self._is_from_this_session(msg):
372 if not self._hidden and not self._is_from_this_session(msg):
373 if self._local_kernel:
373 if self._local_kernel:
374 if not msg['content']['restart']:
374 if not msg['content']['restart']:
375 sys.exit(0)
375 sys.exit(0)
376 else:
376 else:
377 # we just got notified of a restart!
377 # we just got notified of a restart!
378 time.sleep(0.25) # wait 1/4 sec to reset
378 time.sleep(0.25) # wait 1/4 sec to reset
379 # lest the request for a new prompt
379 # lest the request for a new prompt
380 # goes to the old kernel
380 # goes to the old kernel
381 self.reset()
381 self.reset()
382 else: # remote kernel, prompt on Kernel shutdown/reset
382 else: # remote kernel, prompt on Kernel shutdown/reset
383 title = self.window().windowTitle()
383 title = self.window().windowTitle()
384 if not msg['content']['restart']:
384 if not msg['content']['restart']:
385 reply = QtGui.QMessageBox.question(self, title,
385 reply = QtGui.QMessageBox.question(self, title,
386 "Kernel has been shutdown permanently. Close the Console?",
386 "Kernel has been shutdown permanently. Close the Console?",
387 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
387 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
388 if reply == QtGui.QMessageBox.Yes:
388 if reply == QtGui.QMessageBox.Yes:
389 sys.exit(0)
389 sys.exit(0)
390 else:
390 else:
391 reply = QtGui.QMessageBox.question(self, title,
391 reply = QtGui.QMessageBox.question(self, title,
392 "Kernel has been reset. Clear the Console?",
392 "Kernel has been reset. Clear the Console?",
393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
394 if reply == QtGui.QMessageBox.Yes:
394 if reply == QtGui.QMessageBox.Yes:
395 time.sleep(0.25) # wait 1/4 sec to reset
395 time.sleep(0.25) # wait 1/4 sec to reset
396 # lest the request for a new prompt
396 # lest the request for a new prompt
397 # goes to the old kernel
397 # goes to the old kernel
398 self.reset()
398 self.reset()
399
399
400 def _started_channels(self):
400 def _started_channels(self):
401 """ Called when the KernelManager channels have started listening or
401 """ Called when the KernelManager channels have started listening or
402 when the frontend is assigned an already listening KernelManager.
402 when the frontend is assigned an already listening KernelManager.
403 """
403 """
404 self.reset()
404 self.reset()
405
405
406 #---------------------------------------------------------------------------
406 #---------------------------------------------------------------------------
407 # 'FrontendWidget' public interface
407 # 'FrontendWidget' public interface
408 #---------------------------------------------------------------------------
408 #---------------------------------------------------------------------------
409
409
410 def copy_raw(self):
410 def copy_raw(self):
411 """ Copy the currently selected text to the clipboard without attempting
411 """ Copy the currently selected text to the clipboard without attempting
412 to remove prompts or otherwise alter the text.
412 to remove prompts or otherwise alter the text.
413 """
413 """
414 self._control.copy()
414 self._control.copy()
415
415
416 def execute_file(self, path, hidden=False):
416 def execute_file(self, path, hidden=False):
417 """ Attempts to execute file with 'path'. If 'hidden', no output is
417 """ Attempts to execute file with 'path'. If 'hidden', no output is
418 shown.
418 shown.
419 """
419 """
420 self.execute('execfile("%s")' % path, hidden=hidden)
420 self.execute('execfile("%s")' % path, hidden=hidden)
421
421
422 def interrupt_kernel(self):
422 def interrupt_kernel(self):
423 """ Attempts to interrupt the running kernel.
423 """ Attempts to interrupt the running kernel.
424 """
424 """
425 if self.custom_interrupt:
425 if self.custom_interrupt:
426 self.custom_interrupt_requested.emit()
426 self.custom_interrupt_requested.emit()
427 elif self.kernel_manager.has_kernel:
427 elif self.kernel_manager.has_kernel:
428 self.kernel_manager.interrupt_kernel()
428 self.kernel_manager.interrupt_kernel()
429 else:
429 else:
430 self._append_plain_text('Kernel process is either remote or '
430 self._append_plain_text('Kernel process is either remote or '
431 'unspecified. Cannot interrupt.\n')
431 'unspecified. Cannot interrupt.\n')
432
432
433 def reset(self):
433 def reset(self):
434 """ Resets the widget to its initial state. Similar to ``clear``, but
434 """ Resets the widget to its initial state. Similar to ``clear``, but
435 also re-writes the banner and aborts execution if necessary.
435 also re-writes the banner and aborts execution if necessary.
436 """
436 """
437 if self._executing:
437 if self._executing:
438 self._executing = False
438 self._executing = False
439 self._request_info['execute'] = None
439 self._request_info['execute'] = None
440 self._reading = False
440 self._reading = False
441 self._highlighter.highlighting_on = False
441 self._highlighter.highlighting_on = False
442
442
443 self._control.clear()
443 self._control.clear()
444 self._append_plain_text(self._get_banner())
444 self._append_plain_text(self._get_banner())
445 self._show_interpreter_prompt()
445 self._show_interpreter_prompt()
446
446
447 def restart_kernel(self, message, now=False):
447 def restart_kernel(self, message, now=False):
448 """ Attempts to restart the running kernel.
448 """ Attempts to restart the running kernel.
449 """
449 """
450 # FIXME: now should be configurable via a checkbox in the dialog. Right
450 # FIXME: now should be configurable via a checkbox in the dialog. Right
451 # now at least the heartbeat path sets it to True and the manual restart
451 # now at least the heartbeat path sets it to True and the manual restart
452 # to False. But those should just be the pre-selected states of a
452 # to False. But those should just be the pre-selected states of a
453 # checkbox that the user could override if so desired. But I don't know
453 # checkbox that the user could override if so desired. But I don't know
454 # enough Qt to go implementing the checkbox now.
454 # enough Qt to go implementing the checkbox now.
455
455
456 if self.custom_restart:
456 if self.custom_restart:
457 self.custom_restart_requested.emit()
457 self.custom_restart_requested.emit()
458
458
459 elif self.kernel_manager.has_kernel:
459 elif self.kernel_manager.has_kernel:
460 # Pause the heart beat channel to prevent further warnings.
460 # Pause the heart beat channel to prevent further warnings.
461 self.kernel_manager.hb_channel.pause()
461 self.kernel_manager.hb_channel.pause()
462
462
463 # Prompt the user to restart the kernel. Un-pause the heartbeat if
463 # Prompt the user to restart the kernel. Un-pause the heartbeat if
464 # they decline. (If they accept, the heartbeat will be un-paused
464 # they decline. (If they accept, the heartbeat will be un-paused
465 # automatically when the kernel is restarted.)
465 # automatically when the kernel is restarted.)
466 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
466 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
467 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
467 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
468 message, buttons)
468 message, buttons)
469 if result == QtGui.QMessageBox.Yes:
469 if result == QtGui.QMessageBox.Yes:
470 try:
470 try:
471 self.kernel_manager.restart_kernel(now=now)
471 self.kernel_manager.restart_kernel(now=now)
472 except RuntimeError:
472 except RuntimeError:
473 self._append_plain_text('Kernel started externally. '
473 self._append_plain_text('Kernel started externally. '
474 'Cannot restart.\n')
474 'Cannot restart.\n')
475 else:
475 else:
476 self.reset()
476 self.reset()
477 else:
477 else:
478 self.kernel_manager.hb_channel.unpause()
478 self.kernel_manager.hb_channel.unpause()
479
479
480 else:
480 else:
481 self._append_plain_text('Kernel process is either remote or '
481 self._append_plain_text('Kernel process is either remote or '
482 'unspecified. Cannot restart.\n')
482 'unspecified. Cannot restart.\n')
483
483
484 #---------------------------------------------------------------------------
484 #---------------------------------------------------------------------------
485 # 'FrontendWidget' protected interface
485 # 'FrontendWidget' protected interface
486 #---------------------------------------------------------------------------
486 #---------------------------------------------------------------------------
487
487
488 def _call_tip(self):
488 def _call_tip(self):
489 """ Shows a call tip, if appropriate, at the current cursor location.
489 """ Shows a call tip, if appropriate, at the current cursor location.
490 """
490 """
491 # Decide if it makes sense to show a call tip
491 # Decide if it makes sense to show a call tip
492 cursor = self._get_cursor()
492 cursor = self._get_cursor()
493 cursor.movePosition(QtGui.QTextCursor.Left)
493 cursor.movePosition(QtGui.QTextCursor.Left)
494 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
494 if cursor.document().characterAt(cursor.position()) != '(':
495 return False
495 return False
496 context = self._get_context(cursor)
496 context = self._get_context(cursor)
497 if not context:
497 if not context:
498 return False
498 return False
499
499
500 # Send the metadata request to the kernel
500 # Send the metadata request to the kernel
501 name = '.'.join(context)
501 name = '.'.join(context)
502 msg_id = self.kernel_manager.xreq_channel.object_info(name)
502 msg_id = self.kernel_manager.xreq_channel.object_info(name)
503 pos = self._get_cursor().position()
503 pos = self._get_cursor().position()
504 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
504 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
505 return True
505 return True
506
506
507 def _complete(self):
507 def _complete(self):
508 """ Performs completion at the current cursor location.
508 """ Performs completion at the current cursor location.
509 """
509 """
510 context = self._get_context()
510 context = self._get_context()
511 if context:
511 if context:
512 # Send the completion request to the kernel
512 # Send the completion request to the kernel
513 msg_id = self.kernel_manager.xreq_channel.complete(
513 msg_id = self.kernel_manager.xreq_channel.complete(
514 '.'.join(context), # text
514 '.'.join(context), # text
515 self._get_input_buffer_cursor_line(), # line
515 self._get_input_buffer_cursor_line(), # line
516 self._get_input_buffer_cursor_column(), # cursor_pos
516 self._get_input_buffer_cursor_column(), # cursor_pos
517 self.input_buffer) # block
517 self.input_buffer) # block
518 pos = self._get_cursor().position()
518 pos = self._get_cursor().position()
519 info = self._CompletionRequest(msg_id, pos)
519 info = self._CompletionRequest(msg_id, pos)
520 self._request_info['complete'] = info
520 self._request_info['complete'] = info
521
521
522 def _get_banner(self):
522 def _get_banner(self):
523 """ Gets a banner to display at the beginning of a session.
523 """ Gets a banner to display at the beginning of a session.
524 """
524 """
525 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
525 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
526 '"license" for more information.'
526 '"license" for more information.'
527 return banner % (sys.version, sys.platform)
527 return banner % (sys.version, sys.platform)
528
528
529 def _get_context(self, cursor=None):
529 def _get_context(self, cursor=None):
530 """ Gets the context for the specified cursor (or the current cursor
530 """ Gets the context for the specified cursor (or the current cursor
531 if none is specified).
531 if none is specified).
532 """
532 """
533 if cursor is None:
533 if cursor is None:
534 cursor = self._get_cursor()
534 cursor = self._get_cursor()
535 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
535 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
536 QtGui.QTextCursor.KeepAnchor)
536 QtGui.QTextCursor.KeepAnchor)
537 text = unicode(cursor.selection().toPlainText())
537 text = cursor.selection().toPlainText()
538 return self._completion_lexer.get_context(text)
538 return self._completion_lexer.get_context(text)
539
539
540 def _process_execute_abort(self, msg):
540 def _process_execute_abort(self, msg):
541 """ Process a reply for an aborted execution request.
541 """ Process a reply for an aborted execution request.
542 """
542 """
543 self._append_plain_text("ERROR: execution aborted\n")
543 self._append_plain_text("ERROR: execution aborted\n")
544
544
545 def _process_execute_error(self, msg):
545 def _process_execute_error(self, msg):
546 """ Process a reply for an execution request that resulted in an error.
546 """ Process a reply for an execution request that resulted in an error.
547 """
547 """
548 content = msg['content']
548 content = msg['content']
549 # If a SystemExit is passed along, this means exit() was called - also
549 # If a SystemExit is passed along, this means exit() was called - also
550 # all the ipython %exit magic syntax of '-k' to be used to keep
550 # all the ipython %exit magic syntax of '-k' to be used to keep
551 # the kernel running
551 # the kernel running
552 if content['ename']=='SystemExit':
552 if content['ename']=='SystemExit':
553 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
553 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
554 self._keep_kernel_on_exit = keepkernel
554 self._keep_kernel_on_exit = keepkernel
555 self.exit_requested.emit()
555 self.exit_requested.emit()
556 else:
556 else:
557 traceback = ''.join(content['traceback'])
557 traceback = ''.join(content['traceback'])
558 self._append_plain_text(traceback)
558 self._append_plain_text(traceback)
559
559
560 def _process_execute_ok(self, msg):
560 def _process_execute_ok(self, msg):
561 """ Process a reply for a successful execution equest.
561 """ Process a reply for a successful execution equest.
562 """
562 """
563 payload = msg['content']['payload']
563 payload = msg['content']['payload']
564 for item in payload:
564 for item in payload:
565 if not self._process_execute_payload(item):
565 if not self._process_execute_payload(item):
566 warning = 'Warning: received unknown payload of type %s'
566 warning = 'Warning: received unknown payload of type %s'
567 print(warning % repr(item['source']))
567 print(warning % repr(item['source']))
568
568
569 def _process_execute_payload(self, item):
569 def _process_execute_payload(self, item):
570 """ Process a single payload item from the list of payload items in an
570 """ Process a single payload item from the list of payload items in an
571 execution reply. Returns whether the payload was handled.
571 execution reply. Returns whether the payload was handled.
572 """
572 """
573 # The basic FrontendWidget doesn't handle payloads, as they are a
573 # The basic FrontendWidget doesn't handle payloads, as they are a
574 # mechanism for going beyond the standard Python interpreter model.
574 # mechanism for going beyond the standard Python interpreter model.
575 return False
575 return False
576
576
577 def _show_interpreter_prompt(self):
577 def _show_interpreter_prompt(self):
578 """ Shows a prompt for the interpreter.
578 """ Shows a prompt for the interpreter.
579 """
579 """
580 self._show_prompt('>>> ')
580 self._show_prompt('>>> ')
581
581
582 def _show_interpreter_prompt_for_reply(self, msg):
582 def _show_interpreter_prompt_for_reply(self, msg):
583 """ Shows a prompt for the interpreter given an 'execute_reply' message.
583 """ Shows a prompt for the interpreter given an 'execute_reply' message.
584 """
584 """
585 self._show_interpreter_prompt()
585 self._show_interpreter_prompt()
586
586
587 #------ Signal handlers ----------------------------------------------------
587 #------ Signal handlers ----------------------------------------------------
588
588
589 def _document_contents_change(self, position, removed, added):
589 def _document_contents_change(self, position, removed, added):
590 """ Called whenever the document's content changes. Display a call tip
590 """ Called whenever the document's content changes. Display a call tip
591 if appropriate.
591 if appropriate.
592 """
592 """
593 # Calculate where the cursor should be *after* the change:
593 # Calculate where the cursor should be *after* the change:
594 position += added
594 position += added
595
595
596 document = self._control.document()
596 document = self._control.document()
597 if position == self._get_cursor().position():
597 if position == self._get_cursor().position():
598 self._call_tip()
598 self._call_tip()
@@ -1,163 +1,163 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtGui
2 from IPython.external.qt import QtGui
3
3
4 # Local imports
4 # Local imports
5 from console_widget import ConsoleWidget
5 from console_widget import ConsoleWidget
6
6
7
7
8 class HistoryConsoleWidget(ConsoleWidget):
8 class HistoryConsoleWidget(ConsoleWidget):
9 """ A ConsoleWidget that keeps a history of the commands that have been
9 """ A ConsoleWidget that keeps a history of the commands that have been
10 executed and provides a readline-esque interface to this history.
10 executed and provides a readline-esque interface to this history.
11 """
11 """
12
12
13 #---------------------------------------------------------------------------
13 #---------------------------------------------------------------------------
14 # 'object' interface
14 # 'object' interface
15 #---------------------------------------------------------------------------
15 #---------------------------------------------------------------------------
16
16
17 def __init__(self, *args, **kw):
17 def __init__(self, *args, **kw):
18 super(HistoryConsoleWidget, self).__init__(*args, **kw)
18 super(HistoryConsoleWidget, self).__init__(*args, **kw)
19
19
20 # HistoryConsoleWidget protected variables.
20 # HistoryConsoleWidget protected variables.
21 self._history = []
21 self._history = []
22 self._history_index = 0
22 self._history_index = 0
23 self._history_prefix = ''
23 self._history_prefix = ''
24
24
25 #---------------------------------------------------------------------------
25 #---------------------------------------------------------------------------
26 # 'ConsoleWidget' public interface
26 # 'ConsoleWidget' public interface
27 #---------------------------------------------------------------------------
27 #---------------------------------------------------------------------------
28
28
29 def execute(self, source=None, hidden=False, interactive=False):
29 def execute(self, source=None, hidden=False, interactive=False):
30 """ Reimplemented to the store history.
30 """ Reimplemented to the store history.
31 """
31 """
32 if not hidden:
32 if not hidden:
33 history = self.input_buffer if source is None else source
33 history = self.input_buffer if source is None else source
34
34
35 executed = super(HistoryConsoleWidget, self).execute(
35 executed = super(HistoryConsoleWidget, self).execute(
36 source, hidden, interactive)
36 source, hidden, interactive)
37
37
38 if executed and not hidden:
38 if executed and not hidden:
39 # Save the command unless it was an empty string or was identical
39 # Save the command unless it was an empty string or was identical
40 # to the previous command.
40 # to the previous command.
41 history = history.rstrip()
41 history = history.rstrip()
42 if history and (not self._history or self._history[-1] != history):
42 if history and (not self._history or self._history[-1] != history):
43 self._history.append(history)
43 self._history.append(history)
44
44
45 # Move the history index to the most recent item.
45 # Move the history index to the most recent item.
46 self._history_index = len(self._history)
46 self._history_index = len(self._history)
47
47
48 return executed
48 return executed
49
49
50 #---------------------------------------------------------------------------
50 #---------------------------------------------------------------------------
51 # 'ConsoleWidget' abstract interface
51 # 'ConsoleWidget' abstract interface
52 #---------------------------------------------------------------------------
52 #---------------------------------------------------------------------------
53
53
54 def _up_pressed(self):
54 def _up_pressed(self):
55 """ Called when the up key is pressed. Returns whether to continue
55 """ Called when the up key is pressed. Returns whether to continue
56 processing the event.
56 processing the event.
57 """
57 """
58 prompt_cursor = self._get_prompt_cursor()
58 prompt_cursor = self._get_prompt_cursor()
59 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
59 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
60
60
61 # Set a search prefix based on the cursor position.
61 # Set a search prefix based on the cursor position.
62 col = self._get_input_buffer_cursor_column()
62 col = self._get_input_buffer_cursor_column()
63 input_buffer = self.input_buffer
63 input_buffer = self.input_buffer
64 if self._history_index == len(self._history) or \
64 if self._history_index == len(self._history) or \
65 (self._history_prefix and col != len(self._history_prefix)):
65 (self._history_prefix and col != len(self._history_prefix)):
66 self._history_index = len(self._history)
66 self._history_index = len(self._history)
67 self._history_prefix = input_buffer[:col]
67 self._history_prefix = input_buffer[:col]
68
68
69 # Perform the search.
69 # Perform the search.
70 self.history_previous(self._history_prefix)
70 self.history_previous(self._history_prefix)
71
71
72 # Go to the first line of the prompt for seemless history scrolling.
72 # Go to the first line of the prompt for seemless history scrolling.
73 # Emulate readline: keep the cursor position fixed for a prefix
73 # Emulate readline: keep the cursor position fixed for a prefix
74 # search.
74 # search.
75 cursor = self._get_prompt_cursor()
75 cursor = self._get_prompt_cursor()
76 if self._history_prefix:
76 if self._history_prefix:
77 cursor.movePosition(QtGui.QTextCursor.Right,
77 cursor.movePosition(QtGui.QTextCursor.Right,
78 n=len(self._history_prefix))
78 n=len(self._history_prefix))
79 else:
79 else:
80 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
80 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
81 self._set_cursor(cursor)
81 self._set_cursor(cursor)
82
82
83 return False
83 return False
84
84
85 return True
85 return True
86
86
87 def _down_pressed(self):
87 def _down_pressed(self):
88 """ Called when the down key is pressed. Returns whether to continue
88 """ Called when the down key is pressed. Returns whether to continue
89 processing the event.
89 processing the event.
90 """
90 """
91 end_cursor = self._get_end_cursor()
91 end_cursor = self._get_end_cursor()
92 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
92 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
93
93
94 # Perform the search.
94 # Perform the search.
95 self.history_next(self._history_prefix)
95 self.history_next(self._history_prefix)
96
96
97 # Emulate readline: keep the cursor position fixed for a prefix
97 # Emulate readline: keep the cursor position fixed for a prefix
98 # search. (We don't need to move the cursor to the end of the buffer
98 # search. (We don't need to move the cursor to the end of the buffer
99 # in the other case because this happens automatically when the
99 # in the other case because this happens automatically when the
100 # input buffer is set.)
100 # input buffer is set.)
101 if self._history_prefix:
101 if self._history_prefix:
102 cursor = self._get_prompt_cursor()
102 cursor = self._get_prompt_cursor()
103 cursor.movePosition(QtGui.QTextCursor.Right,
103 cursor.movePosition(QtGui.QTextCursor.Right,
104 n=len(self._history_prefix))
104 n=len(self._history_prefix))
105 self._set_cursor(cursor)
105 self._set_cursor(cursor)
106
106
107 return False
107 return False
108
108
109 return True
109 return True
110
110
111 #---------------------------------------------------------------------------
111 #---------------------------------------------------------------------------
112 # 'HistoryConsoleWidget' public interface
112 # 'HistoryConsoleWidget' public interface
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114
114
115 def history_previous(self, prefix=''):
115 def history_previous(self, prefix=''):
116 """ If possible, set the input buffer to a previous item in the history.
116 """ If possible, set the input buffer to a previous item in the history.
117
117
118 Parameters:
118 Parameters:
119 -----------
119 -----------
120 prefix : str, optional
120 prefix : str, optional
121 If specified, search for an item with this prefix.
121 If specified, search for an item with this prefix.
122 """
122 """
123 index = self._history_index
123 index = self._history_index
124 while index > 0:
124 while index > 0:
125 index -= 1
125 index -= 1
126 history = self._history[index]
126 history = self._history[index]
127 if history.startswith(prefix):
127 if history.startswith(prefix):
128 break
128 break
129 else:
129 else:
130 history = None
130 history = None
131
131
132 if history is not None:
132 if history is not None:
133 self._history_index = index
133 self._history_index = index
134 self.input_buffer = history
134 self.input_buffer = history
135
135
136 def history_next(self, prefix=''):
136 def history_next(self, prefix=''):
137 """ Set the input buffer to a subsequent item in the history, or to the
137 """ Set the input buffer to a subsequent item in the history, or to the
138 original search prefix if there is no such item.
138 original search prefix if there is no such item.
139
139
140 Parameters:
140 Parameters:
141 -----------
141 -----------
142 prefix : str, optional
142 prefix : str, optional
143 If specified, search for an item with this prefix.
143 If specified, search for an item with this prefix.
144 """
144 """
145 while self._history_index < len(self._history) - 1:
145 while self._history_index < len(self._history) - 1:
146 self._history_index += 1
146 self._history_index += 1
147 history = self._history[self._history_index]
147 history = self._history[self._history_index]
148 if history.startswith(prefix):
148 if history.startswith(prefix):
149 break
149 break
150 else:
150 else:
151 self._history_index = len(self._history)
151 self._history_index = len(self._history)
152 history = prefix
152 history = prefix
153 self.input_buffer = history
153 self.input_buffer = history
154
154
155 #---------------------------------------------------------------------------
155 #---------------------------------------------------------------------------
156 # 'HistoryConsoleWidget' protected interface
156 # 'HistoryConsoleWidget' protected interface
157 #---------------------------------------------------------------------------
157 #---------------------------------------------------------------------------
158
158
159 def _set_history(self, history):
159 def _set_history(self, history):
160 """ Replace the current history with a sequence of history items.
160 """ Replace the current history with a sequence of history items.
161 """
161 """
162 self._history = list(history)
162 self._history = list(history)
163 self._history_index = len(self._history)
163 self._history_index = len(self._history)
@@ -1,497 +1,497 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3
3
4 TODO: Add support for retrieving the system default editor. Requires code
4 TODO: Add support for retrieving the system default editor. Requires code
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 Linux (use the xdg system).
6 Linux (use the xdg system).
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Imports
10 # Imports
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 # Standard library imports
13 # Standard library imports
14 from collections import namedtuple
14 from collections import namedtuple
15 import re
15 import re
16 from subprocess import Popen
16 from subprocess import Popen
17 from textwrap import dedent
17 from textwrap import dedent
18
18
19 # System library imports
19 # System library imports
20 from PyQt4 import QtCore, QtGui
20 from IPython.external.qt import QtCore, QtGui
21
21
22 # Local imports
22 # Local imports
23 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 from IPython.core.inputsplitter import IPythonInputSplitter, \
24 transform_ipy_prompt
24 transform_ipy_prompt
25 from IPython.core.usage import default_gui_banner
25 from IPython.core.usage import default_gui_banner
26 from IPython.utils.traitlets import Bool, Str
26 from IPython.utils.traitlets import Bool, Str
27 from frontend_widget import FrontendWidget
27 from frontend_widget import FrontendWidget
28 from styles import (default_light_style_sheet, default_light_syntax_style,
28 from styles import (default_light_style_sheet, default_light_syntax_style,
29 default_dark_style_sheet, default_dark_syntax_style,
29 default_dark_style_sheet, default_dark_syntax_style,
30 default_bw_style_sheet, default_bw_syntax_style)
30 default_bw_style_sheet, default_bw_syntax_style)
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Constants
33 # Constants
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 # Default strings to build and display input and output prompts (and separators
36 # Default strings to build and display input and output prompts (and separators
37 # in between)
37 # in between)
38 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
38 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
39 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
39 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
40 default_input_sep = '\n'
40 default_input_sep = '\n'
41 default_output_sep = ''
41 default_output_sep = ''
42 default_output_sep2 = ''
42 default_output_sep2 = ''
43
43
44 # Base path for most payload sources.
44 # Base path for most payload sources.
45 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
45 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
46
46
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48 # IPythonWidget class
48 # IPythonWidget class
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50
50
51 class IPythonWidget(FrontendWidget):
51 class IPythonWidget(FrontendWidget):
52 """ A FrontendWidget for an IPython kernel.
52 """ A FrontendWidget for an IPython kernel.
53 """
53 """
54
54
55 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
55 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
56 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 # settings.
57 # settings.
58 custom_edit = Bool(False)
58 custom_edit = Bool(False)
59 custom_edit_requested = QtCore.pyqtSignal(object, object)
59 custom_edit_requested = QtCore.Signal(object, object)
60
60
61 # A command for invoking a system text editor. If the string contains a
61 # A command for invoking a system text editor. If the string contains a
62 # {filename} format specifier, it will be used. Otherwise, the filename will
62 # {filename} format specifier, it will be used. Otherwise, the filename will
63 # be appended to the end the command.
63 # be appended to the end the command.
64 editor = Str('default', config=True)
64 editor = Str('default', config=True)
65
65
66 # The editor command to use when a specific line number is requested. The
66 # The editor command to use when a specific line number is requested. The
67 # string should contain two format specifiers: {line} and {filename}. If
67 # string should contain two format specifiers: {line} and {filename}. If
68 # this parameter is not specified, the line number option to the %edit magic
68 # this parameter is not specified, the line number option to the %edit magic
69 # will be ignored.
69 # will be ignored.
70 editor_line = Str(config=True)
70 editor_line = Str(config=True)
71
71
72 # A CSS stylesheet. The stylesheet can contain classes for:
72 # A CSS stylesheet. The stylesheet can contain classes for:
73 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
73 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
74 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
74 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
75 # 3. IPython: .error, .in-prompt, .out-prompt, etc
75 # 3. IPython: .error, .in-prompt, .out-prompt, etc
76 style_sheet = Str(config=True)
76 style_sheet = Str(config=True)
77
77
78 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
78 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
79 # the style sheet is queried for Pygments style information.
79 # the style sheet is queried for Pygments style information.
80 syntax_style = Str(config=True)
80 syntax_style = Str(config=True)
81
81
82 # Prompts.
82 # Prompts.
83 in_prompt = Str(default_in_prompt, config=True)
83 in_prompt = Str(default_in_prompt, config=True)
84 out_prompt = Str(default_out_prompt, config=True)
84 out_prompt = Str(default_out_prompt, config=True)
85 input_sep = Str(default_input_sep, config=True)
85 input_sep = Str(default_input_sep, config=True)
86 output_sep = Str(default_output_sep, config=True)
86 output_sep = Str(default_output_sep, config=True)
87 output_sep2 = Str(default_output_sep2, config=True)
87 output_sep2 = Str(default_output_sep2, config=True)
88
88
89 # FrontendWidget protected class variables.
89 # FrontendWidget protected class variables.
90 _input_splitter_class = IPythonInputSplitter
90 _input_splitter_class = IPythonInputSplitter
91
91
92 # IPythonWidget protected class variables.
92 # IPythonWidget protected class variables.
93 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
93 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
94 _payload_source_edit = zmq_shell_source + '.edit_magic'
94 _payload_source_edit = zmq_shell_source + '.edit_magic'
95 _payload_source_exit = zmq_shell_source + '.ask_exit'
95 _payload_source_exit = zmq_shell_source + '.ask_exit'
96 _payload_source_loadpy = zmq_shell_source + '.magic_loadpy'
96 _payload_source_loadpy = zmq_shell_source + '.magic_loadpy'
97 _payload_source_page = 'IPython.zmq.page.page'
97 _payload_source_page = 'IPython.zmq.page.page'
98
98
99 #---------------------------------------------------------------------------
99 #---------------------------------------------------------------------------
100 # 'object' interface
100 # 'object' interface
101 #---------------------------------------------------------------------------
101 #---------------------------------------------------------------------------
102
102
103 def __init__(self, *args, **kw):
103 def __init__(self, *args, **kw):
104 super(IPythonWidget, self).__init__(*args, **kw)
104 super(IPythonWidget, self).__init__(*args, **kw)
105
105
106 # IPythonWidget protected variables.
106 # IPythonWidget protected variables.
107 self._code_to_load = None
107 self._code_to_load = None
108 self._payload_handlers = {
108 self._payload_handlers = {
109 self._payload_source_edit : self._handle_payload_edit,
109 self._payload_source_edit : self._handle_payload_edit,
110 self._payload_source_exit : self._handle_payload_exit,
110 self._payload_source_exit : self._handle_payload_exit,
111 self._payload_source_page : self._handle_payload_page,
111 self._payload_source_page : self._handle_payload_page,
112 self._payload_source_loadpy : self._handle_payload_loadpy }
112 self._payload_source_loadpy : self._handle_payload_loadpy }
113 self._previous_prompt_obj = None
113 self._previous_prompt_obj = None
114 self._keep_kernel_on_exit = None
114 self._keep_kernel_on_exit = None
115
115
116 # Initialize widget styling.
116 # Initialize widget styling.
117 if self.style_sheet:
117 if self.style_sheet:
118 self._style_sheet_changed()
118 self._style_sheet_changed()
119 self._syntax_style_changed()
119 self._syntax_style_changed()
120 else:
120 else:
121 self.set_default_style()
121 self.set_default_style()
122
122
123 #---------------------------------------------------------------------------
123 #---------------------------------------------------------------------------
124 # 'BaseFrontendMixin' abstract interface
124 # 'BaseFrontendMixin' abstract interface
125 #---------------------------------------------------------------------------
125 #---------------------------------------------------------------------------
126
126
127 def _handle_complete_reply(self, rep):
127 def _handle_complete_reply(self, rep):
128 """ Reimplemented to support IPython's improved completion machinery.
128 """ Reimplemented to support IPython's improved completion machinery.
129 """
129 """
130 cursor = self._get_cursor()
130 cursor = self._get_cursor()
131 info = self._request_info.get('complete')
131 info = self._request_info.get('complete')
132 if info and info.id == rep['parent_header']['msg_id'] and \
132 if info and info.id == rep['parent_header']['msg_id'] and \
133 info.pos == cursor.position():
133 info.pos == cursor.position():
134 matches = rep['content']['matches']
134 matches = rep['content']['matches']
135 text = rep['content']['matched_text']
135 text = rep['content']['matched_text']
136 offset = len(text)
136 offset = len(text)
137
137
138 # Clean up matches with period and path separators if the matched
138 # Clean up matches with period and path separators if the matched
139 # text has not been transformed. This is done by truncating all
139 # text has not been transformed. This is done by truncating all
140 # but the last component and then suitably decreasing the offset
140 # but the last component and then suitably decreasing the offset
141 # between the current cursor position and the start of completion.
141 # between the current cursor position and the start of completion.
142 if len(matches) > 1 and matches[0][:offset] == text:
142 if len(matches) > 1 and matches[0][:offset] == text:
143 parts = re.split(r'[./\\]', text)
143 parts = re.split(r'[./\\]', text)
144 sep_count = len(parts) - 1
144 sep_count = len(parts) - 1
145 if sep_count:
145 if sep_count:
146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
147 matches = [ match[chop_length:] for match in matches ]
147 matches = [ match[chop_length:] for match in matches ]
148 offset -= chop_length
148 offset -= chop_length
149
149
150 # Move the cursor to the start of the match and complete.
150 # Move the cursor to the start of the match and complete.
151 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
151 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
152 self._complete_with_items(cursor, matches)
152 self._complete_with_items(cursor, matches)
153
153
154 def _handle_execute_reply(self, msg):
154 def _handle_execute_reply(self, msg):
155 """ Reimplemented to support prompt requests.
155 """ Reimplemented to support prompt requests.
156 """
156 """
157 info = self._request_info.get('execute')
157 info = self._request_info.get('execute')
158 if info and info.id == msg['parent_header']['msg_id']:
158 if info and info.id == msg['parent_header']['msg_id']:
159 if info.kind == 'prompt':
159 if info.kind == 'prompt':
160 number = msg['content']['execution_count'] + 1
160 number = msg['content']['execution_count'] + 1
161 self._show_interpreter_prompt(number)
161 self._show_interpreter_prompt(number)
162 else:
162 else:
163 super(IPythonWidget, self)._handle_execute_reply(msg)
163 super(IPythonWidget, self)._handle_execute_reply(msg)
164
164
165 def _handle_history_reply(self, msg):
165 def _handle_history_reply(self, msg):
166 """ Implemented to handle history replies, which are only supported by
166 """ Implemented to handle history replies, which are only supported by
167 the IPython kernel.
167 the IPython kernel.
168 """
168 """
169 history_dict = msg['content']['history']
169 history_dict = msg['content']['history']
170 input_history_dict = {}
170 input_history_dict = {}
171 for key,val in history_dict.items():
171 for key,val in history_dict.items():
172 input_history_dict[int(key)] = val
172 input_history_dict[int(key)] = val
173 items = [ val.rstrip() for _, val in sorted(input_history_dict.items()) ]
173 items = [ val.rstrip() for _, val in sorted(input_history_dict.items()) ]
174 self._set_history(items)
174 self._set_history(items)
175
175
176 def _handle_pyout(self, msg):
176 def _handle_pyout(self, msg):
177 """ Reimplemented for IPython-style "display hook".
177 """ Reimplemented for IPython-style "display hook".
178 """
178 """
179 if not self._hidden and self._is_from_this_session(msg):
179 if not self._hidden and self._is_from_this_session(msg):
180 content = msg['content']
180 content = msg['content']
181 prompt_number = content['execution_count']
181 prompt_number = content['execution_count']
182 data = content['data']
182 data = content['data']
183 if data.has_key('text/html'):
183 if data.has_key('text/html'):
184 self._append_plain_text(self.output_sep)
184 self._append_plain_text(self.output_sep)
185 self._append_html(self._make_out_prompt(prompt_number))
185 self._append_html(self._make_out_prompt(prompt_number))
186 html = data['text/html']
186 html = data['text/html']
187 self._append_plain_text('\n')
187 self._append_plain_text('\n')
188 self._append_html(html + self.output_sep2)
188 self._append_html(html + self.output_sep2)
189 elif data.has_key('text/plain'):
189 elif data.has_key('text/plain'):
190 self._append_plain_text(self.output_sep)
190 self._append_plain_text(self.output_sep)
191 self._append_html(self._make_out_prompt(prompt_number))
191 self._append_html(self._make_out_prompt(prompt_number))
192 text = data['text/plain']
192 text = data['text/plain']
193 self._append_plain_text(text + self.output_sep2)
193 self._append_plain_text(text + self.output_sep2)
194
194
195 def _handle_display_data(self, msg):
195 def _handle_display_data(self, msg):
196 """ The base handler for the ``display_data`` message.
196 """ The base handler for the ``display_data`` message.
197 """
197 """
198 # For now, we don't display data from other frontends, but we
198 # For now, we don't display data from other frontends, but we
199 # eventually will as this allows all frontends to monitor the display
199 # eventually will as this allows all frontends to monitor the display
200 # data. But we need to figure out how to handle this in the GUI.
200 # data. But we need to figure out how to handle this in the GUI.
201 if not self._hidden and self._is_from_this_session(msg):
201 if not self._hidden and self._is_from_this_session(msg):
202 source = msg['content']['source']
202 source = msg['content']['source']
203 data = msg['content']['data']
203 data = msg['content']['data']
204 metadata = msg['content']['metadata']
204 metadata = msg['content']['metadata']
205 # In the regular IPythonWidget, we simply print the plain text
205 # In the regular IPythonWidget, we simply print the plain text
206 # representation.
206 # representation.
207 if data.has_key('text/html'):
207 if data.has_key('text/html'):
208 html = data['text/html']
208 html = data['text/html']
209 self._append_html(html)
209 self._append_html(html)
210 elif data.has_key('text/plain'):
210 elif data.has_key('text/plain'):
211 text = data['text/plain']
211 text = data['text/plain']
212 self._append_plain_text(text)
212 self._append_plain_text(text)
213 # This newline seems to be needed for text and html output.
213 # This newline seems to be needed for text and html output.
214 self._append_plain_text(u'\n')
214 self._append_plain_text(u'\n')
215
215
216 def _started_channels(self):
216 def _started_channels(self):
217 """ Reimplemented to make a history request.
217 """ Reimplemented to make a history request.
218 """
218 """
219 super(IPythonWidget, self)._started_channels()
219 super(IPythonWidget, self)._started_channels()
220 self.kernel_manager.xreq_channel.history(raw=True, output=False)
220 self.kernel_manager.xreq_channel.history(raw=True, output=False)
221
221
222 #---------------------------------------------------------------------------
222 #---------------------------------------------------------------------------
223 # 'ConsoleWidget' public interface
223 # 'ConsoleWidget' public interface
224 #---------------------------------------------------------------------------
224 #---------------------------------------------------------------------------
225
225
226 def copy(self):
226 def copy(self):
227 """ Copy the currently selected text to the clipboard, removing prompts
227 """ Copy the currently selected text to the clipboard, removing prompts
228 if possible.
228 if possible.
229 """
229 """
230 text = unicode(self._control.textCursor().selection().toPlainText())
230 text = self._control.textCursor().selection().toPlainText()
231 if text:
231 if text:
232 lines = map(transform_ipy_prompt, text.splitlines())
232 lines = map(transform_ipy_prompt, text.splitlines())
233 text = '\n'.join(lines)
233 text = '\n'.join(lines)
234 QtGui.QApplication.clipboard().setText(text)
234 QtGui.QApplication.clipboard().setText(text)
235
235
236 #---------------------------------------------------------------------------
236 #---------------------------------------------------------------------------
237 # 'FrontendWidget' public interface
237 # 'FrontendWidget' public interface
238 #---------------------------------------------------------------------------
238 #---------------------------------------------------------------------------
239
239
240 def execute_file(self, path, hidden=False):
240 def execute_file(self, path, hidden=False):
241 """ Reimplemented to use the 'run' magic.
241 """ Reimplemented to use the 'run' magic.
242 """
242 """
243 self.execute('%%run %s' % path, hidden=hidden)
243 self.execute('%%run %s' % path, hidden=hidden)
244
244
245 #---------------------------------------------------------------------------
245 #---------------------------------------------------------------------------
246 # 'FrontendWidget' protected interface
246 # 'FrontendWidget' protected interface
247 #---------------------------------------------------------------------------
247 #---------------------------------------------------------------------------
248
248
249 def _complete(self):
249 def _complete(self):
250 """ Reimplemented to support IPython's improved completion machinery.
250 """ Reimplemented to support IPython's improved completion machinery.
251 """
251 """
252 # We let the kernel split the input line, so we *always* send an empty
252 # We let the kernel split the input line, so we *always* send an empty
253 # text field. Readline-based frontends do get a real text field which
253 # text field. Readline-based frontends do get a real text field which
254 # they can use.
254 # they can use.
255 text = ''
255 text = ''
256
256
257 # Send the completion request to the kernel
257 # Send the completion request to the kernel
258 msg_id = self.kernel_manager.xreq_channel.complete(
258 msg_id = self.kernel_manager.xreq_channel.complete(
259 text, # text
259 text, # text
260 self._get_input_buffer_cursor_line(), # line
260 self._get_input_buffer_cursor_line(), # line
261 self._get_input_buffer_cursor_column(), # cursor_pos
261 self._get_input_buffer_cursor_column(), # cursor_pos
262 self.input_buffer) # block
262 self.input_buffer) # block
263 pos = self._get_cursor().position()
263 pos = self._get_cursor().position()
264 info = self._CompletionRequest(msg_id, pos)
264 info = self._CompletionRequest(msg_id, pos)
265 self._request_info['complete'] = info
265 self._request_info['complete'] = info
266
266
267 def _get_banner(self):
267 def _get_banner(self):
268 """ Reimplemented to return IPython's default banner.
268 """ Reimplemented to return IPython's default banner.
269 """
269 """
270 return default_gui_banner
270 return default_gui_banner
271
271
272 def _process_execute_error(self, msg):
272 def _process_execute_error(self, msg):
273 """ Reimplemented for IPython-style traceback formatting.
273 """ Reimplemented for IPython-style traceback formatting.
274 """
274 """
275 content = msg['content']
275 content = msg['content']
276 traceback = '\n'.join(content['traceback']) + '\n'
276 traceback = '\n'.join(content['traceback']) + '\n'
277 if False:
277 if False:
278 # FIXME: For now, tracebacks come as plain text, so we can't use
278 # FIXME: For now, tracebacks come as plain text, so we can't use
279 # the html renderer yet. Once we refactor ultratb to produce
279 # the html renderer yet. Once we refactor ultratb to produce
280 # properly styled tracebacks, this branch should be the default
280 # properly styled tracebacks, this branch should be the default
281 traceback = traceback.replace(' ', '&nbsp;')
281 traceback = traceback.replace(' ', '&nbsp;')
282 traceback = traceback.replace('\n', '<br/>')
282 traceback = traceback.replace('\n', '<br/>')
283
283
284 ename = content['ename']
284 ename = content['ename']
285 ename_styled = '<span class="error">%s</span>' % ename
285 ename_styled = '<span class="error">%s</span>' % ename
286 traceback = traceback.replace(ename, ename_styled)
286 traceback = traceback.replace(ename, ename_styled)
287
287
288 self._append_html(traceback)
288 self._append_html(traceback)
289 else:
289 else:
290 # This is the fallback for now, using plain text with ansi escapes
290 # This is the fallback for now, using plain text with ansi escapes
291 self._append_plain_text(traceback)
291 self._append_plain_text(traceback)
292
292
293 def _process_execute_payload(self, item):
293 def _process_execute_payload(self, item):
294 """ Reimplemented to dispatch payloads to handler methods.
294 """ Reimplemented to dispatch payloads to handler methods.
295 """
295 """
296 handler = self._payload_handlers.get(item['source'])
296 handler = self._payload_handlers.get(item['source'])
297 if handler is None:
297 if handler is None:
298 # We have no handler for this type of payload, simply ignore it
298 # We have no handler for this type of payload, simply ignore it
299 return False
299 return False
300 else:
300 else:
301 handler(item)
301 handler(item)
302 return True
302 return True
303
303
304 def _show_interpreter_prompt(self, number=None):
304 def _show_interpreter_prompt(self, number=None):
305 """ Reimplemented for IPython-style prompts.
305 """ Reimplemented for IPython-style prompts.
306 """
306 """
307 # If a number was not specified, make a prompt number request.
307 # If a number was not specified, make a prompt number request.
308 if number is None:
308 if number is None:
309 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
309 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
310 info = self._ExecutionRequest(msg_id, 'prompt')
310 info = self._ExecutionRequest(msg_id, 'prompt')
311 self._request_info['execute'] = info
311 self._request_info['execute'] = info
312 return
312 return
313
313
314 # Show a new prompt and save information about it so that it can be
314 # Show a new prompt and save information about it so that it can be
315 # updated later if the prompt number turns out to be wrong.
315 # updated later if the prompt number turns out to be wrong.
316 self._prompt_sep = self.input_sep
316 self._prompt_sep = self.input_sep
317 self._show_prompt(self._make_in_prompt(number), html=True)
317 self._show_prompt(self._make_in_prompt(number), html=True)
318 block = self._control.document().lastBlock()
318 block = self._control.document().lastBlock()
319 length = len(self._prompt)
319 length = len(self._prompt)
320 self._previous_prompt_obj = self._PromptBlock(block, length, number)
320 self._previous_prompt_obj = self._PromptBlock(block, length, number)
321
321
322 # Update continuation prompt to reflect (possibly) new prompt length.
322 # Update continuation prompt to reflect (possibly) new prompt length.
323 self._set_continuation_prompt(
323 self._set_continuation_prompt(
324 self._make_continuation_prompt(self._prompt), html=True)
324 self._make_continuation_prompt(self._prompt), html=True)
325
325
326 # Load code from the %loadpy magic, if necessary.
326 # Load code from the %loadpy magic, if necessary.
327 if self._code_to_load is not None:
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 self._code_to_load = None
329 self._code_to_load = None
330
330
331 def _show_interpreter_prompt_for_reply(self, msg):
331 def _show_interpreter_prompt_for_reply(self, msg):
332 """ Reimplemented for IPython-style prompts.
332 """ Reimplemented for IPython-style prompts.
333 """
333 """
334 # Update the old prompt number if necessary.
334 # Update the old prompt number if necessary.
335 content = msg['content']
335 content = msg['content']
336 previous_prompt_number = content['execution_count']
336 previous_prompt_number = content['execution_count']
337 if self._previous_prompt_obj and \
337 if self._previous_prompt_obj and \
338 self._previous_prompt_obj.number != previous_prompt_number:
338 self._previous_prompt_obj.number != previous_prompt_number:
339 block = self._previous_prompt_obj.block
339 block = self._previous_prompt_obj.block
340
340
341 # Make sure the prompt block has not been erased.
341 # Make sure the prompt block has not been erased.
342 if block.isValid() and not block.text().isEmpty():
342 if block.isValid() and not block.text().isEmpty():
343
343
344 # Remove the old prompt and insert a new prompt.
344 # Remove the old prompt and insert a new prompt.
345 cursor = QtGui.QTextCursor(block)
345 cursor = QtGui.QTextCursor(block)
346 cursor.movePosition(QtGui.QTextCursor.Right,
346 cursor.movePosition(QtGui.QTextCursor.Right,
347 QtGui.QTextCursor.KeepAnchor,
347 QtGui.QTextCursor.KeepAnchor,
348 self._previous_prompt_obj.length)
348 self._previous_prompt_obj.length)
349 prompt = self._make_in_prompt(previous_prompt_number)
349 prompt = self._make_in_prompt(previous_prompt_number)
350 self._prompt = self._insert_html_fetching_plain_text(
350 self._prompt = self._insert_html_fetching_plain_text(
351 cursor, prompt)
351 cursor, prompt)
352
352
353 # When the HTML is inserted, Qt blows away the syntax
353 # When the HTML is inserted, Qt blows away the syntax
354 # highlighting for the line, so we need to rehighlight it.
354 # highlighting for the line, so we need to rehighlight it.
355 self._highlighter.rehighlightBlock(cursor.block())
355 self._highlighter.rehighlightBlock(cursor.block())
356
356
357 self._previous_prompt_obj = None
357 self._previous_prompt_obj = None
358
358
359 # Show a new prompt with the kernel's estimated prompt number.
359 # Show a new prompt with the kernel's estimated prompt number.
360 self._show_interpreter_prompt(previous_prompt_number + 1)
360 self._show_interpreter_prompt(previous_prompt_number + 1)
361
361
362 #---------------------------------------------------------------------------
362 #---------------------------------------------------------------------------
363 # 'IPythonWidget' interface
363 # 'IPythonWidget' interface
364 #---------------------------------------------------------------------------
364 #---------------------------------------------------------------------------
365
365
366 def set_default_style(self, colors='lightbg'):
366 def set_default_style(self, colors='lightbg'):
367 """ Sets the widget style to the class defaults.
367 """ Sets the widget style to the class defaults.
368
368
369 Parameters:
369 Parameters:
370 -----------
370 -----------
371 colors : str, optional (default lightbg)
371 colors : str, optional (default lightbg)
372 Whether to use the default IPython light background or dark
372 Whether to use the default IPython light background or dark
373 background or B&W style.
373 background or B&W style.
374 """
374 """
375 colors = colors.lower()
375 colors = colors.lower()
376 if colors=='lightbg':
376 if colors=='lightbg':
377 self.style_sheet = default_light_style_sheet
377 self.style_sheet = default_light_style_sheet
378 self.syntax_style = default_light_syntax_style
378 self.syntax_style = default_light_syntax_style
379 elif colors=='linux':
379 elif colors=='linux':
380 self.style_sheet = default_dark_style_sheet
380 self.style_sheet = default_dark_style_sheet
381 self.syntax_style = default_dark_syntax_style
381 self.syntax_style = default_dark_syntax_style
382 elif colors=='nocolor':
382 elif colors=='nocolor':
383 self.style_sheet = default_bw_style_sheet
383 self.style_sheet = default_bw_style_sheet
384 self.syntax_style = default_bw_syntax_style
384 self.syntax_style = default_bw_syntax_style
385 else:
385 else:
386 raise KeyError("No such color scheme: %s"%colors)
386 raise KeyError("No such color scheme: %s"%colors)
387
387
388 #---------------------------------------------------------------------------
388 #---------------------------------------------------------------------------
389 # 'IPythonWidget' protected interface
389 # 'IPythonWidget' protected interface
390 #---------------------------------------------------------------------------
390 #---------------------------------------------------------------------------
391
391
392 def _edit(self, filename, line=None):
392 def _edit(self, filename, line=None):
393 """ Opens a Python script for editing.
393 """ Opens a Python script for editing.
394
394
395 Parameters:
395 Parameters:
396 -----------
396 -----------
397 filename : str
397 filename : str
398 A path to a local system file.
398 A path to a local system file.
399
399
400 line : int, optional
400 line : int, optional
401 A line of interest in the file.
401 A line of interest in the file.
402 """
402 """
403 if self.custom_edit:
403 if self.custom_edit:
404 self.custom_edit_requested.emit(filename, line)
404 self.custom_edit_requested.emit(filename, line)
405 elif self.editor == 'default':
405 elif self.editor == 'default':
406 self._append_plain_text('No default editor available.\n')
406 self._append_plain_text('No default editor available.\n')
407 else:
407 else:
408 try:
408 try:
409 filename = '"%s"' % filename
409 filename = '"%s"' % filename
410 if line and self.editor_line:
410 if line and self.editor_line:
411 command = self.editor_line.format(filename=filename,
411 command = self.editor_line.format(filename=filename,
412 line=line)
412 line=line)
413 else:
413 else:
414 try:
414 try:
415 command = self.editor.format()
415 command = self.editor.format()
416 except KeyError:
416 except KeyError:
417 command = self.editor.format(filename=filename)
417 command = self.editor.format(filename=filename)
418 else:
418 else:
419 command += ' ' + filename
419 command += ' ' + filename
420 except KeyError:
420 except KeyError:
421 self._append_plain_text('Invalid editor command.\n')
421 self._append_plain_text('Invalid editor command.\n')
422 else:
422 else:
423 try:
423 try:
424 Popen(command, shell=True)
424 Popen(command, shell=True)
425 except OSError:
425 except OSError:
426 msg = 'Opening editor with command "%s" failed.\n'
426 msg = 'Opening editor with command "%s" failed.\n'
427 self._append_plain_text(msg % command)
427 self._append_plain_text(msg % command)
428
428
429 def _make_in_prompt(self, number):
429 def _make_in_prompt(self, number):
430 """ Given a prompt number, returns an HTML In prompt.
430 """ Given a prompt number, returns an HTML In prompt.
431 """
431 """
432 body = self.in_prompt % number
432 body = self.in_prompt % number
433 return '<span class="in-prompt">%s</span>' % body
433 return '<span class="in-prompt">%s</span>' % body
434
434
435 def _make_continuation_prompt(self, prompt):
435 def _make_continuation_prompt(self, prompt):
436 """ Given a plain text version of an In prompt, returns an HTML
436 """ Given a plain text version of an In prompt, returns an HTML
437 continuation prompt.
437 continuation prompt.
438 """
438 """
439 end_chars = '...: '
439 end_chars = '...: '
440 space_count = len(prompt.lstrip('\n')) - len(end_chars)
440 space_count = len(prompt.lstrip('\n')) - len(end_chars)
441 body = '&nbsp;' * space_count + end_chars
441 body = '&nbsp;' * space_count + end_chars
442 return '<span class="in-prompt">%s</span>' % body
442 return '<span class="in-prompt">%s</span>' % body
443
443
444 def _make_out_prompt(self, number):
444 def _make_out_prompt(self, number):
445 """ Given a prompt number, returns an HTML Out prompt.
445 """ Given a prompt number, returns an HTML Out prompt.
446 """
446 """
447 body = self.out_prompt % number
447 body = self.out_prompt % number
448 return '<span class="out-prompt">%s</span>' % body
448 return '<span class="out-prompt">%s</span>' % body
449
449
450 #------ Payload handlers --------------------------------------------------
450 #------ Payload handlers --------------------------------------------------
451
451
452 # Payload handlers with a generic interface: each takes the opaque payload
452 # Payload handlers with a generic interface: each takes the opaque payload
453 # dict, unpacks it and calls the underlying functions with the necessary
453 # dict, unpacks it and calls the underlying functions with the necessary
454 # arguments.
454 # arguments.
455
455
456 def _handle_payload_edit(self, item):
456 def _handle_payload_edit(self, item):
457 self._edit(item['filename'], item['line_number'])
457 self._edit(item['filename'], item['line_number'])
458
458
459 def _handle_payload_exit(self, item):
459 def _handle_payload_exit(self, item):
460 self._keep_kernel_on_exit = item['keepkernel']
460 self._keep_kernel_on_exit = item['keepkernel']
461 self.exit_requested.emit()
461 self.exit_requested.emit()
462
462
463 def _handle_payload_loadpy(self, item):
463 def _handle_payload_loadpy(self, item):
464 # Simple save the text of the .py file for later. The text is written
464 # Simple save the text of the .py file for later. The text is written
465 # to the buffer when _prompt_started_hook is called.
465 # to the buffer when _prompt_started_hook is called.
466 self._code_to_load = item['text']
466 self._code_to_load = item['text']
467
467
468 def _handle_payload_page(self, item):
468 def _handle_payload_page(self, item):
469 # Since the plain text widget supports only a very small subset of HTML
469 # Since the plain text widget supports only a very small subset of HTML
470 # and we have no control over the HTML source, we only page HTML
470 # and we have no control over the HTML source, we only page HTML
471 # payloads in the rich text widget.
471 # payloads in the rich text widget.
472 if item['html'] and self.kind == 'rich':
472 if item['html'] and self.kind == 'rich':
473 self._page(item['html'], html=True)
473 self._page(item['html'], html=True)
474 else:
474 else:
475 self._page(item['text'], html=False)
475 self._page(item['text'], html=False)
476
476
477 #------ Trait change handlers --------------------------------------------
477 #------ Trait change handlers --------------------------------------------
478
478
479 def _style_sheet_changed(self):
479 def _style_sheet_changed(self):
480 """ Set the style sheets of the underlying widgets.
480 """ Set the style sheets of the underlying widgets.
481 """
481 """
482 self.setStyleSheet(self.style_sheet)
482 self.setStyleSheet(self.style_sheet)
483 self._control.document().setDefaultStyleSheet(self.style_sheet)
483 self._control.document().setDefaultStyleSheet(self.style_sheet)
484 if self._page_control:
484 if self._page_control:
485 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
485 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
486
486
487 bg_color = self._control.palette().background().color()
487 bg_color = self._control.palette().background().color()
488 self._ansi_processor.set_background_color(bg_color)
488 self._ansi_processor.set_background_color(bg_color)
489
489
490 def _syntax_style_changed(self):
490 def _syntax_style_changed(self):
491 """ Set the style for the syntax highlighter.
491 """ Set the style for the syntax highlighter.
492 """
492 """
493 if self.syntax_style:
493 if self.syntax_style:
494 self._highlighter.set_style(self.syntax_style)
494 self._highlighter.set_style(self.syntax_style)
495 else:
495 else:
496 self._highlighter.set_style_sheet(self.style_sheet)
496 self._highlighter.set_style_sheet(self.style_sheet)
497
497
@@ -1,273 +1,275 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Imports
5 # Imports
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7
7
8 # Systemm library imports
8 # Systemm library imports
9 from PyQt4 import QtGui
9 from IPython.external.qt import QtGui
10 from pygments.styles import get_all_styles
10 from pygments.styles import get_all_styles
11
11 # Local imports
12 # Local imports
12 from IPython.external.argparse import ArgumentParser
13 from IPython.external.argparse import ArgumentParser
13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 from IPython.frontend.qt.console import styles
17 from IPython.frontend.qt.console import styles
17 from IPython.frontend.qt.kernelmanager import QtKernelManager
18 from IPython.frontend.qt.kernelmanager import QtKernelManager
18
19
19 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
20 # Network Constants
21 # Network Constants
21 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
22
23
23 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
24 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
24
25
25 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
26 # Classes
27 # Classes
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28
29
29 class MainWindow(QtGui.QMainWindow):
30 class MainWindow(QtGui.QMainWindow):
30
31
31 #---------------------------------------------------------------------------
32 #---------------------------------------------------------------------------
32 # 'object' interface
33 # 'object' interface
33 #---------------------------------------------------------------------------
34 #---------------------------------------------------------------------------
34
35
35 def __init__(self, app, frontend, existing=False, may_close=True):
36 def __init__(self, app, frontend, existing=False, may_close=True):
36 """ Create a MainWindow for the specified FrontendWidget.
37 """ Create a MainWindow for the specified FrontendWidget.
37
38
38 The app is passed as an argument to allow for different
39 The app is passed as an argument to allow for different
39 closing behavior depending on whether we are the Kernel's parent.
40 closing behavior depending on whether we are the Kernel's parent.
40
41
41 If existing is True, then this Console does not own the Kernel.
42 If existing is True, then this Console does not own the Kernel.
42
43
43 If may_close is True, then this Console is permitted to close the kernel
44 If may_close is True, then this Console is permitted to close the kernel
44 """
45 """
45 super(MainWindow, self).__init__()
46 super(MainWindow, self).__init__()
46 self._app = app
47 self._app = app
47 self._frontend = frontend
48 self._frontend = frontend
48 self._existing = existing
49 self._existing = existing
49 if existing:
50 if existing:
50 self._may_close = may_close
51 self._may_close = may_close
51 else:
52 else:
52 self._may_close = True
53 self._may_close = True
53 self._frontend.exit_requested.connect(self.close)
54 self._frontend.exit_requested.connect(self.close)
54 self.setCentralWidget(frontend)
55 self.setCentralWidget(frontend)
55
56
56 #---------------------------------------------------------------------------
57 #---------------------------------------------------------------------------
57 # QWidget interface
58 # QWidget interface
58 #---------------------------------------------------------------------------
59 #---------------------------------------------------------------------------
59
60
60 def closeEvent(self, event):
61 def closeEvent(self, event):
61 """ Close the window and the kernel (if necessary).
62 """ Close the window and the kernel (if necessary).
62
63
63 This will prompt the user if they are finished with the kernel, and if
64 This will prompt the user if they are finished with the kernel, and if
64 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
65 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
65 it closes without prompt.
66 it closes without prompt.
66 """
67 """
67 keepkernel = None #Use the prompt by default
68 keepkernel = None #Use the prompt by default
68 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
69 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
69 keepkernel = self._frontend._keep_kernel_on_exit
70 keepkernel = self._frontend._keep_kernel_on_exit
70
71
71 kernel_manager = self._frontend.kernel_manager
72 kernel_manager = self._frontend.kernel_manager
72
73
73 if keepkernel is None: #show prompt
74 if keepkernel is None: #show prompt
74 if kernel_manager and kernel_manager.channels_running:
75 if kernel_manager and kernel_manager.channels_running:
75 title = self.window().windowTitle()
76 title = self.window().windowTitle()
76 cancel = QtGui.QMessageBox.Cancel
77 cancel = QtGui.QMessageBox.Cancel
77 okay = QtGui.QMessageBox.Ok
78 okay = QtGui.QMessageBox.Ok
78 if self._may_close:
79 if self._may_close:
79 msg = "You are closing this Console window."
80 msg = "You are closing this Console window."
80 info = "Would you like to quit the Kernel and all attached Consoles as well?"
81 info = "Would you like to quit the Kernel and all attached Consoles as well?"
81 justthis = QtGui.QPushButton("&No, just this Console", self)
82 justthis = QtGui.QPushButton("&No, just this Console", self)
82 justthis.setShortcut('N')
83 justthis.setShortcut('N')
83 closeall = QtGui.QPushButton("&Yes, quit everything", self)
84 closeall = QtGui.QPushButton("&Yes, quit everything", self)
84 closeall.setShortcut('Y')
85 closeall.setShortcut('Y')
85 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
86 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
87 title, msg)
86 box.setInformativeText(info)
88 box.setInformativeText(info)
87 box.addButton(cancel)
89 box.addButton(cancel)
88 box.addButton(justthis, QtGui.QMessageBox.NoRole)
90 box.addButton(justthis, QtGui.QMessageBox.NoRole)
89 box.addButton(closeall, QtGui.QMessageBox.YesRole)
91 box.addButton(closeall, QtGui.QMessageBox.YesRole)
90 box.setDefaultButton(closeall)
92 box.setDefaultButton(closeall)
91 box.setEscapeButton(cancel)
93 box.setEscapeButton(cancel)
92 reply = box.exec_()
94 reply = box.exec_()
93 if reply == 1: # close All
95 if reply == 1: # close All
94 kernel_manager.shutdown_kernel()
96 kernel_manager.shutdown_kernel()
95 #kernel_manager.stop_channels()
97 #kernel_manager.stop_channels()
96 event.accept()
98 event.accept()
97 elif reply == 0: # close Console
99 elif reply == 0: # close Console
98 if not self._existing:
100 if not self._existing:
99 # Have kernel: don't quit, just close the window
101 # Have kernel: don't quit, just close the window
100 self._app.setQuitOnLastWindowClosed(False)
102 self._app.setQuitOnLastWindowClosed(False)
101 self.deleteLater()
103 self.deleteLater()
102 event.accept()
104 event.accept()
103 else:
105 else:
104 event.ignore()
106 event.ignore()
105 else:
107 else:
106 reply = QtGui.QMessageBox.question(self, title,
108 reply = QtGui.QMessageBox.question(self, title,
107 "Are you sure you want to close this Console?"+
109 "Are you sure you want to close this Console?"+
108 "\nThe Kernel and other Consoles will remain active.",
110 "\nThe Kernel and other Consoles will remain active.",
109 okay|cancel,
111 okay|cancel,
110 defaultButton=okay
112 defaultButton=okay
111 )
113 )
112 if reply == okay:
114 if reply == okay:
113 event.accept()
115 event.accept()
114 else:
116 else:
115 event.ignore()
117 event.ignore()
116 elif keepkernel: #close console but leave kernel running (no prompt)
118 elif keepkernel: #close console but leave kernel running (no prompt)
117 if kernel_manager and kernel_manager.channels_running:
119 if kernel_manager and kernel_manager.channels_running:
118 if not self._existing:
120 if not self._existing:
119 # I have the kernel: don't quit, just close the window
121 # I have the kernel: don't quit, just close the window
120 self._app.setQuitOnLastWindowClosed(False)
122 self._app.setQuitOnLastWindowClosed(False)
121 event.accept()
123 event.accept()
122 else: #close console and kernel (no prompt)
124 else: #close console and kernel (no prompt)
123 if kernel_manager and kernel_manager.channels_running:
125 if kernel_manager and kernel_manager.channels_running:
124 kernel_manager.shutdown_kernel()
126 kernel_manager.shutdown_kernel()
125 event.accept()
127 event.accept()
126
128
127 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
128 # Main entry point
130 # Main entry point
129 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
130
132
131 def main():
133 def main():
132 """ Entry point for application.
134 """ Entry point for application.
133 """
135 """
134 # Parse command line arguments.
136 # Parse command line arguments.
135 parser = ArgumentParser()
137 parser = ArgumentParser()
136 kgroup = parser.add_argument_group('kernel options')
138 kgroup = parser.add_argument_group('kernel options')
137 kgroup.add_argument('-e', '--existing', action='store_true',
139 kgroup.add_argument('-e', '--existing', action='store_true',
138 help='connect to an existing kernel')
140 help='connect to an existing kernel')
139 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
141 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
140 help=\
142 help=\
141 "set the kernel\'s IP address [default localhost].\
143 "set the kernel\'s IP address [default localhost].\
142 If the IP address is something other than localhost, then \
144 If the IP address is something other than localhost, then \
143 Consoles on other machines will be able to connect\
145 Consoles on other machines will be able to connect\
144 to the Kernel, so be careful!")
146 to the Kernel, so be careful!")
145 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
147 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
146 help='set the XREQ channel port [default random]')
148 help='set the XREQ channel port [default random]')
147 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
149 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
148 help='set the SUB channel port [default random]')
150 help='set the SUB channel port [default random]')
149 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
151 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
150 help='set the REP channel port [default random]')
152 help='set the REP channel port [default random]')
151 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
153 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
152 help='set the heartbeat port [default random]')
154 help='set the heartbeat port [default random]')
153
155
154 egroup = kgroup.add_mutually_exclusive_group()
156 egroup = kgroup.add_mutually_exclusive_group()
155 egroup.add_argument('--pure', action='store_true', help = \
157 egroup.add_argument('--pure', action='store_true', help = \
156 'use a pure Python kernel instead of an IPython kernel')
158 'use a pure Python kernel instead of an IPython kernel')
157 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
159 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
158 const='auto', help = \
160 const='auto', help = \
159 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
161 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
160 given, the GUI backend is matplotlib's, otherwise use one of: \
162 given, the GUI backend is matplotlib's, otherwise use one of: \
161 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
163 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
162
164
163 wgroup = parser.add_argument_group('widget options')
165 wgroup = parser.add_argument_group('widget options')
164 wgroup.add_argument('--paging', type=str, default='inside',
166 wgroup.add_argument('--paging', type=str, default='inside',
165 choices = ['inside', 'hsplit', 'vsplit', 'none'],
167 choices = ['inside', 'hsplit', 'vsplit', 'none'],
166 help='set the paging style [default inside]')
168 help='set the paging style [default inside]')
167 wgroup.add_argument('--rich', action='store_true',
169 wgroup.add_argument('--rich', action='store_true',
168 help='enable rich text support')
170 help='enable rich text support')
169 wgroup.add_argument('--gui-completion', action='store_true',
171 wgroup.add_argument('--gui-completion', action='store_true',
170 help='use a GUI widget for tab completion')
172 help='use a GUI widget for tab completion')
171 wgroup.add_argument('--style', type=str,
173 wgroup.add_argument('--style', type=str,
172 choices = list(get_all_styles()),
174 choices = list(get_all_styles()),
173 help='specify a pygments style for by name.')
175 help='specify a pygments style for by name.')
174 wgroup.add_argument('--stylesheet', type=str,
176 wgroup.add_argument('--stylesheet', type=str,
175 help="path to a custom CSS stylesheet.")
177 help="path to a custom CSS stylesheet.")
176 wgroup.add_argument('--colors', type=str,
178 wgroup.add_argument('--colors', type=str,
177 help="Set the color scheme (LightBG,Linux,NoColor). This is guessed\
179 help="Set the color scheme (LightBG,Linux,NoColor). This is guessed\
178 based on the pygments style if not set.")
180 based on the pygments style if not set.")
179
181
180 args = parser.parse_args()
182 args = parser.parse_args()
181
183
182 # parse the colors arg down to current known labels
184 # parse the colors arg down to current known labels
183 if args.colors:
185 if args.colors:
184 colors=args.colors.lower()
186 colors=args.colors.lower()
185 if colors in ('lightbg', 'light'):
187 if colors in ('lightbg', 'light'):
186 colors='lightbg'
188 colors='lightbg'
187 elif colors in ('dark', 'linux'):
189 elif colors in ('dark', 'linux'):
188 colors='linux'
190 colors='linux'
189 else:
191 else:
190 colors='nocolor'
192 colors='nocolor'
191 elif args.style:
193 elif args.style:
192 if args.style=='bw':
194 if args.style=='bw':
193 colors='nocolor'
195 colors='nocolor'
194 elif styles.dark_style(args.style):
196 elif styles.dark_style(args.style):
195 colors='linux'
197 colors='linux'
196 else:
198 else:
197 colors='lightbg'
199 colors='lightbg'
198 else:
200 else:
199 colors=None
201 colors=None
200
202
201 # Don't let Qt or ZMQ swallow KeyboardInterupts.
203 # Don't let Qt or ZMQ swallow KeyboardInterupts.
202 import signal
204 import signal
203 signal.signal(signal.SIGINT, signal.SIG_DFL)
205 signal.signal(signal.SIGINT, signal.SIG_DFL)
204
206
205 # Create a KernelManager and start a kernel.
207 # Create a KernelManager and start a kernel.
206 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
208 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
207 sub_address=(args.ip, args.sub),
209 sub_address=(args.ip, args.sub),
208 rep_address=(args.ip, args.rep),
210 rep_address=(args.ip, args.rep),
209 hb_address=(args.ip, args.hb))
211 hb_address=(args.ip, args.hb))
210 if not args.existing:
212 if not args.existing:
211 # if not args.ip in LOCAL_IPS+ALL_ALIAS:
213 # if not args.ip in LOCAL_IPS+ALL_ALIAS:
212 # raise ValueError("Must bind a local ip, such as: %s"%LOCAL_IPS)
214 # raise ValueError("Must bind a local ip, such as: %s"%LOCAL_IPS)
213
215
214 kwargs = dict(ip=args.ip)
216 kwargs = dict(ip=args.ip)
215 if args.pure:
217 if args.pure:
216 kwargs['ipython']=False
218 kwargs['ipython']=False
217 else:
219 else:
218 kwargs['colors']=colors
220 kwargs['colors']=colors
219 if args.pylab:
221 if args.pylab:
220 kwargs['pylab']=args.pylab
222 kwargs['pylab']=args.pylab
221
223
222 kernel_manager.start_kernel(**kwargs)
224 kernel_manager.start_kernel(**kwargs)
223 kernel_manager.start_channels()
225 kernel_manager.start_channels()
224
226
225 local_kernel = (not args.existing) or args.ip in LOCAL_IPS
227 local_kernel = (not args.existing) or args.ip in LOCAL_IPS
226 # Create the widget.
228 # Create the widget.
227 app = QtGui.QApplication([])
229 app = QtGui.QApplication([])
228 if args.pure:
230 if args.pure:
229 kind = 'rich' if args.rich else 'plain'
231 kind = 'rich' if args.rich else 'plain'
230 widget = FrontendWidget(kind=kind, paging=args.paging, local_kernel=local_kernel)
232 widget = FrontendWidget(kind=kind, paging=args.paging, local_kernel=local_kernel)
231 elif args.rich or args.pylab:
233 elif args.rich or args.pylab:
232 widget = RichIPythonWidget(paging=args.paging, local_kernel=local_kernel)
234 widget = RichIPythonWidget(paging=args.paging, local_kernel=local_kernel)
233 else:
235 else:
234 widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
236 widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
235 widget.gui_completion = args.gui_completion
237 widget.gui_completion = args.gui_completion
236 widget.kernel_manager = kernel_manager
238 widget.kernel_manager = kernel_manager
237
239
238 # configure the style:
240 # configure the style:
239 if not args.pure: # only IPythonWidget supports styles
241 if not args.pure: # only IPythonWidget supports styles
240 if args.style:
242 if args.style:
241 widget.syntax_style = args.style
243 widget.syntax_style = args.style
242 widget.style_sheet = styles.sheet_from_template(args.style, colors)
244 widget.style_sheet = styles.sheet_from_template(args.style, colors)
243 widget._syntax_style_changed()
245 widget._syntax_style_changed()
244 widget._style_sheet_changed()
246 widget._style_sheet_changed()
245 elif colors:
247 elif colors:
246 # use a default style
248 # use a default style
247 widget.set_default_style(colors=colors)
249 widget.set_default_style(colors=colors)
248 else:
250 else:
249 # this is redundant for now, but allows the widget's
251 # this is redundant for now, but allows the widget's
250 # defaults to change
252 # defaults to change
251 widget.set_default_style()
253 widget.set_default_style()
252
254
253 if args.stylesheet:
255 if args.stylesheet:
254 # we got an expicit stylesheet
256 # we got an expicit stylesheet
255 if os.path.isfile(args.stylesheet):
257 if os.path.isfile(args.stylesheet):
256 with open(args.stylesheet) as f:
258 with open(args.stylesheet) as f:
257 sheet = f.read()
259 sheet = f.read()
258 widget.style_sheet = sheet
260 widget.style_sheet = sheet
259 widget._style_sheet_changed()
261 widget._style_sheet_changed()
260 else:
262 else:
261 raise IOError("Stylesheet %r not found."%args.stylesheet)
263 raise IOError("Stylesheet %r not found."%args.stylesheet)
262
264
263 # Create the main window.
265 # Create the main window.
264 window = MainWindow(app, widget, args.existing, may_close=local_kernel)
266 window = MainWindow(app, widget, args.existing, may_close=local_kernel)
265 window.setWindowTitle('Python' if args.pure else 'IPython')
267 window.setWindowTitle('Python' if args.pure else 'IPython')
266 window.show()
268 window.show()
267
269
268 # Start the application main loop.
270 # Start the application main loop.
269 app.exec_()
271 app.exec_()
270
272
271
273
272 if __name__ == '__main__':
274 if __name__ == '__main__':
273 main()
275 main()
@@ -1,226 +1,224 b''
1 # System library imports.
1 # System library imports.
2 from PyQt4 import QtGui
2 from IPython.external.qt import QtGui
3 from pygments.formatters.html import HtmlFormatter
3 from pygments.formatters.html import HtmlFormatter
4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
5 from pygments.lexers import PythonLexer
5 from pygments.lexers import PythonLexer
6 from pygments.styles import get_style_by_name
6 from pygments.styles import get_style_by_name
7
7
8
8
9 def get_tokens_unprocessed(self, text, stack=('root',)):
9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 """ Split ``text`` into (tokentype, text) pairs.
10 """ Split ``text`` into (tokentype, text) pairs.
11
11
12 Monkeypatched to store the final stack on the object itself.
12 Monkeypatched to store the final stack on the object itself.
13 """
13 """
14 pos = 0
14 pos = 0
15 tokendefs = self._tokens
15 tokendefs = self._tokens
16 if hasattr(self, '_saved_state_stack'):
16 if hasattr(self, '_saved_state_stack'):
17 statestack = list(self._saved_state_stack)
17 statestack = list(self._saved_state_stack)
18 else:
18 else:
19 statestack = list(stack)
19 statestack = list(stack)
20 statetokens = tokendefs[statestack[-1]]
20 statetokens = tokendefs[statestack[-1]]
21 while 1:
21 while 1:
22 for rexmatch, action, new_state in statetokens:
22 for rexmatch, action, new_state in statetokens:
23 m = rexmatch(text, pos)
23 m = rexmatch(text, pos)
24 if m:
24 if m:
25 if type(action) is _TokenType:
25 if type(action) is _TokenType:
26 yield pos, action, m.group()
26 yield pos, action, m.group()
27 else:
27 else:
28 for item in action(self, m):
28 for item in action(self, m):
29 yield item
29 yield item
30 pos = m.end()
30 pos = m.end()
31 if new_state is not None:
31 if new_state is not None:
32 # state transition
32 # state transition
33 if isinstance(new_state, tuple):
33 if isinstance(new_state, tuple):
34 for state in new_state:
34 for state in new_state:
35 if state == '#pop':
35 if state == '#pop':
36 statestack.pop()
36 statestack.pop()
37 elif state == '#push':
37 elif state == '#push':
38 statestack.append(statestack[-1])
38 statestack.append(statestack[-1])
39 else:
39 else:
40 statestack.append(state)
40 statestack.append(state)
41 elif isinstance(new_state, int):
41 elif isinstance(new_state, int):
42 # pop
42 # pop
43 del statestack[new_state:]
43 del statestack[new_state:]
44 elif new_state == '#push':
44 elif new_state == '#push':
45 statestack.append(statestack[-1])
45 statestack.append(statestack[-1])
46 else:
46 else:
47 assert False, "wrong state def: %r" % new_state
47 assert False, "wrong state def: %r" % new_state
48 statetokens = tokendefs[statestack[-1]]
48 statetokens = tokendefs[statestack[-1]]
49 break
49 break
50 else:
50 else:
51 try:
51 try:
52 if text[pos] == '\n':
52 if text[pos] == '\n':
53 # at EOL, reset state to "root"
53 # at EOL, reset state to "root"
54 pos += 1
54 pos += 1
55 statestack = ['root']
55 statestack = ['root']
56 statetokens = tokendefs['root']
56 statetokens = tokendefs['root']
57 yield pos, Text, u'\n'
57 yield pos, Text, u'\n'
58 continue
58 continue
59 yield pos, Error, text[pos]
59 yield pos, Error, text[pos]
60 pos += 1
60 pos += 1
61 except IndexError:
61 except IndexError:
62 break
62 break
63 self._saved_state_stack = list(statestack)
63 self._saved_state_stack = list(statestack)
64
64
65 # Monkeypatch!
65 # Monkeypatch!
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67
67
68
68
69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
70 """ Storage for the user data associated with each line.
70 """ Storage for the user data associated with each line.
71 """
71 """
72
72
73 syntax_stack = ('root',)
73 syntax_stack = ('root',)
74
74
75 def __init__(self, **kwds):
75 def __init__(self, **kwds):
76 for key, value in kwds.iteritems():
76 for key, value in kwds.iteritems():
77 setattr(self, key, value)
77 setattr(self, key, value)
78 QtGui.QTextBlockUserData.__init__(self)
78 QtGui.QTextBlockUserData.__init__(self)
79
79
80 def __repr__(self):
80 def __repr__(self):
81 attrs = ['syntax_stack']
81 attrs = ['syntax_stack']
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 for attr in attrs ])
83 for attr in attrs ])
84 return 'PygmentsBlockUserData(%s)' % kwds
84 return 'PygmentsBlockUserData(%s)' % kwds
85
85
86
86
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 """ Syntax highlighter that uses Pygments for parsing. """
88 """ Syntax highlighter that uses Pygments for parsing. """
89
89
90 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
91 # 'QSyntaxHighlighter' interface
91 # 'QSyntaxHighlighter' interface
92 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
93
93
94 def __init__(self, parent, lexer=None):
94 def __init__(self, parent, lexer=None):
95 super(PygmentsHighlighter, self).__init__(parent)
95 super(PygmentsHighlighter, self).__init__(parent)
96
96
97 self._document = QtGui.QTextDocument()
97 self._document = QtGui.QTextDocument()
98 self._formatter = HtmlFormatter(nowrap=True)
98 self._formatter = HtmlFormatter(nowrap=True)
99 self._lexer = lexer if lexer else PythonLexer()
99 self._lexer = lexer if lexer else PythonLexer()
100 self.set_style('default')
100 self.set_style('default')
101
101
102 def highlightBlock(self, qstring):
102 def highlightBlock(self, string):
103 """ Highlight a block of text.
103 """ Highlight a block of text.
104 """
104 """
105 qstring = unicode(qstring)
106 prev_data = self.currentBlock().previous().userData()
105 prev_data = self.currentBlock().previous().userData()
107
108 if prev_data is not None:
106 if prev_data is not None:
109 self._lexer._saved_state_stack = prev_data.syntax_stack
107 self._lexer._saved_state_stack = prev_data.syntax_stack
110 elif hasattr(self._lexer, '_saved_state_stack'):
108 elif hasattr(self._lexer, '_saved_state_stack'):
111 del self._lexer._saved_state_stack
109 del self._lexer._saved_state_stack
112
110
113 # Lex the text using Pygments
111 # Lex the text using Pygments
114 index = 0
112 index = 0
115 for token, text in self._lexer.get_tokens(qstring):
113 for token, text in self._lexer.get_tokens(string):
116 length = len(text)
114 length = len(text)
117 self.setFormat(index, length, self._get_format(token))
115 self.setFormat(index, length, self._get_format(token))
118 index += length
116 index += length
119
117
120 if hasattr(self._lexer, '_saved_state_stack'):
118 if hasattr(self._lexer, '_saved_state_stack'):
121 data = PygmentsBlockUserData(
119 data = PygmentsBlockUserData(
122 syntax_stack=self._lexer._saved_state_stack)
120 syntax_stack=self._lexer._saved_state_stack)
123 self.currentBlock().setUserData(data)
121 self.currentBlock().setUserData(data)
124 # Clean up for the next go-round.
122 # Clean up for the next go-round.
125 del self._lexer._saved_state_stack
123 del self._lexer._saved_state_stack
126
124
127 #---------------------------------------------------------------------------
125 #---------------------------------------------------------------------------
128 # 'PygmentsHighlighter' interface
126 # 'PygmentsHighlighter' interface
129 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
130
128
131 def set_style(self, style):
129 def set_style(self, style):
132 """ Sets the style to the specified Pygments style.
130 """ Sets the style to the specified Pygments style.
133 """
131 """
134 if isinstance(style, basestring):
132 if isinstance(style, basestring):
135 style = get_style_by_name(style)
133 style = get_style_by_name(style)
136 self._style = style
134 self._style = style
137 self._clear_caches()
135 self._clear_caches()
138
136
139 def set_style_sheet(self, stylesheet):
137 def set_style_sheet(self, stylesheet):
140 """ Sets a CSS stylesheet. The classes in the stylesheet should
138 """ Sets a CSS stylesheet. The classes in the stylesheet should
141 correspond to those generated by:
139 correspond to those generated by:
142
140
143 pygmentize -S <style> -f html
141 pygmentize -S <style> -f html
144
142
145 Note that 'set_style' and 'set_style_sheet' completely override each
143 Note that 'set_style' and 'set_style_sheet' completely override each
146 other, i.e. they cannot be used in conjunction.
144 other, i.e. they cannot be used in conjunction.
147 """
145 """
148 self._document.setDefaultStyleSheet(stylesheet)
146 self._document.setDefaultStyleSheet(stylesheet)
149 self._style = None
147 self._style = None
150 self._clear_caches()
148 self._clear_caches()
151
149
152 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
153 # Protected interface
151 # Protected interface
154 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
155
153
156 def _clear_caches(self):
154 def _clear_caches(self):
157 """ Clear caches for brushes and formats.
155 """ Clear caches for brushes and formats.
158 """
156 """
159 self._brushes = {}
157 self._brushes = {}
160 self._formats = {}
158 self._formats = {}
161
159
162 def _get_format(self, token):
160 def _get_format(self, token):
163 """ Returns a QTextCharFormat for token or None.
161 """ Returns a QTextCharFormat for token or None.
164 """
162 """
165 if token in self._formats:
163 if token in self._formats:
166 return self._formats[token]
164 return self._formats[token]
167
165
168 if self._style is None:
166 if self._style is None:
169 result = self._get_format_from_document(token, self._document)
167 result = self._get_format_from_document(token, self._document)
170 else:
168 else:
171 result = self._get_format_from_style(token, self._style)
169 result = self._get_format_from_style(token, self._style)
172
170
173 self._formats[token] = result
171 self._formats[token] = result
174 return result
172 return result
175
173
176 def _get_format_from_document(self, token, document):
174 def _get_format_from_document(self, token, document):
177 """ Returns a QTextCharFormat for token by
175 """ Returns a QTextCharFormat for token by
178 """
176 """
179 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
177 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
180 self._document.setHtml(html)
178 self._document.setHtml(html)
181 return QtGui.QTextCursor(self._document).charFormat()
179 return QtGui.QTextCursor(self._document).charFormat()
182
180
183 def _get_format_from_style(self, token, style):
181 def _get_format_from_style(self, token, style):
184 """ Returns a QTextCharFormat for token by reading a Pygments style.
182 """ Returns a QTextCharFormat for token by reading a Pygments style.
185 """
183 """
186 result = QtGui.QTextCharFormat()
184 result = QtGui.QTextCharFormat()
187 for key, value in style.style_for_token(token).items():
185 for key, value in style.style_for_token(token).items():
188 if value:
186 if value:
189 if key == 'color':
187 if key == 'color':
190 result.setForeground(self._get_brush(value))
188 result.setForeground(self._get_brush(value))
191 elif key == 'bgcolor':
189 elif key == 'bgcolor':
192 result.setBackground(self._get_brush(value))
190 result.setBackground(self._get_brush(value))
193 elif key == 'bold':
191 elif key == 'bold':
194 result.setFontWeight(QtGui.QFont.Bold)
192 result.setFontWeight(QtGui.QFont.Bold)
195 elif key == 'italic':
193 elif key == 'italic':
196 result.setFontItalic(True)
194 result.setFontItalic(True)
197 elif key == 'underline':
195 elif key == 'underline':
198 result.setUnderlineStyle(
196 result.setUnderlineStyle(
199 QtGui.QTextCharFormat.SingleUnderline)
197 QtGui.QTextCharFormat.SingleUnderline)
200 elif key == 'sans':
198 elif key == 'sans':
201 result.setFontStyleHint(QtGui.QFont.SansSerif)
199 result.setFontStyleHint(QtGui.QFont.SansSerif)
202 elif key == 'roman':
200 elif key == 'roman':
203 result.setFontStyleHint(QtGui.QFont.Times)
201 result.setFontStyleHint(QtGui.QFont.Times)
204 elif key == 'mono':
202 elif key == 'mono':
205 result.setFontStyleHint(QtGui.QFont.TypeWriter)
203 result.setFontStyleHint(QtGui.QFont.TypeWriter)
206 return result
204 return result
207
205
208 def _get_brush(self, color):
206 def _get_brush(self, color):
209 """ Returns a brush for the color.
207 """ Returns a brush for the color.
210 """
208 """
211 result = self._brushes.get(color)
209 result = self._brushes.get(color)
212 if result is None:
210 if result is None:
213 qcolor = self._get_color(color)
211 qcolor = self._get_color(color)
214 result = QtGui.QBrush(qcolor)
212 result = QtGui.QBrush(qcolor)
215 self._brushes[color] = result
213 self._brushes[color] = result
216 return result
214 return result
217
215
218 def _get_color(self, color):
216 def _get_color(self, color):
219 """ Returns a QColor built from a Pygments color string.
217 """ Returns a QColor built from a Pygments color string.
220 """
218 """
221 qcolor = QtGui.QColor()
219 qcolor = QtGui.QColor()
222 qcolor.setRgb(int(color[:2], base=16),
220 qcolor.setRgb(int(color[:2], base=16),
223 int(color[2:4], base=16),
221 int(color[2:4], base=16),
224 int(color[4:6], base=16))
222 int(color[4:6], base=16))
225 return qcolor
223 return qcolor
226
224
@@ -1,271 +1,273 b''
1 # System library imports
1 # Standard libary imports.
2 from base64 import decodestring
2 import os
3 import os
3 import re
4 import re
4 from base64 import decodestring
5
5 from PyQt4 import QtCore, QtGui
6 # System libary imports.
7 from IPython.external.qt import QtCore, QtGui
6
8
7 # Local imports
9 # Local imports
8 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
9 from ipython_widget import IPythonWidget
11 from ipython_widget import IPythonWidget
10
12
11
13
12 class RichIPythonWidget(IPythonWidget):
14 class RichIPythonWidget(IPythonWidget):
13 """ An IPythonWidget that supports rich text, including lists, images, and
15 """ An IPythonWidget that supports rich text, including lists, images, and
14 tables. Note that raw performance will be reduced compared to the plain
16 tables. Note that raw performance will be reduced compared to the plain
15 text version.
17 text version.
16 """
18 """
17
19
18 # RichIPythonWidget protected class variables.
20 # RichIPythonWidget protected class variables.
19 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
20 _svg_text_format_property = 1
22 _svg_text_format_property = 1
21
23
22 #---------------------------------------------------------------------------
24 #---------------------------------------------------------------------------
23 # 'object' interface
25 # 'object' interface
24 #---------------------------------------------------------------------------
26 #---------------------------------------------------------------------------
25
27
26 def __init__(self, *args, **kw):
28 def __init__(self, *args, **kw):
27 """ Create a RichIPythonWidget.
29 """ Create a RichIPythonWidget.
28 """
30 """
29 kw['kind'] = 'rich'
31 kw['kind'] = 'rich'
30 super(RichIPythonWidget, self).__init__(*args, **kw)
32 super(RichIPythonWidget, self).__init__(*args, **kw)
31 # Dictionary for resolving Qt names to images when
33 # Dictionary for resolving Qt names to images when
32 # generating XHTML output
34 # generating XHTML output
33 self._name_to_svg = {}
35 self._name_to_svg = {}
34
36
35 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
36 # 'ConsoleWidget' protected interface
38 # 'ConsoleWidget' protected interface
37 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
38
40
39 def _context_menu_make(self, pos):
41 def _context_menu_make(self, pos):
40 """ Reimplemented to return a custom context menu for images.
42 """ Reimplemented to return a custom context menu for images.
41 """
43 """
42 format = self._control.cursorForPosition(pos).charFormat()
44 format = self._control.cursorForPosition(pos).charFormat()
43 name = format.stringProperty(QtGui.QTextFormat.ImageName)
45 name = format.stringProperty(QtGui.QTextFormat.ImageName)
44 if name.isEmpty():
46 if name.isEmpty():
45 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
47 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
46 else:
48 else:
47 menu = QtGui.QMenu()
49 menu = QtGui.QMenu()
48
50
49 menu.addAction('Copy Image', lambda: self._copy_image(name))
51 menu.addAction('Copy Image', lambda: self._copy_image(name))
50 menu.addAction('Save Image As...', lambda: self._save_image(name))
52 menu.addAction('Save Image As...', lambda: self._save_image(name))
51 menu.addSeparator()
53 menu.addSeparator()
52
54
53 svg = format.stringProperty(self._svg_text_format_property)
55 svg = format.stringProperty(self._svg_text_format_property)
54 if not svg.isEmpty():
56 if not svg.isEmpty():
55 menu.addSeparator()
57 menu.addSeparator()
56 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
58 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
57 menu.addAction('Save SVG As...',
59 menu.addAction('Save SVG As...',
58 lambda: save_svg(svg, self._control))
60 lambda: save_svg(svg, self._control))
59 return menu
61 return menu
60
62
61 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
62 # 'BaseFrontendMixin' abstract interface
64 # 'BaseFrontendMixin' abstract interface
63 #---------------------------------------------------------------------------
65 #---------------------------------------------------------------------------
64
66
65 def _handle_pyout(self, msg):
67 def _handle_pyout(self, msg):
66 """ Overridden to handle rich data types, like SVG.
68 """ Overridden to handle rich data types, like SVG.
67 """
69 """
68 if not self._hidden and self._is_from_this_session(msg):
70 if not self._hidden and self._is_from_this_session(msg):
69 content = msg['content']
71 content = msg['content']
70 prompt_number = content['execution_count']
72 prompt_number = content['execution_count']
71 data = content['data']
73 data = content['data']
72 if data.has_key('image/svg+xml'):
74 if data.has_key('image/svg+xml'):
73 self._append_plain_text(self.output_sep)
75 self._append_plain_text(self.output_sep)
74 self._append_html(self._make_out_prompt(prompt_number))
76 self._append_html(self._make_out_prompt(prompt_number))
75 # TODO: try/except this call.
77 # TODO: try/except this call.
76 self._append_svg(data['image/svg+xml'])
78 self._append_svg(data['image/svg+xml'])
77 self._append_html(self.output_sep2)
79 self._append_html(self.output_sep2)
78 elif data.has_key('image/png'):
80 elif data.has_key('image/png'):
79 self._append_plain_text(self.output_sep)
81 self._append_plain_text(self.output_sep)
80 self._append_html(self._make_out_prompt(prompt_number))
82 self._append_html(self._make_out_prompt(prompt_number))
81 # This helps the output to look nice.
83 # This helps the output to look nice.
82 self._append_plain_text('\n')
84 self._append_plain_text('\n')
83 # TODO: try/except these calls
85 # TODO: try/except these calls
84 png = decodestring(data['image/png'])
86 png = decodestring(data['image/png'])
85 self._append_png(png)
87 self._append_png(png)
86 self._append_html(self.output_sep2)
88 self._append_html(self.output_sep2)
87 else:
89 else:
88 # Default back to the plain text representation.
90 # Default back to the plain text representation.
89 return super(RichIPythonWidget, self)._handle_pyout(msg)
91 return super(RichIPythonWidget, self)._handle_pyout(msg)
90
92
91 def _handle_display_data(self, msg):
93 def _handle_display_data(self, msg):
92 """ Overridden to handle rich data types, like SVG.
94 """ Overridden to handle rich data types, like SVG.
93 """
95 """
94 if not self._hidden and self._is_from_this_session(msg):
96 if not self._hidden and self._is_from_this_session(msg):
95 source = msg['content']['source']
97 source = msg['content']['source']
96 data = msg['content']['data']
98 data = msg['content']['data']
97 metadata = msg['content']['metadata']
99 metadata = msg['content']['metadata']
98 # Try to use the svg or html representations.
100 # Try to use the svg or html representations.
99 # FIXME: Is this the right ordering of things to try?
101 # FIXME: Is this the right ordering of things to try?
100 if data.has_key('image/svg+xml'):
102 if data.has_key('image/svg+xml'):
101 svg = data['image/svg+xml']
103 svg = data['image/svg+xml']
102 # TODO: try/except this call.
104 # TODO: try/except this call.
103 self._append_svg(svg)
105 self._append_svg(svg)
104 elif data.has_key('image/png'):
106 elif data.has_key('image/png'):
105 # TODO: try/except these calls
107 # TODO: try/except these calls
106 # PNG data is base64 encoded as it passes over the network
108 # PNG data is base64 encoded as it passes over the network
107 # in a JSON structure so we decode it.
109 # in a JSON structure so we decode it.
108 png = decodestring(data['image/png'])
110 png = decodestring(data['image/png'])
109 self._append_png(png)
111 self._append_png(png)
110 else:
112 else:
111 # Default back to the plain text representation.
113 # Default back to the plain text representation.
112 return super(RichIPythonWidget, self)._handle_display_data(msg)
114 return super(RichIPythonWidget, self)._handle_display_data(msg)
113
115
114 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
115 # 'FrontendWidget' protected interface
117 # 'FrontendWidget' protected interface
116 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
117
119
118 def _process_execute_payload(self, item):
120 def _process_execute_payload(self, item):
119 """ Reimplemented to handle matplotlib plot payloads.
121 """ Reimplemented to handle matplotlib plot payloads.
120 """
122 """
121 # TODO: remove this as all plot data is coming back through the
123 # TODO: remove this as all plot data is coming back through the
122 # display_data message type.
124 # display_data message type.
123 if item['source'] == self._payload_source_plot:
125 if item['source'] == self._payload_source_plot:
124 if item['format'] == 'svg':
126 if item['format'] == 'svg':
125 svg = item['data']
127 svg = item['data']
126 self._append_svg(svg)
128 self._append_svg(svg)
127 return True
129 return True
128 else:
130 else:
129 # Add other plot formats here!
131 # Add other plot formats here!
130 return False
132 return False
131 else:
133 else:
132 return super(RichIPythonWidget, self)._process_execute_payload(item)
134 return super(RichIPythonWidget, self)._process_execute_payload(item)
133
135
134 #---------------------------------------------------------------------------
136 #---------------------------------------------------------------------------
135 # 'RichIPythonWidget' protected interface
137 # 'RichIPythonWidget' protected interface
136 #---------------------------------------------------------------------------
138 #---------------------------------------------------------------------------
137
139
138 def _append_svg(self, svg):
140 def _append_svg(self, svg):
139 """ Append raw svg data to the widget.
141 """ Append raw svg data to the widget.
140 """
142 """
141 try:
143 try:
142 image = svg_to_image(svg)
144 image = svg_to_image(svg)
143 except ValueError:
145 except ValueError:
144 self._append_plain_text('Received invalid plot data.')
146 self._append_plain_text('Received invalid plot data.')
145 else:
147 else:
146 format = self._add_image(image)
148 format = self._add_image(image)
147 self._name_to_svg[str(format.name())] = svg
149 self._name_to_svg[str(format.name())] = svg
148 format.setProperty(self._svg_text_format_property, svg)
150 format.setProperty(self._svg_text_format_property, svg)
149 cursor = self._get_end_cursor()
151 cursor = self._get_end_cursor()
150 cursor.insertBlock()
152 cursor.insertBlock()
151 cursor.insertImage(format)
153 cursor.insertImage(format)
152 cursor.insertBlock()
154 cursor.insertBlock()
153
155
154 def _append_png(self, png):
156 def _append_png(self, png):
155 """ Append raw svg data to the widget.
157 """ Append raw svg data to the widget.
156 """
158 """
157 try:
159 try:
158 image = QtGui.QImage()
160 image = QtGui.QImage()
159 image.loadFromData(png, 'PNG')
161 image.loadFromData(png, 'PNG')
160 except ValueError:
162 except ValueError:
161 self._append_plain_text('Received invalid plot data.')
163 self._append_plain_text('Received invalid plot data.')
162 else:
164 else:
163 format = self._add_image(image)
165 format = self._add_image(image)
164 cursor = self._get_end_cursor()
166 cursor = self._get_end_cursor()
165 cursor.insertBlock()
167 cursor.insertBlock()
166 cursor.insertImage(format)
168 cursor.insertImage(format)
167 cursor.insertBlock()
169 cursor.insertBlock()
168
170
169 def _add_image(self, image):
171 def _add_image(self, image):
170 """ Adds the specified QImage to the document and returns a
172 """ Adds the specified QImage to the document and returns a
171 QTextImageFormat that references it.
173 QTextImageFormat that references it.
172 """
174 """
173 document = self._control.document()
175 document = self._control.document()
174 name = QtCore.QString.number(image.cacheKey())
176 name = str(image.cacheKey())
175 document.addResource(QtGui.QTextDocument.ImageResource,
177 document.addResource(QtGui.QTextDocument.ImageResource,
176 QtCore.QUrl(name), image)
178 QtCore.QUrl(name), image)
177 format = QtGui.QTextImageFormat()
179 format = QtGui.QTextImageFormat()
178 format.setName(name)
180 format.setName(name)
179 return format
181 return format
180
182
181 def _copy_image(self, name):
183 def _copy_image(self, name):
182 """ Copies the ImageResource with 'name' to the clipboard.
184 """ Copies the ImageResource with 'name' to the clipboard.
183 """
185 """
184 image = self._get_image(name)
186 image = self._get_image(name)
185 QtGui.QApplication.clipboard().setImage(image)
187 QtGui.QApplication.clipboard().setImage(image)
186
188
187 def _get_image(self, name):
189 def _get_image(self, name):
188 """ Returns the QImage stored as the ImageResource with 'name'.
190 """ Returns the QImage stored as the ImageResource with 'name'.
189 """
191 """
190 document = self._control.document()
192 document = self._control.document()
191 variant = document.resource(QtGui.QTextDocument.ImageResource,
193 variant = document.resource(QtGui.QTextDocument.ImageResource,
192 QtCore.QUrl(name))
194 QtCore.QUrl(name))
193 return variant.toPyObject()
195 return variant.toPyObject()
194
196
195 def _save_image(self, name, format='PNG'):
197 def _save_image(self, name, format='PNG'):
196 """ Shows a save dialog for the ImageResource with 'name'.
198 """ Shows a save dialog for the ImageResource with 'name'.
197 """
199 """
198 dialog = QtGui.QFileDialog(self._control, 'Save Image')
200 dialog = QtGui.QFileDialog(self._control, 'Save Image')
199 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
201 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
200 dialog.setDefaultSuffix(format.lower())
202 dialog.setDefaultSuffix(format.lower())
201 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
203 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
202 if dialog.exec_():
204 if dialog.exec_():
203 filename = dialog.selectedFiles()[0]
205 filename = dialog.selectedFiles()[0]
204 image = self._get_image(name)
206 image = self._get_image(name)
205 image.save(filename, format)
207 image.save(filename, format)
206
208
207 def image_tag(self, match, path = None, format = "png"):
209 def image_tag(self, match, path = None, format = "png"):
208 """ Return (X)HTML mark-up for the image-tag given by match.
210 """ Return (X)HTML mark-up for the image-tag given by match.
209
211
210 Parameters
212 Parameters
211 ----------
213 ----------
212 match : re.SRE_Match
214 match : re.SRE_Match
213 A match to an HTML image tag as exported by Qt, with
215 A match to an HTML image tag as exported by Qt, with
214 match.group("Name") containing the matched image ID.
216 match.group("Name") containing the matched image ID.
215
217
216 path : string|None, optional [default None]
218 path : string|None, optional [default None]
217 If not None, specifies a path to which supporting files
219 If not None, specifies a path to which supporting files
218 may be written (e.g., for linked images).
220 may be written (e.g., for linked images).
219 If None, all images are to be included inline.
221 If None, all images are to be included inline.
220
222
221 format : "png"|"svg", optional [default "png"]
223 format : "png"|"svg", optional [default "png"]
222 Format for returned or referenced images.
224 Format for returned or referenced images.
223
225
224 Subclasses supporting image display should override this
226 Subclasses supporting image display should override this
225 method.
227 method.
226 """
228 """
227
229
228 if(format == "png"):
230 if(format == "png"):
229 try:
231 try:
230 image = self._get_image(match.group("name"))
232 image = self._get_image(match.group("name"))
231 except KeyError:
233 except KeyError:
232 return "<b>Couldn't find image %s</b>" % match.group("name")
234 return "<b>Couldn't find image %s</b>" % match.group("name")
233
235
234 if(path is not None):
236 if(path is not None):
235 if not os.path.exists(path):
237 if not os.path.exists(path):
236 os.mkdir(path)
238 os.mkdir(path)
237 relpath = os.path.basename(path)
239 relpath = os.path.basename(path)
238 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
240 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
239 "PNG")):
241 "PNG")):
240 return '<img src="%s/qt_img%s.png">' % (relpath,
242 return '<img src="%s/qt_img%s.png">' % (relpath,
241 match.group("name"))
243 match.group("name"))
242 else:
244 else:
243 return "<b>Couldn't save image!</b>"
245 return "<b>Couldn't save image!</b>"
244 else:
246 else:
245 ba = QtCore.QByteArray()
247 ba = QtCore.QByteArray()
246 buffer_ = QtCore.QBuffer(ba)
248 buffer_ = QtCore.QBuffer(ba)
247 buffer_.open(QtCore.QIODevice.WriteOnly)
249 buffer_.open(QtCore.QIODevice.WriteOnly)
248 image.save(buffer_, "PNG")
250 image.save(buffer_, "PNG")
249 buffer_.close()
251 buffer_.close()
250 return '<img src="data:image/png;base64,\n%s\n" />' % (
252 return '<img src="data:image/png;base64,\n%s\n" />' % (
251 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
253 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
252
254
253 elif(format == "svg"):
255 elif(format == "svg"):
254 try:
256 try:
255 svg = str(self._name_to_svg[match.group("name")])
257 svg = str(self._name_to_svg[match.group("name")])
256 except KeyError:
258 except KeyError:
257 return "<b>Couldn't find image %s</b>" % match.group("name")
259 return "<b>Couldn't find image %s</b>" % match.group("name")
258
260
259 # Not currently checking path, because it's tricky to find a
261 # Not currently checking path, because it's tricky to find a
260 # cross-browser way to embed external SVG images (e.g., via
262 # cross-browser way to embed external SVG images (e.g., via
261 # object or embed tags).
263 # object or embed tags).
262
264
263 # Chop stand-alone header from matplotlib SVG
265 # Chop stand-alone header from matplotlib SVG
264 offset = svg.find("<svg")
266 offset = svg.find("<svg")
265 assert(offset > -1)
267 assert(offset > -1)
266
268
267 return svg[offset:]
269 return svg[offset:]
268
270
269 else:
271 else:
270 return '<b>Unrecognized image format</b>'
272 return '<b>Unrecognized image format</b>'
271
273
@@ -1,242 +1,242 b''
1 """ Defines a KernelManager that provides signals and slots.
1 """ Defines a KernelManager that provides signals and slots.
2 """
2 """
3
3
4 # System library imports.
4 # System library imports.
5 from PyQt4 import QtCore
5 from IPython.external.qt import QtCore
6
6
7 # IPython imports.
7 # IPython imports.
8 from IPython.utils.traitlets import Type
8 from IPython.utils.traitlets import Type
9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
10 XReqSocketChannel, RepSocketChannel, HBSocketChannel
10 XReqSocketChannel, RepSocketChannel, HBSocketChannel
11 from util import MetaQObjectHasTraits, SuperQObject
11 from util import MetaQObjectHasTraits, SuperQObject
12
12
13
13
14 class SocketChannelQObject(SuperQObject):
14 class SocketChannelQObject(SuperQObject):
15
15
16 # Emitted when the channel is started.
16 # Emitted when the channel is started.
17 started = QtCore.pyqtSignal()
17 started = QtCore.Signal()
18
18
19 # Emitted when the channel is stopped.
19 # Emitted when the channel is stopped.
20 stopped = QtCore.pyqtSignal()
20 stopped = QtCore.Signal()
21
21
22 #---------------------------------------------------------------------------
22 #---------------------------------------------------------------------------
23 # 'ZmqSocketChannel' interface
23 # 'ZmqSocketChannel' interface
24 #---------------------------------------------------------------------------
24 #---------------------------------------------------------------------------
25
25
26 def start(self):
26 def start(self):
27 """ Reimplemented to emit signal.
27 """ Reimplemented to emit signal.
28 """
28 """
29 super(SocketChannelQObject, self).start()
29 super(SocketChannelQObject, self).start()
30 self.started.emit()
30 self.started.emit()
31
31
32 def stop(self):
32 def stop(self):
33 """ Reimplemented to emit signal.
33 """ Reimplemented to emit signal.
34 """
34 """
35 super(SocketChannelQObject, self).stop()
35 super(SocketChannelQObject, self).stop()
36 self.stopped.emit()
36 self.stopped.emit()
37
37
38
38
39 class QtXReqSocketChannel(SocketChannelQObject, XReqSocketChannel):
39 class QtXReqSocketChannel(SocketChannelQObject, XReqSocketChannel):
40
40
41 # Emitted when any message is received.
41 # Emitted when any message is received.
42 message_received = QtCore.pyqtSignal(object)
42 message_received = QtCore.Signal(object)
43
43
44 # Emitted when a reply has been received for the corresponding request
44 # Emitted when a reply has been received for the corresponding request
45 # type.
45 # type.
46 execute_reply = QtCore.pyqtSignal(object)
46 execute_reply = QtCore.Signal(object)
47 complete_reply = QtCore.pyqtSignal(object)
47 complete_reply = QtCore.Signal(object)
48 object_info_reply = QtCore.pyqtSignal(object)
48 object_info_reply = QtCore.Signal(object)
49
49
50 # Emitted when the first reply comes back.
50 # Emitted when the first reply comes back.
51 first_reply = QtCore.pyqtSignal()
51 first_reply = QtCore.Signal()
52
52
53 # Used by the first_reply signal logic to determine if a reply is the
53 # Used by the first_reply signal logic to determine if a reply is the
54 # first.
54 # first.
55 _handlers_called = False
55 _handlers_called = False
56
56
57 #---------------------------------------------------------------------------
57 #---------------------------------------------------------------------------
58 # 'XReqSocketChannel' interface
58 # 'XReqSocketChannel' interface
59 #---------------------------------------------------------------------------
59 #---------------------------------------------------------------------------
60
60
61 def call_handlers(self, msg):
61 def call_handlers(self, msg):
62 """ Reimplemented to emit signals instead of making callbacks.
62 """ Reimplemented to emit signals instead of making callbacks.
63 """
63 """
64 # Emit the generic signal.
64 # Emit the generic signal.
65 self.message_received.emit(msg)
65 self.message_received.emit(msg)
66
66
67 # Emit signals for specialized message types.
67 # Emit signals for specialized message types.
68 msg_type = msg['msg_type']
68 msg_type = msg['msg_type']
69 signal = getattr(self, msg_type, None)
69 signal = getattr(self, msg_type, None)
70 if signal:
70 if signal:
71 signal.emit(msg)
71 signal.emit(msg)
72
72
73 if not self._handlers_called:
73 if not self._handlers_called:
74 self.first_reply.emit()
74 self.first_reply.emit()
75 self._handlers_called = True
75 self._handlers_called = True
76
76
77 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
78 # 'QtXReqSocketChannel' interface
78 # 'QtXReqSocketChannel' interface
79 #---------------------------------------------------------------------------
79 #---------------------------------------------------------------------------
80
80
81 def reset_first_reply(self):
81 def reset_first_reply(self):
82 """ Reset the first_reply signal to fire again on the next reply.
82 """ Reset the first_reply signal to fire again on the next reply.
83 """
83 """
84 self._handlers_called = False
84 self._handlers_called = False
85
85
86
86
87 class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel):
87 class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel):
88
88
89 # Emitted when any message is received.
89 # Emitted when any message is received.
90 message_received = QtCore.pyqtSignal(object)
90 message_received = QtCore.Signal(object)
91
91
92 # Emitted when a message of type 'stream' is received.
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 # Emitted when a message of type 'pyin' is received.
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 # Emitted when a message of type 'pyout' is received.
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 # Emitted when a message of type 'pyerr' is received.
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 # Emitted when a message of type 'display_data' is received
104 # Emitted when a message of type 'display_data' is received
105 display_data_received = QtCore.pyqtSignal(object)
105 display_data_received = QtCore.pyqtSignal(object)
106
106
107 # Emitted when a crash report message is received from the kernel's
107 # Emitted when a crash report message is received from the kernel's
108 # last-resort sys.excepthook.
108 # last-resort sys.excepthook.
109 crash_received = QtCore.pyqtSignal(object)
109 crash_received = QtCore.Signal(object)
110
110
111 # Emitted when a shutdown is noticed.
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 # 'SubSocketChannel' interface
115 # 'SubSocketChannel' interface
116 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
117
117
118 def call_handlers(self, msg):
118 def call_handlers(self, msg):
119 """ Reimplemented to emit signals instead of making callbacks.
119 """ Reimplemented to emit signals instead of making callbacks.
120 """
120 """
121 # Emit the generic signal.
121 # Emit the generic signal.
122 self.message_received.emit(msg)
122 self.message_received.emit(msg)
123 # Emit signals for specialized message types.
123 # Emit signals for specialized message types.
124 msg_type = msg['msg_type']
124 msg_type = msg['msg_type']
125 signal = getattr(self, msg_type + '_received', None)
125 signal = getattr(self, msg_type + '_received', None)
126 if signal:
126 if signal:
127 signal.emit(msg)
127 signal.emit(msg)
128 elif msg_type in ('stdout', 'stderr'):
128 elif msg_type in ('stdout', 'stderr'):
129 self.stream_received.emit(msg)
129 self.stream_received.emit(msg)
130
130
131 def flush(self):
131 def flush(self):
132 """ Reimplemented to ensure that signals are dispatched immediately.
132 """ Reimplemented to ensure that signals are dispatched immediately.
133 """
133 """
134 super(QtSubSocketChannel, self).flush()
134 super(QtSubSocketChannel, self).flush()
135 QtCore.QCoreApplication.instance().processEvents()
135 QtCore.QCoreApplication.instance().processEvents()
136
136
137
137
138 class QtRepSocketChannel(SocketChannelQObject, RepSocketChannel):
138 class QtRepSocketChannel(SocketChannelQObject, RepSocketChannel):
139
139
140 # Emitted when any message is received.
140 # Emitted when any message is received.
141 message_received = QtCore.pyqtSignal(object)
141 message_received = QtCore.Signal(object)
142
142
143 # Emitted when an input request is received.
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 # 'RepSocketChannel' interface
147 # 'RepSocketChannel' interface
148 #---------------------------------------------------------------------------
148 #---------------------------------------------------------------------------
149
149
150 def call_handlers(self, msg):
150 def call_handlers(self, msg):
151 """ Reimplemented to emit signals instead of making callbacks.
151 """ Reimplemented to emit signals instead of making callbacks.
152 """
152 """
153 # Emit the generic signal.
153 # Emit the generic signal.
154 self.message_received.emit(msg)
154 self.message_received.emit(msg)
155
155
156 # Emit signals for specialized message types.
156 # Emit signals for specialized message types.
157 msg_type = msg['msg_type']
157 msg_type = msg['msg_type']
158 if msg_type == 'input_request':
158 if msg_type == 'input_request':
159 self.input_requested.emit(msg)
159 self.input_requested.emit(msg)
160
160
161
161
162 class QtHBSocketChannel(SocketChannelQObject, HBSocketChannel):
162 class QtHBSocketChannel(SocketChannelQObject, HBSocketChannel):
163
163
164 # Emitted when the kernel has died.
164 # Emitted when the kernel has died.
165 kernel_died = QtCore.pyqtSignal(object)
165 kernel_died = QtCore.Signal(object)
166
166
167 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
168 # 'HBSocketChannel' interface
168 # 'HBSocketChannel' interface
169 #---------------------------------------------------------------------------
169 #---------------------------------------------------------------------------
170
170
171 def call_handlers(self, since_last_heartbeat):
171 def call_handlers(self, since_last_heartbeat):
172 """ Reimplemented to emit signals instead of making callbacks.
172 """ Reimplemented to emit signals instead of making callbacks.
173 """
173 """
174 # Emit the generic signal.
174 # Emit the generic signal.
175 self.kernel_died.emit(since_last_heartbeat)
175 self.kernel_died.emit(since_last_heartbeat)
176
176
177
177
178 class QtKernelManager(KernelManager, SuperQObject):
178 class QtKernelManager(KernelManager, SuperQObject):
179 """ A KernelManager that provides signals and slots.
179 """ A KernelManager that provides signals and slots.
180 """
180 """
181
181
182 __metaclass__ = MetaQObjectHasTraits
182 __metaclass__ = MetaQObjectHasTraits
183
183
184 # Emitted when the kernel manager has started listening.
184 # Emitted when the kernel manager has started listening.
185 started_channels = QtCore.pyqtSignal()
185 started_channels = QtCore.Signal()
186
186
187 # Emitted when the kernel manager has stopped listening.
187 # Emitted when the kernel manager has stopped listening.
188 stopped_channels = QtCore.pyqtSignal()
188 stopped_channels = QtCore.Signal()
189
189
190 # Use Qt-specific channel classes that emit signals.
190 # Use Qt-specific channel classes that emit signals.
191 sub_channel_class = Type(QtSubSocketChannel)
191 sub_channel_class = Type(QtSubSocketChannel)
192 xreq_channel_class = Type(QtXReqSocketChannel)
192 xreq_channel_class = Type(QtXReqSocketChannel)
193 rep_channel_class = Type(QtRepSocketChannel)
193 rep_channel_class = Type(QtRepSocketChannel)
194 hb_channel_class = Type(QtHBSocketChannel)
194 hb_channel_class = Type(QtHBSocketChannel)
195
195
196 #---------------------------------------------------------------------------
196 #---------------------------------------------------------------------------
197 # 'KernelManager' interface
197 # 'KernelManager' interface
198 #---------------------------------------------------------------------------
198 #---------------------------------------------------------------------------
199
199
200 #------ Kernel process management ------------------------------------------
200 #------ Kernel process management ------------------------------------------
201
201
202 def start_kernel(self, *args, **kw):
202 def start_kernel(self, *args, **kw):
203 """ Reimplemented for proper heartbeat management.
203 """ Reimplemented for proper heartbeat management.
204 """
204 """
205 if self._xreq_channel is not None:
205 if self._xreq_channel is not None:
206 self._xreq_channel.reset_first_reply()
206 self._xreq_channel.reset_first_reply()
207 super(QtKernelManager, self).start_kernel(*args, **kw)
207 super(QtKernelManager, self).start_kernel(*args, **kw)
208
208
209 #------ Channel management -------------------------------------------------
209 #------ Channel management -------------------------------------------------
210
210
211 def start_channels(self, *args, **kw):
211 def start_channels(self, *args, **kw):
212 """ Reimplemented to emit signal.
212 """ Reimplemented to emit signal.
213 """
213 """
214 super(QtKernelManager, self).start_channels(*args, **kw)
214 super(QtKernelManager, self).start_channels(*args, **kw)
215 self.started_channels.emit()
215 self.started_channels.emit()
216
216
217 def stop_channels(self):
217 def stop_channels(self):
218 """ Reimplemented to emit signal.
218 """ Reimplemented to emit signal.
219 """
219 """
220 super(QtKernelManager, self).stop_channels()
220 super(QtKernelManager, self).stop_channels()
221 self.stopped_channels.emit()
221 self.stopped_channels.emit()
222
222
223 @property
223 @property
224 def xreq_channel(self):
224 def xreq_channel(self):
225 """ Reimplemented for proper heartbeat management.
225 """ Reimplemented for proper heartbeat management.
226 """
226 """
227 if self._xreq_channel is None:
227 if self._xreq_channel is None:
228 self._xreq_channel = super(QtKernelManager, self).xreq_channel
228 self._xreq_channel = super(QtKernelManager, self).xreq_channel
229 self._xreq_channel.first_reply.connect(self._first_reply)
229 self._xreq_channel.first_reply.connect(self._first_reply)
230 return self._xreq_channel
230 return self._xreq_channel
231
231
232 #---------------------------------------------------------------------------
232 #---------------------------------------------------------------------------
233 # Protected interface
233 # Protected interface
234 #---------------------------------------------------------------------------
234 #---------------------------------------------------------------------------
235
235
236 def _first_reply(self):
236 def _first_reply(self):
237 """ Unpauses the heartbeat channel when the first reply is received on
237 """ Unpauses the heartbeat channel when the first reply is received on
238 the execute channel. Note that this will *not* start the heartbeat
238 the execute channel. Note that this will *not* start the heartbeat
239 channel if it is not already running!
239 channel if it is not already running!
240 """
240 """
241 if self._hb_channel is not None:
241 if self._hb_channel is not None:
242 self._hb_channel.unpause()
242 self._hb_channel.unpause()
@@ -1,89 +1,80 b''
1 """ Defines utility functions for working with SVG documents in Qt.
1 """ Defines utility functions for working with SVG documents in Qt.
2 """
2 """
3
3
4 # System library imports.
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 def save_svg(string, parent=None):
8 def save_svg(string, parent=None):
9 """ Prompts the user to save an SVG document to disk.
9 """ Prompts the user to save an SVG document to disk.
10
10
11 Parameters:
11 Parameters:
12 -----------
12 -----------
13 string : str
13 string : str
14 A Python string or QString containing a SVG document.
14 A Python string containing a SVG document.
15
15
16 parent : QWidget, optional
16 parent : QWidget, optional
17 The parent to use for the file dialog.
17 The parent to use for the file dialog.
18
18
19 Returns:
19 Returns:
20 --------
20 --------
21 The name of the file to which the document was saved, or None if the save
21 The name of the file to which the document was saved, or None if the save
22 was cancelled.
22 was cancelled.
23 """
23 """
24 dialog = QtGui.QFileDialog(parent, 'Save SVG Document')
24 dialog = QtGui.QFileDialog(parent, 'Save SVG Document')
25 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
25 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
26 dialog.setDefaultSuffix('svg')
26 dialog.setDefaultSuffix('svg')
27 dialog.setNameFilter('SVG document (*.svg)')
27 dialog.setNameFilter('SVG document (*.svg)')
28 if dialog.exec_():
28 if dialog.exec_():
29 filename = dialog.selectedFiles()[0]
29 filename = dialog.selectedFiles()[0]
30 f = open(filename, 'w')
30 f = open(filename, 'w')
31 try:
31 try:
32 f.write(string)
32 f.write(string)
33 finally:
33 finally:
34 f.close()
34 f.close()
35 return filename
35 return filename
36 return None
36 return None
37
37
38 def svg_to_clipboard(string):
38 def svg_to_clipboard(string):
39 """ Copy a SVG document to the clipboard.
39 """ Copy a SVG document to the clipboard.
40
40
41 Parameters:
41 Parameters:
42 -----------
42 -----------
43 string : str
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 mime_data = QtCore.QMimeData()
46 mime_data = QtCore.QMimeData()
51 mime_data.setData('image/svg+xml', bytes)
47 mime_data.setData('image/svg+xml', string)
52 QtGui.QApplication.clipboard().setMimeData(mime_data)
48 QtGui.QApplication.clipboard().setMimeData(mime_data)
53
49
54 def svg_to_image(string, size=None):
50 def svg_to_image(string, size=None):
55 """ Convert a SVG document to a QImage.
51 """ Convert a SVG document to a QImage.
56
52
57 Parameters:
53 Parameters:
58 -----------
54 -----------
59 string : str
55 string : str
60 A Python string or QString containing a SVG document.
56 A Python string containing a SVG document.
61
57
62 size : QSize, optional
58 size : QSize, optional
63 The size of the image that is produced. If not specified, the SVG
59 The size of the image that is produced. If not specified, the SVG
64 document's default size is used.
60 document's default size is used.
65
61
66 Raises:
62 Raises:
67 -------
63 -------
68 ValueError
64 ValueError
69 If an invalid SVG string is provided.
65 If an invalid SVG string is provided.
70
66
71 Returns:
67 Returns:
72 --------
68 --------
73 A QImage of format QImage.Format_ARGB32.
69 A QImage of format QImage.Format_ARGB32.
74 """
70 """
75 if isinstance(string, basestring):
71 renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(string))
76 bytes = QtCore.QByteArray.fromRawData(string) # shallow copy
77 else:
78 bytes = string.toAscii()
79
80 renderer = QtSvg.QSvgRenderer(bytes)
81 if not renderer.isValid():
72 if not renderer.isValid():
82 raise ValueError('Invalid SVG data.')
73 raise ValueError('Invalid SVG data.')
83
74
84 if size is None:
75 if size is None:
85 size = renderer.defaultSize()
76 size = renderer.defaultSize()
86 image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32)
77 image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32)
87 painter = QtGui.QPainter(image)
78 painter = QtGui.QPainter(image)
88 renderer.render(painter)
79 renderer.render(painter)
89 return image
80 return image
@@ -1,106 +1,106 b''
1 """ Defines miscellaneous Qt-related helper classes and functions.
1 """ Defines miscellaneous Qt-related helper classes and functions.
2 """
2 """
3
3
4 # Standard library imports.
4 # Standard library imports.
5 import inspect
5 import inspect
6
6
7 # System library imports.
7 # System library imports.
8 from PyQt4 import QtCore, QtGui
8 from IPython.external.qt import QtCore, QtGui
9
9
10 # IPython imports.
10 # IPython imports.
11 from IPython.utils.traitlets import HasTraits, TraitType
11 from IPython.utils.traitlets import HasTraits, TraitType
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Metaclasses
14 # Metaclasses
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 MetaHasTraits = type(HasTraits)
17 MetaHasTraits = type(HasTraits)
18 MetaQObject = type(QtCore.QObject)
18 MetaQObject = type(QtCore.QObject)
19
19
20 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
20 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
21 """ A metaclass that inherits from the metaclasses of HasTraits and QObject.
21 """ A metaclass that inherits from the metaclasses of HasTraits and QObject.
22
22
23 Using this metaclass allows a class to inherit from both HasTraits and
23 Using this metaclass allows a class to inherit from both HasTraits and
24 QObject. Using SuperQObject instead of QObject is highly recommended. See
24 QObject. Using SuperQObject instead of QObject is highly recommended. See
25 QtKernelManager for an example.
25 QtKernelManager for an example.
26 """
26 """
27 def __new__(mcls, name, bases, classdict):
27 def __new__(mcls, name, bases, classdict):
28 # FIXME: this duplicates the code from MetaHasTraits.
28 # FIXME: this duplicates the code from MetaHasTraits.
29 # I don't think a super() call will help me here.
29 # I don't think a super() call will help me here.
30 for k,v in classdict.iteritems():
30 for k,v in classdict.iteritems():
31 if isinstance(v, TraitType):
31 if isinstance(v, TraitType):
32 v.name = k
32 v.name = k
33 elif inspect.isclass(v):
33 elif inspect.isclass(v):
34 if issubclass(v, TraitType):
34 if issubclass(v, TraitType):
35 vinst = v()
35 vinst = v()
36 vinst.name = k
36 vinst.name = k
37 classdict[k] = vinst
37 classdict[k] = vinst
38 cls = MetaQObject.__new__(mcls, name, bases, classdict)
38 cls = MetaQObject.__new__(mcls, name, bases, classdict)
39 return cls
39 return cls
40
40
41 def __init__(mcls, name, bases, classdict):
41 def __init__(mcls, name, bases, classdict):
42 # Note: super() did not work, so we explicitly call these.
42 # Note: super() did not work, so we explicitly call these.
43 MetaQObject.__init__(mcls, name, bases, classdict)
43 MetaQObject.__init__(mcls, name, bases, classdict)
44 MetaHasTraits.__init__(mcls, name, bases, classdict)
44 MetaHasTraits.__init__(mcls, name, bases, classdict)
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Classes
47 # Classes
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49
49
50 class SuperQObject(QtCore.QObject):
50 class SuperQObject(QtCore.QObject):
51 """ Permits the use of super() in class hierarchies that contain QObject.
51 """ Permits the use of super() in class hierarchies that contain QObject.
52
52
53 Unlike QObject, SuperQObject does not accept a QObject parent. If it did,
53 Unlike QObject, SuperQObject does not accept a QObject parent. If it did,
54 super could not be emulated properly (all other classes in the heierarchy
54 super could not be emulated properly (all other classes in the heierarchy
55 would have to accept the parent argument--they don't, of course, because
55 would have to accept the parent argument--they don't, of course, because
56 they don't inherit QObject.)
56 they don't inherit QObject.)
57
57
58 This class is primarily useful for attaching signals to existing non-Qt
58 This class is primarily useful for attaching signals to existing non-Qt
59 classes. See QtKernelManager for an example.
59 classes. See QtKernelManager for an example.
60 """
60 """
61
61
62 def __new__(cls, *args, **kw):
62 def __new__(cls, *args, **kw):
63 # We initialize QObject as early as possible. Without this, Qt complains
63 # We initialize QObject as early as possible. Without this, Qt complains
64 # if SuperQObject is not the first class in the super class list.
64 # if SuperQObject is not the first class in the super class list.
65 inst = QtCore.QObject.__new__(cls)
65 inst = QtCore.QObject.__new__(cls)
66 QtCore.QObject.__init__(inst)
66 QtCore.QObject.__init__(inst)
67 return inst
67 return inst
68
68
69 def __init__(self, *args, **kw):
69 def __init__(self, *args, **kw):
70 # Emulate super by calling the next method in the MRO, if there is one.
70 # Emulate super by calling the next method in the MRO, if there is one.
71 mro = self.__class__.mro()
71 mro = self.__class__.mro()
72 for qt_class in QtCore.QObject.mro():
72 for qt_class in QtCore.QObject.mro():
73 mro.remove(qt_class)
73 mro.remove(qt_class)
74 next_index = mro.index(SuperQObject) + 1
74 next_index = mro.index(SuperQObject) + 1
75 if next_index < len(mro):
75 if next_index < len(mro):
76 mro[next_index].__init__(self, *args, **kw)
76 mro[next_index].__init__(self, *args, **kw)
77
77
78 #-----------------------------------------------------------------------------
78 #-----------------------------------------------------------------------------
79 # Functions
79 # Functions
80 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
81
81
82 def get_font(family, fallback=None):
82 def get_font(family, fallback=None):
83 """Return a font of the requested family, using fallback as alternative.
83 """Return a font of the requested family, using fallback as alternative.
84
84
85 If a fallback is provided, it is used in case the requested family isn't
85 If a fallback is provided, it is used in case the requested family isn't
86 found. If no fallback is given, no alternative is chosen and Qt's internal
86 found. If no fallback is given, no alternative is chosen and Qt's internal
87 algorithms may automatically choose a fallback font.
87 algorithms may automatically choose a fallback font.
88
88
89 Parameters
89 Parameters
90 ----------
90 ----------
91 family : str
91 family : str
92 A font name.
92 A font name.
93 fallback : str
93 fallback : str
94 A font name.
94 A font name.
95
95
96 Returns
96 Returns
97 -------
97 -------
98 font : QFont object
98 font : QFont object
99 """
99 """
100 font = QtGui.QFont(family)
100 font = QtGui.QFont(family)
101 # Check whether we got what we wanted using QFontInfo, since exactMatch()
101 # Check whether we got what we wanted using QFontInfo, since exactMatch()
102 # is overly strict and returns false in too many cases.
102 # is overly strict and returns false in too many cases.
103 font_info = QtGui.QFontInfo(font)
103 font_info = QtGui.QFontInfo(font)
104 if fallback is not None and font_info.family() != family:
104 if fallback is not None and font_info.family() != family:
105 font = QtGui.QFont(fallback)
105 font = QtGui.QFont(fallback)
106 return font
106 return font
General Comments 0
You need to be logged in to leave comments. Login now