##// END OF EJS Templates
new completer for qtconsole....
Matthias BUSSONNIER -
Show More
@@ -0,0 +1,226 b''
1 # System library imports
2 from IPython.external.qt import QtCore, QtGui
3 import IPython.utils.html_utils as html_utils
4
5
6 class CompletionHtml(QtGui.QWidget):
7 """ A widget for tab completion, navigable by arrow keys """
8
9 #--------------------------------------------------------------------------
10 # 'QObject' interface
11 #--------------------------------------------------------------------------
12
13 _items = ()
14 _index = (0, 0)
15 _consecutive_tab = 0
16 _size = (1, 1)
17 _old_cursor = None
18 _start_position = 0
19
20 def __init__(self, console_widget):
21 """ Create a completion widget that is attached to the specified Qt
22 text edit widget.
23 """
24 assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
25 super(CompletionHtml, self).__init__()
26
27 self._text_edit = console_widget._control
28 self._console_widget = console_widget
29 self._text_edit.installEventFilter(self)
30
31 # Ensure that the text edit keeps focus when widget is displayed.
32 self.setFocusProxy(self._text_edit)
33
34
35 def eventFilter(self, obj, event):
36 """ Reimplemented to handle keyboard input and to auto-hide when the
37 text edit loses focus.
38 """
39 if obj == self._text_edit:
40 etype = event.type()
41 if etype == QtCore.QEvent.KeyPress:
42 key = event.key()
43 if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,):
44 return False
45 elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,):
46 # ok , called twice, we grab focus, and show the cursor
47 self._consecutive_tab = self._consecutive_tab+1
48 self._update_list()
49 return True
50 elif self._consecutive_tab == 2:
51 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
52 self._complete_current()
53 return True
54 if key in (QtCore.Qt.Key_Tab,):
55 self.select_right()
56 self._update_list()
57 return True
58 elif key in ( QtCore.Qt.Key_Down,):
59 self.select_down()
60 self._update_list()
61 return True
62 elif key in (QtCore.Qt.Key_Right,):
63 self.select_right()
64 self._update_list()
65 return True
66 elif key in ( QtCore.Qt.Key_Up,):
67 self.select_up()
68 self._update_list()
69 return True
70 elif key in ( QtCore.Qt.Key_Left,):
71 self.select_left()
72 self._update_list()
73 return True
74 else :
75 self._cancel_completion()
76 else:
77 self._cancel_completion()
78
79 elif etype == QtCore.QEvent.FocusOut:
80 self._cancel_completion()
81
82 return super(CompletionHtml, self).eventFilter(obj, event)
83
84 #--------------------------------------------------------------------------
85 # 'CompletionHtml' interface
86 #--------------------------------------------------------------------------
87 def _cancel_completion(self):
88 """Cancel the completion, reseting internal variable, clearing buffer """
89 self._consecutive_tab = 0
90 self._console_widget._clear_temporary_buffer()
91 self._index = (0, 0)
92
93 #
94 # ... 2 4 4 4 4 4 4 4 4 4 4 4 4
95 # 2 2 4 4 4 4 4 4 4 4 4 4 4 4
96 #
97 #2 2 x x x x x x x x x x x 5 5
98 #6 6 x x x x x x x x x x x 5 5
99 #6 6 x x x x x x x x x x ? 5 5
100 #6 6 x x x x x x x x x x ? 1 1
101 #
102 #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
103 #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
104 def _select_index(self, row, col):
105 """Change the selection index, and make sure it stays in the right range
106
107 A little more complicated than just dooing modulo the number of row columns
108 to be sure to cycle through all element.
109
110 horizontaly, the element are maped like this :
111 to r <-- a b c d e f --> to g
112 to f <-- g h i j k l --> to m
113 to l <-- m n o p q r --> to a
114
115 and vertically
116 a d g j m p
117 b e h k n q
118 c f i l o r
119 """
120
121 nr, nc = self._size
122 nr = nr-1
123 nc = nc-1
124
125 # case 1
126 if (row > nr and col >= nc) or (row >= nr and col > nc):
127 self._select_index(0, 0)
128 # case 2
129 elif (row <= 0 and col < 0) or (row < 0 and col <= 0):
130 self._select_index(nr, nc)
131 # case 3
132 elif row > nr :
133 self._select_index(0, col+1)
134 # case 4
135 elif row < 0 :
136 self._select_index(nr, col-1)
137 # case 5
138 elif col > nc :
139 self._select_index(row+1, 0)
140 # case 6
141 elif col < 0 :
142 self._select_index(row-1, nc)
143 elif 0 <= row and row <= nr and 0 <= col and col <= nc :
144 self._index = (row, col)
145 else :
146 raise NotImplementedError("you'r trying to go where no completion\
147 have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) )
148
149
150
151 def select_up(self):
152 """move cursor up"""
153 r, c = self._index
154 self._select_index(r-1, c)
155
156 def select_down(self):
157 """move cursor down"""
158 r, c = self._index
159 self._select_index(r+1, c)
160
161 def select_left(self):
162 """move cursor left"""
163 r, c = self._index
164 self._select_index(r, c-1)
165
166 def select_right(self):
167 """move cursor right"""
168 r, c = self._index
169 self._select_index(r, c+1)
170
171 def show_items(self, cursor, items):
172 """ Shows the completion widget with 'items' at the position specified
173 by 'cursor'.
174 """
175 if not items :
176 return
177 self._start_position = cursor.position()
178 self._consecutive_tab = 1
179 ci = html_utils.columnize_info(items, empty=' ')
180 self._items = ci['item_matrix']
181 self._size = (ci['rows_number'], ci['columns_number'])
182 self._old_cursor = cursor
183 self._index = (0, 0)
184 self._update_list(hilight=False)
185
186
187 def _update_list(self, hilight=True):
188 """ update the list of completion and hilight the currently selected completion """
189 if len(self._items) > 100:
190 items = self._items[:100]
191 else :
192 items = self._items
193 items_m = items
194
195 self._console_widget._clear_temporary_buffer()
196 if(hilight):
197 strng = html_utils.html_tableify(items_m, select=self._index)
198 else:
199 strng = html_utils.html_tableify(items_m, select=None)
200 self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True)
201
202 #--------------------------------------------------------------------------
203 # Protected interface
204 #--------------------------------------------------------------------------
205
206 def _complete_current(self):
207 """ Perform the completion with the currently selected item.
208 """
209 i = self._index
210 item = self._items[i[0]][i[1]]
211 item = item.strip()
212 if item :
213 self._current_text_cursor().insertText(item)
214 self._cancel_completion()
215
216 def _current_text_cursor(self):
217 """ Returns a cursor with text between the start position and the
218 current position selected.
219 """
220 cursor = self._text_edit.textCursor()
221 if cursor.position() >= self._start_position:
222 cursor.setPosition(self._start_position,
223 QtGui.QTextCursor.KeepAnchor)
224 return cursor
225
226
@@ -0,0 +1,73 b''
1 # System library imports
2 from IPython.external.qt import QtCore, QtGui
3 import IPython.utils.html_utils as html_utils
4
5
6 class CompletionPlain(QtGui.QWidget):
7 """ A widget for tab completion, navigable by arrow keys """
8
9 #--------------------------------------------------------------------------
10 # 'QObject' interface
11 #--------------------------------------------------------------------------
12
13 _items = ()
14 _index = (0, 0)
15 _old_cursor = None
16
17 def __init__(self, console_widget):
18 """ Create a completion widget that is attached to the specified Qt
19 text edit widget.
20 """
21 assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 super(CompletionPlain, self).__init__()
23
24 self._text_edit = console_widget._control
25 self._console_widget = console_widget
26
27 self._text_edit.installEventFilter(self)
28
29 def eventFilter(self, obj, event):
30 """ Reimplemented to handle keyboard input and to auto-hide when the
31 text edit loses focus.
32 """
33 if obj == self._text_edit:
34 etype = event.type()
35
36 if etype == QtCore.QEvent.KeyPress:
37 self._cancel_completion()
38
39 return super(CompletionPlain, self).eventFilter(obj, event)
40
41 #--------------------------------------------------------------------------
42 # 'CompletionPlain' interface
43 #--------------------------------------------------------------------------
44 def _cancel_completion(self):
45 """Cancel the completion, reseting internal variable, clearing buffer """
46 self._console_widget._clear_temporary_buffer()
47 self._index = (0, 0)
48
49
50 def show_items(self, cursor, items):
51 """ Shows the completion widget with 'items' at the position specified
52 by 'cursor'.
53 """
54 if not items :
55 return
56
57 ci = html_utils.columnize_info(items, empty=' ')
58 self._items = ci['item_matrix']
59 self._old_cursor = cursor
60 self._update_list()
61
62
63 def _update_list(self):
64 """ update the list of completion and hilight the currently selected completion """
65 if len(self._items) > 100:
66 items = self._items[:100]
67 else :
68 items = self._items
69 items_m = items
70
71 self._console_widget._clear_temporary_buffer()
72 strng = html_utils.html_tableify(items_m, select=None)
73 self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True)
@@ -0,0 +1,124 b''
1 """some html utilis"""
2 from IPython.core.display import HTML
3
4
5 def columnize_info(items, separator_width=1, displaywidth=80, empty=None):
6 """ Get info on a list of string to display it as a multicolumns list
7
8 returns :
9 ---------
10
11 a dict containing several parameters:
12
13 'item_matrix' : list of list with the innermost list representing a row
14 'columns_number': number of columns
15 'rows_number' : number of rown
16 'columns_width' : a list indicating the maximum length of the element in each columns
17
18 Parameters :
19 ------------
20 separator_width : when trying to ajust the number of column, consider a separator size of this much caracters
21 displaywidth : try to fit the columns in this width
22 empty : if the number of items is different from nrows * ncols, fill with empty
23
24 """
25 # Note: this code is adapted from columnize 0.3.2.
26 # See http://code.google.com/p/pycolumnize/
27
28 # Some degenerate cases.
29 size = len(items)
30 if size == 0:
31 return {'item_matrix' :[[empty]],
32 'columns_number':1,
33 'rows_number':1,
34 'columns_width':[0]}
35 elif size == 1:
36 return {'item_matrix' :[[items[0]]],
37 'columns_number':1,
38 'rows_number':1,
39 'columns_width':[len(items[0])]}
40
41 # Special case: if any item is longer than the maximum width, there's no
42 # point in triggering the logic below...
43 item_len = map(len, items) # save these, we can reuse them below
44 #longest = max(item_len)
45 #if longest >= displaywidth:
46 # return (items, [longest])
47
48 # Try every row count from 1 upwards
49 array_index = lambda nrows, row, col: nrows*col + row
50 nrows = 1
51 for nrows in range(1, size):
52 ncols = (size + nrows - 1) // nrows
53 colwidths = []
54 totwidth = -separator_width
55 for col in range(ncols):
56 # Get max column width for this column
57 colwidth = 0
58 for row in range(nrows):
59 i = array_index(nrows, row, col)
60 if i >= size:
61 break
62 len_x = item_len[i]
63 colwidth = max(colwidth, len_x)
64 colwidths.append(colwidth)
65 totwidth += colwidth + separator_width
66 if totwidth > displaywidth:
67 break
68 if totwidth <= displaywidth:
69 break
70
71 # The smallest number of rows computed and the max widths for each
72 # column has been obtained. Now we just have to format each of the rows.
73 reorderd_items = []
74 for row in range(nrows):
75 texts = []
76 for col in range(ncols):
77 i = row + nrows*col
78 if i >= size:
79 texts.append(empty)
80 else:
81 texts.append(items[i])
82 #while texts and not texts[-1]:
83 # del texts[-1]
84 #for col in range(len(texts)):
85 # texts[col] = texts[col].ljust(colwidths[col])
86 reorderd_items.append(texts)
87
88 return {'item_matrix' :reorderd_items,
89 'columns_number':ncols,
90 'rows_number':nrows,
91 'columns_width':colwidths}
92
93
94 def column_table(items, select=None) :
95 """ return a html table of the item with a select class on one"""
96 items_m = columnize_info(items)['item_matrix']
97 return HTML(html_tableify(items_m, select=select))
98
99 def html_tableify(item_matrix, select=None) :
100 """ returnr a string for an html table"""
101 if not item_matrix :
102 return ''
103 html_cols = []
104 tds = lambda text : u'<td>'+text+u'</td>'
105 trs = lambda text : u'<tr>'+text+u'</tr>'
106 tds_items = [map(tds, row) for row in item_matrix ]
107 if select :
108 row, col = select
109 try :
110 tds_items[row][col] = u'<td class="inverted">'\
111 +item_matrix[row][col]\
112 +u'</td>'
113 except IndexError :
114 pass
115 #select the right item
116 html_cols = map(trs, (u''.join(row) for row in tds_items))
117 html = (u'<table class="completion">'+(u''.join(html_cols))+u'</table>')
118 css = u"""
119 <style>
120 table.completion tr td
121 { padding-right : 4px; }
122 </style>
123 """
124 return css+html
@@ -1,134 +1,135 b''
1 # System library imports
1 # System library imports
2 from IPython.external.qt 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, console_widget):
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 text_edit = console_widget._text_edit
17 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 super(CompletionWidget, self).__init__()
19 super(CompletionWidget, self).__init__()
19
20
20 self._text_edit = text_edit
21 self._text_edit = text_edit
21
22
22 self.setAttribute(QtCore.Qt.WA_StaticContents)
23 self.setAttribute(QtCore.Qt.WA_StaticContents)
23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
24 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
24
25
25 # Ensure that the text edit keeps focus when widget is displayed.
26 # Ensure that the text edit keeps focus when widget is displayed.
26 self.setFocusProxy(self._text_edit)
27 self.setFocusProxy(self._text_edit)
27
28
28 self.setFrameShadow(QtGui.QFrame.Plain)
29 self.setFrameShadow(QtGui.QFrame.Plain)
29 self.setFrameShape(QtGui.QFrame.StyledPanel)
30 self.setFrameShape(QtGui.QFrame.StyledPanel)
30
31
31 self.itemActivated.connect(self._complete_current)
32 self.itemActivated.connect(self._complete_current)
32
33
33 def eventFilter(self, obj, event):
34 def eventFilter(self, obj, event):
34 """ Reimplemented to handle keyboard input and to auto-hide when the
35 """ Reimplemented to handle keyboard input and to auto-hide when the
35 text edit loses focus.
36 text edit loses focus.
36 """
37 """
37 if obj == self._text_edit:
38 if obj == self._text_edit:
38 etype = event.type()
39 etype = event.type()
39
40
40 if etype == QtCore.QEvent.KeyPress:
41 if etype == QtCore.QEvent.KeyPress:
41 key, text = event.key(), event.text()
42 key, text = event.key(), event.text()
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
43 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
43 QtCore.Qt.Key_Tab):
44 QtCore.Qt.Key_Tab):
44 self._complete_current()
45 self._complete_current()
45 return True
46 return True
46 elif key == QtCore.Qt.Key_Escape:
47 elif key == QtCore.Qt.Key_Escape:
47 self.hide()
48 self.hide()
48 return True
49 return True
49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
50 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
51 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
52 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
52 self.keyPressEvent(event)
53 self.keyPressEvent(event)
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 return super(CompletionWidget, self).eventFilter(obj, event)
59 return super(CompletionWidget, self).eventFilter(obj, event)
59
60
60 #--------------------------------------------------------------------------
61 #--------------------------------------------------------------------------
61 # 'QWidget' interface
62 # 'QWidget' interface
62 #--------------------------------------------------------------------------
63 #--------------------------------------------------------------------------
63
64
64 def hideEvent(self, event):
65 def hideEvent(self, event):
65 """ Reimplemented to disconnect signal handlers and event filter.
66 """ Reimplemented to disconnect signal handlers and event filter.
66 """
67 """
67 super(CompletionWidget, self).hideEvent(event)
68 super(CompletionWidget, self).hideEvent(event)
68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
69 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
69 self._text_edit.removeEventFilter(self)
70 self._text_edit.removeEventFilter(self)
70
71
71 def showEvent(self, event):
72 def showEvent(self, event):
72 """ Reimplemented to connect signal handlers and event filter.
73 """ Reimplemented to connect signal handlers and event filter.
73 """
74 """
74 super(CompletionWidget, self).showEvent(event)
75 super(CompletionWidget, self).showEvent(event)
75 self._text_edit.cursorPositionChanged.connect(self._update_current)
76 self._text_edit.cursorPositionChanged.connect(self._update_current)
76 self._text_edit.installEventFilter(self)
77 self._text_edit.installEventFilter(self)
77
78
78 #--------------------------------------------------------------------------
79 #--------------------------------------------------------------------------
79 # 'CompletionWidget' interface
80 # 'CompletionWidget' interface
80 #--------------------------------------------------------------------------
81 #--------------------------------------------------------------------------
81
82
82 def show_items(self, cursor, items):
83 def show_items(self, cursor, items):
83 """ Shows the completion widget with 'items' at the position specified
84 """ Shows the completion widget with 'items' at the position specified
84 by 'cursor'.
85 by 'cursor'.
85 """
86 """
86 text_edit = self._text_edit
87 text_edit = self._text_edit
87 point = text_edit.cursorRect(cursor).bottomRight()
88 point = text_edit.cursorRect(cursor).bottomRight()
88 point = text_edit.mapToGlobal(point)
89 point = text_edit.mapToGlobal(point)
89 height = self.sizeHint().height()
90 height = self.sizeHint().height()
90 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
91 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
91 if screen_rect.size().height() - point.y() - height < 0:
92 if screen_rect.size().height() - point.y() - height < 0:
92 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
93 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
93 point.setY(point.y() - height)
94 point.setY(point.y() - height)
94 self.move(point)
95 self.move(point)
95
96
96 self._start_position = cursor.position()
97 self._start_position = cursor.position()
97 self.clear()
98 self.clear()
98 self.addItems(items)
99 self.addItems(items)
99 self.setCurrentRow(0)
100 self.setCurrentRow(0)
100 self.show()
101 self.show()
101
102
102 #--------------------------------------------------------------------------
103 #--------------------------------------------------------------------------
103 # Protected interface
104 # Protected interface
104 #--------------------------------------------------------------------------
105 #--------------------------------------------------------------------------
105
106
106 def _complete_current(self):
107 def _complete_current(self):
107 """ Perform the completion with the currently selected item.
108 """ Perform the completion with the currently selected item.
108 """
109 """
109 self._current_text_cursor().insertText(self.currentItem().text())
110 self._current_text_cursor().insertText(self.currentItem().text())
110 self.hide()
111 self.hide()
111
112
112 def _current_text_cursor(self):
113 def _current_text_cursor(self):
113 """ Returns a cursor with text between the start position and the
114 """ Returns a cursor with text between the start position and the
114 current position selected.
115 current position selected.
115 """
116 """
116 cursor = self._text_edit.textCursor()
117 cursor = self._text_edit.textCursor()
117 if cursor.position() >= self._start_position:
118 if cursor.position() >= self._start_position:
118 cursor.setPosition(self._start_position,
119 cursor.setPosition(self._start_position,
119 QtGui.QTextCursor.KeepAnchor)
120 QtGui.QTextCursor.KeepAnchor)
120 return cursor
121 return cursor
121
122
122 def _update_current(self):
123 def _update_current(self):
123 """ Updates the current item based on the current text.
124 """ Updates the current item based on the current text.
124 """
125 """
125 prefix = self._current_text_cursor().selection().toPlainText()
126 prefix = self._current_text_cursor().selection().toPlainText()
126 if prefix:
127 if prefix:
127 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
128 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
128 QtCore.Qt.MatchCaseSensitive))
129 QtCore.Qt.MatchCaseSensitive))
129 if items:
130 if items:
130 self.setCurrentItem(items[0])
131 self.setCurrentItem(items[0])
131 else:
132 else:
132 self.hide()
133 self.hide()
133 else:
134 else:
134 self.hide()
135 self.hide()
@@ -1,1878 +1,1880 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
9 from os.path import commonprefix
8 from os.path import commonprefix
10 import re
9 import re
11 import sys
10 import sys
12 from textwrap import dedent
11 from textwrap import dedent
13 from unicodedata import category
12 from unicodedata import category
14
13
15 # System library imports
14 # System library imports
16 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
17
16
18 # Local imports
17 # Local imports
19 from IPython.config.configurable import LoggingConfigurable
18 from IPython.config.configurable import LoggingConfigurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
19 from IPython.frontend.qt.rich_text import HtmlExporter
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
20 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.utils.text import columnize
21 from IPython.utils.text import columnize
23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
22 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 from ansi_code_processor import QtAnsiCodeProcessor
23 from ansi_code_processor import QtAnsiCodeProcessor
25 from completion_widget import CompletionWidget
24 from completion_widget import CompletionWidget
25 from completion_html import CompletionHtml
26 from completion_plain import CompletionPlain
26 from kill_ring import QtKillRing
27 from kill_ring import QtKillRing
27
28
28 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
29 # Functions
30 # Functions
30 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
31
32
32 def is_letter_or_number(char):
33 def is_letter_or_number(char):
33 """ Returns whether the specified unicode character is a letter or a number.
34 """ Returns whether the specified unicode character is a letter or a number.
34 """
35 """
35 cat = category(char)
36 cat = category(char)
36 return cat.startswith('L') or cat.startswith('N')
37 return cat.startswith('L') or cat.startswith('N')
37
38
38 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
39 # Classes
40 # Classes
40 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
41
42
42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 """ An abstract base class for console-type widgets. This class has
44 """ An abstract base class for console-type widgets. This class has
44 functionality for:
45 functionality for:
45
46
46 * Maintaining a prompt and editing region
47 * Maintaining a prompt and editing region
47 * Providing the traditional Unix-style console keyboard shortcuts
48 * Providing the traditional Unix-style console keyboard shortcuts
48 * Performing tab completion
49 * Performing tab completion
49 * Paging text
50 * Paging text
50 * Handling ANSI escape codes
51 * Handling ANSI escape codes
51
52
52 ConsoleWidget also provides a number of utility methods that will be
53 ConsoleWidget also provides a number of utility methods that will be
53 convenient to implementors of a console-style widget.
54 convenient to implementors of a console-style widget.
54 """
55 """
55 __metaclass__ = MetaQObjectHasTraits
56 __metaclass__ = MetaQObjectHasTraits
56
57
57 #------ Configuration ------------------------------------------------------
58 #------ Configuration ------------------------------------------------------
58
59
59 ansi_codes = Bool(True, config=True,
60 ansi_codes = Bool(True, config=True,
60 help="Whether to process ANSI escape codes."
61 help="Whether to process ANSI escape codes."
61 )
62 )
62 buffer_size = Integer(500, config=True,
63 buffer_size = Integer(500, config=True,
63 help="""
64 help="""
64 The maximum number of lines of text before truncation. Specifying a
65 The maximum number of lines of text before truncation. Specifying a
65 non-positive number disables text truncation (not recommended).
66 non-positive number disables text truncation (not recommended).
66 """
67 """
67 )
68 )
68 gui_completion = Bool(False, config=True,
69 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
69 help="""
70 default_value = 'ncurses',
70 Use a list widget instead of plain text output for tab completion.
71 help="""
71 """
72 The type of completer to use. Valid values are:
73
74 'plain' : Show the availlable completion as a text list
75 Below the editting area.
76 'droplist': Show the completion in a drop down list navigable
77 by the arrow keys, and from which you can select
78 completion by pressing Return.
79 'ncurses' : Show the completion as a text list which is navigable by
80 `tab` and arrow keys.
81 """
72 )
82 )
73 # NOTE: this value can only be specified during initialization.
83 # NOTE: this value can only be specified during initialization.
74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
84 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 help="""
85 help="""
76 The type of underlying text widget to use. Valid values are 'plain',
86 The type of underlying text widget to use. Valid values are 'plain',
77 which specifies a QPlainTextEdit, and 'rich', which specifies a
87 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 QTextEdit.
88 QTextEdit.
79 """
89 """
80 )
90 )
81 # NOTE: this value can only be specified during initialization.
91 # NOTE: this value can only be specified during initialization.
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
92 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 default_value='inside', config=True,
93 default_value='inside', config=True,
84 help="""
94 help="""
85 The type of paging to use. Valid values are:
95 The type of paging to use. Valid values are:
86
96
87 'inside' : The widget pages like a traditional terminal.
97 'inside' : The widget pages like a traditional terminal.
88 'hsplit' : When paging is requested, the widget is split
98 'hsplit' : When paging is requested, the widget is split
89 horizontally. The top pane contains the console, and the
99 horizontally. The top pane contains the console, and the
90 bottom pane contains the paged text.
100 bottom pane contains the paged text.
91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
101 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 used.
102 used.
93 'custom' : No action is taken by the widget beyond emitting a
103 'custom' : No action is taken by the widget beyond emitting a
94 'custom_page_requested(str)' signal.
104 'custom_page_requested(str)' signal.
95 'none' : The text is written directly to the console.
105 'none' : The text is written directly to the console.
96 """)
106 """)
97
107
98 font_family = Unicode(config=True,
108 font_family = Unicode(config=True,
99 help="""The font family to use for the console.
109 help="""The font family to use for the console.
100 On OSX this defaults to Monaco, on Windows the default is
110 On OSX this defaults to Monaco, on Windows the default is
101 Consolas with fallback of Courier, and on other platforms
111 Consolas with fallback of Courier, and on other platforms
102 the default is Monospace.
112 the default is Monospace.
103 """)
113 """)
104 def _font_family_default(self):
114 def _font_family_default(self):
105 if sys.platform == 'win32':
115 if sys.platform == 'win32':
106 # Consolas ships with Vista/Win7, fallback to Courier if needed
116 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 return 'Consolas'
117 return 'Consolas'
108 elif sys.platform == 'darwin':
118 elif sys.platform == 'darwin':
109 # OSX always has Monaco, no need for a fallback
119 # OSX always has Monaco, no need for a fallback
110 return 'Monaco'
120 return 'Monaco'
111 else:
121 else:
112 # Monospace should always exist, no need for a fallback
122 # Monospace should always exist, no need for a fallback
113 return 'Monospace'
123 return 'Monospace'
114
124
115 font_size = Integer(config=True,
125 font_size = Integer(config=True,
116 help="""The font size. If unconfigured, Qt will be entrusted
126 help="""The font size. If unconfigured, Qt will be entrusted
117 with the size of the font.
127 with the size of the font.
118 """)
128 """)
119
129
120 # Whether to override ShortcutEvents for the keybindings defined by this
130 # Whether to override ShortcutEvents for the keybindings defined by this
121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
131 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
132 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 override_shortcuts = Bool(False)
133 override_shortcuts = Bool(False)
124
134
125 #------ Signals ------------------------------------------------------------
135 #------ Signals ------------------------------------------------------------
126
136
127 # Signals that indicate ConsoleWidget state.
137 # Signals that indicate ConsoleWidget state.
128 copy_available = QtCore.Signal(bool)
138 copy_available = QtCore.Signal(bool)
129 redo_available = QtCore.Signal(bool)
139 redo_available = QtCore.Signal(bool)
130 undo_available = QtCore.Signal(bool)
140 undo_available = QtCore.Signal(bool)
131
141
132 # Signal emitted when paging is needed and the paging style has been
142 # Signal emitted when paging is needed and the paging style has been
133 # specified as 'custom'.
143 # specified as 'custom'.
134 custom_page_requested = QtCore.Signal(object)
144 custom_page_requested = QtCore.Signal(object)
135
145
136 # Signal emitted when the font is changed.
146 # Signal emitted when the font is changed.
137 font_changed = QtCore.Signal(QtGui.QFont)
147 font_changed = QtCore.Signal(QtGui.QFont)
138
148
139 #------ Protected class variables ------------------------------------------
149 #------ Protected class variables ------------------------------------------
140
150
141 # control handles
151 # control handles
142 _control = None
152 _control = None
143 _page_control = None
153 _page_control = None
144 _splitter = None
154 _splitter = None
145
155
146 # When the control key is down, these keys are mapped.
156 # When the control key is down, these keys are mapped.
147 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
157 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
148 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
158 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
149 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
159 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
150 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
160 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
151 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
161 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
152 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
162 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
153 if not sys.platform == 'darwin':
163 if not sys.platform == 'darwin':
154 # On OS X, Ctrl-E already does the right thing, whereas End moves the
164 # On OS X, Ctrl-E already does the right thing, whereas End moves the
155 # cursor to the bottom of the buffer.
165 # cursor to the bottom of the buffer.
156 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
166 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
157
167
158 # The shortcuts defined by this widget. We need to keep track of these to
168 # The shortcuts defined by this widget. We need to keep track of these to
159 # support 'override_shortcuts' above.
169 # support 'override_shortcuts' above.
160 _shortcuts = set(_ctrl_down_remap.keys() +
170 _shortcuts = set(_ctrl_down_remap.keys() +
161 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
171 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
162 QtCore.Qt.Key_V ])
172 QtCore.Qt.Key_V ])
163
173
174 _is_completing = False
175
164 #---------------------------------------------------------------------------
176 #---------------------------------------------------------------------------
165 # 'QObject' interface
177 # 'QObject' interface
166 #---------------------------------------------------------------------------
178 #---------------------------------------------------------------------------
167
179
168 def __init__(self, parent=None, **kw):
180 def __init__(self, parent=None, **kw):
169 """ Create a ConsoleWidget.
181 """ Create a ConsoleWidget.
170
182
171 Parameters:
183 Parameters:
172 -----------
184 -----------
173 parent : QWidget, optional [default None]
185 parent : QWidget, optional [default None]
174 The parent for this widget.
186 The parent for this widget.
175 """
187 """
176 QtGui.QWidget.__init__(self, parent)
188 QtGui.QWidget.__init__(self, parent)
177 LoggingConfigurable.__init__(self, **kw)
189 LoggingConfigurable.__init__(self, **kw)
178
190
179 # While scrolling the pager on Mac OS X, it tears badly. The
191 # While scrolling the pager on Mac OS X, it tears badly. The
180 # NativeGesture is platform and perhaps build-specific hence
192 # NativeGesture is platform and perhaps build-specific hence
181 # we take adequate precautions here.
193 # we take adequate precautions here.
182 self._pager_scroll_events = [QtCore.QEvent.Wheel]
194 self._pager_scroll_events = [QtCore.QEvent.Wheel]
183 if hasattr(QtCore.QEvent, 'NativeGesture'):
195 if hasattr(QtCore.QEvent, 'NativeGesture'):
184 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
196 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
185
197
186 # Create the layout and underlying text widget.
198 # Create the layout and underlying text widget.
187 layout = QtGui.QStackedLayout(self)
199 layout = QtGui.QStackedLayout(self)
188 layout.setContentsMargins(0, 0, 0, 0)
200 layout.setContentsMargins(0, 0, 0, 0)
189 self._control = self._create_control()
201 self._control = self._create_control()
190 if self.paging in ('hsplit', 'vsplit'):
202 if self.paging in ('hsplit', 'vsplit'):
191 self._splitter = QtGui.QSplitter()
203 self._splitter = QtGui.QSplitter()
192 if self.paging == 'hsplit':
204 if self.paging == 'hsplit':
193 self._splitter.setOrientation(QtCore.Qt.Horizontal)
205 self._splitter.setOrientation(QtCore.Qt.Horizontal)
194 else:
206 else:
195 self._splitter.setOrientation(QtCore.Qt.Vertical)
207 self._splitter.setOrientation(QtCore.Qt.Vertical)
196 self._splitter.addWidget(self._control)
208 self._splitter.addWidget(self._control)
197 layout.addWidget(self._splitter)
209 layout.addWidget(self._splitter)
198 else:
210 else:
199 layout.addWidget(self._control)
211 layout.addWidget(self._control)
200
212
201 # Create the paging widget, if necessary.
213 # Create the paging widget, if necessary.
202 if self.paging in ('inside', 'hsplit', 'vsplit'):
214 if self.paging in ('inside', 'hsplit', 'vsplit'):
203 self._page_control = self._create_page_control()
215 self._page_control = self._create_page_control()
204 if self._splitter:
216 if self._splitter:
205 self._page_control.hide()
217 self._page_control.hide()
206 self._splitter.addWidget(self._page_control)
218 self._splitter.addWidget(self._page_control)
207 else:
219 else:
208 layout.addWidget(self._page_control)
220 layout.addWidget(self._page_control)
209
221
210 # Initialize protected variables. Some variables contain useful state
222 # Initialize protected variables. Some variables contain useful state
211 # information for subclasses; they should be considered read-only.
223 # information for subclasses; they should be considered read-only.
212 self._append_before_prompt_pos = 0
224 self._append_before_prompt_pos = 0
213 self._ansi_processor = QtAnsiCodeProcessor()
225 self._ansi_processor = QtAnsiCodeProcessor()
214 self._completion_widget = CompletionWidget(self._control)
226 if self.gui_completion == 'ncurses':
227 self._completion_widget = CompletionHtml(self)
228 elif self.gui_completion == 'droplist':
229 self._completion_widget = CompletionWidget(self)
230 elif self.gui_completion == 'plain':
231 self._completion_widget = CompletionPlain(self)
232
215 self._continuation_prompt = '> '
233 self._continuation_prompt = '> '
216 self._continuation_prompt_html = None
234 self._continuation_prompt_html = None
217 self._executing = False
235 self._executing = False
218 self._filter_drag = False
236 self._filter_drag = False
219 self._filter_resize = False
237 self._filter_resize = False
220 self._html_exporter = HtmlExporter(self._control)
238 self._html_exporter = HtmlExporter(self._control)
221 self._input_buffer_executing = ''
239 self._input_buffer_executing = ''
222 self._input_buffer_pending = ''
240 self._input_buffer_pending = ''
223 self._kill_ring = QtKillRing(self._control)
241 self._kill_ring = QtKillRing(self._control)
224 self._prompt = ''
242 self._prompt = ''
225 self._prompt_html = None
243 self._prompt_html = None
226 self._prompt_pos = 0
244 self._prompt_pos = 0
227 self._prompt_sep = ''
245 self._prompt_sep = ''
228 self._reading = False
246 self._reading = False
229 self._reading_callback = None
247 self._reading_callback = None
230 self._tab_width = 8
248 self._tab_width = 8
231 self._text_completing_pos = 0
232
249
233 # Set a monospaced font.
250 # Set a monospaced font.
234 self.reset_font()
251 self.reset_font()
235
252
236 # Configure actions.
253 # Configure actions.
237 action = QtGui.QAction('Print', None)
254 action = QtGui.QAction('Print', None)
238 action.setEnabled(True)
255 action.setEnabled(True)
239 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
256 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
240 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
257 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
241 # Only override the default if there is a collision.
258 # Only override the default if there is a collision.
242 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
259 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
243 printkey = "Ctrl+Shift+P"
260 printkey = "Ctrl+Shift+P"
244 action.setShortcut(printkey)
261 action.setShortcut(printkey)
245 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
262 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
246 action.triggered.connect(self.print_)
263 action.triggered.connect(self.print_)
247 self.addAction(action)
264 self.addAction(action)
248 self.print_action = action
265 self.print_action = action
249
266
250 action = QtGui.QAction('Save as HTML/XML', None)
267 action = QtGui.QAction('Save as HTML/XML', None)
251 action.setShortcut(QtGui.QKeySequence.Save)
268 action.setShortcut(QtGui.QKeySequence.Save)
252 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
269 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
253 action.triggered.connect(self.export_html)
270 action.triggered.connect(self.export_html)
254 self.addAction(action)
271 self.addAction(action)
255 self.export_action = action
272 self.export_action = action
256
273
257 action = QtGui.QAction('Select All', None)
274 action = QtGui.QAction('Select All', None)
258 action.setEnabled(True)
275 action.setEnabled(True)
259 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
276 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
260 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
277 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
261 # Only override the default if there is a collision.
278 # Only override the default if there is a collision.
262 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
279 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
263 selectall = "Ctrl+Shift+A"
280 selectall = "Ctrl+Shift+A"
264 action.setShortcut(selectall)
281 action.setShortcut(selectall)
265 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
282 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
266 action.triggered.connect(self.select_all)
283 action.triggered.connect(self.select_all)
267 self.addAction(action)
284 self.addAction(action)
268 self.select_all_action = action
285 self.select_all_action = action
269
286
270 self.increase_font_size = QtGui.QAction("Bigger Font",
287 self.increase_font_size = QtGui.QAction("Bigger Font",
271 self,
288 self,
272 shortcut=QtGui.QKeySequence.ZoomIn,
289 shortcut=QtGui.QKeySequence.ZoomIn,
273 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
290 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
274 statusTip="Increase the font size by one point",
291 statusTip="Increase the font size by one point",
275 triggered=self._increase_font_size)
292 triggered=self._increase_font_size)
276 self.addAction(self.increase_font_size)
293 self.addAction(self.increase_font_size)
277
294
278 self.decrease_font_size = QtGui.QAction("Smaller Font",
295 self.decrease_font_size = QtGui.QAction("Smaller Font",
279 self,
296 self,
280 shortcut=QtGui.QKeySequence.ZoomOut,
297 shortcut=QtGui.QKeySequence.ZoomOut,
281 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
298 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
282 statusTip="Decrease the font size by one point",
299 statusTip="Decrease the font size by one point",
283 triggered=self._decrease_font_size)
300 triggered=self._decrease_font_size)
284 self.addAction(self.decrease_font_size)
301 self.addAction(self.decrease_font_size)
285
302
286 self.reset_font_size = QtGui.QAction("Normal Font",
303 self.reset_font_size = QtGui.QAction("Normal Font",
287 self,
304 self,
288 shortcut="Ctrl+0",
305 shortcut="Ctrl+0",
289 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
306 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
290 statusTip="Restore the Normal font size",
307 statusTip="Restore the Normal font size",
291 triggered=self.reset_font)
308 triggered=self.reset_font)
292 self.addAction(self.reset_font_size)
309 self.addAction(self.reset_font_size)
293
310
294
311
295
312
296 def eventFilter(self, obj, event):
313 def eventFilter(self, obj, event):
297 """ Reimplemented to ensure a console-like behavior in the underlying
314 """ Reimplemented to ensure a console-like behavior in the underlying
298 text widgets.
315 text widgets.
299 """
316 """
300 etype = event.type()
317 etype = event.type()
301 if etype == QtCore.QEvent.KeyPress:
318 if etype == QtCore.QEvent.KeyPress:
302
319
303 # Re-map keys for all filtered widgets.
320 # Re-map keys for all filtered widgets.
304 key = event.key()
321 key = event.key()
305 if self._control_key_down(event.modifiers()) and \
322 if self._control_key_down(event.modifiers()) and \
306 key in self._ctrl_down_remap:
323 key in self._ctrl_down_remap:
307 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
324 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
308 self._ctrl_down_remap[key],
325 self._ctrl_down_remap[key],
309 QtCore.Qt.NoModifier)
326 QtCore.Qt.NoModifier)
310 QtGui.qApp.sendEvent(obj, new_event)
327 QtGui.qApp.sendEvent(obj, new_event)
311 return True
328 return True
312
329
313 elif obj == self._control:
330 elif obj == self._control:
314 return self._event_filter_console_keypress(event)
331 return self._event_filter_console_keypress(event)
315
332
316 elif obj == self._page_control:
333 elif obj == self._page_control:
317 return self._event_filter_page_keypress(event)
334 return self._event_filter_page_keypress(event)
318
335
319 # Make middle-click paste safe.
336 # Make middle-click paste safe.
320 elif etype == QtCore.QEvent.MouseButtonRelease and \
337 elif etype == QtCore.QEvent.MouseButtonRelease and \
321 event.button() == QtCore.Qt.MidButton and \
338 event.button() == QtCore.Qt.MidButton and \
322 obj == self._control.viewport():
339 obj == self._control.viewport():
323 cursor = self._control.cursorForPosition(event.pos())
340 cursor = self._control.cursorForPosition(event.pos())
324 self._control.setTextCursor(cursor)
341 self._control.setTextCursor(cursor)
325 self.paste(QtGui.QClipboard.Selection)
342 self.paste(QtGui.QClipboard.Selection)
326 return True
343 return True
327
344
328 # Manually adjust the scrollbars *after* a resize event is dispatched.
345 # Manually adjust the scrollbars *after* a resize event is dispatched.
329 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
346 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
330 self._filter_resize = True
347 self._filter_resize = True
331 QtGui.qApp.sendEvent(obj, event)
348 QtGui.qApp.sendEvent(obj, event)
332 self._adjust_scrollbars()
349 self._adjust_scrollbars()
333 self._filter_resize = False
350 self._filter_resize = False
334 return True
351 return True
335
352
336 # Override shortcuts for all filtered widgets.
353 # Override shortcuts for all filtered widgets.
337 elif etype == QtCore.QEvent.ShortcutOverride and \
354 elif etype == QtCore.QEvent.ShortcutOverride and \
338 self.override_shortcuts and \
355 self.override_shortcuts and \
339 self._control_key_down(event.modifiers()) and \
356 self._control_key_down(event.modifiers()) and \
340 event.key() in self._shortcuts:
357 event.key() in self._shortcuts:
341 event.accept()
358 event.accept()
342
359
343 # Ensure that drags are safe. The problem is that the drag starting
360 # Ensure that drags are safe. The problem is that the drag starting
344 # logic, which determines whether the drag is a Copy or Move, is locked
361 # logic, which determines whether the drag is a Copy or Move, is locked
345 # down in QTextControl. If the widget is editable, which it must be if
362 # down in QTextControl. If the widget is editable, which it must be if
346 # we're not executing, the drag will be a Move. The following hack
363 # we're not executing, the drag will be a Move. The following hack
347 # prevents QTextControl from deleting the text by clearing the selection
364 # prevents QTextControl from deleting the text by clearing the selection
348 # when a drag leave event originating from this widget is dispatched.
365 # when a drag leave event originating from this widget is dispatched.
349 # The fact that we have to clear the user's selection is unfortunate,
366 # The fact that we have to clear the user's selection is unfortunate,
350 # but the alternative--trying to prevent Qt from using its hardwired
367 # but the alternative--trying to prevent Qt from using its hardwired
351 # drag logic and writing our own--is worse.
368 # drag logic and writing our own--is worse.
352 elif etype == QtCore.QEvent.DragEnter and \
369 elif etype == QtCore.QEvent.DragEnter and \
353 obj == self._control.viewport() and \
370 obj == self._control.viewport() and \
354 event.source() == self._control.viewport():
371 event.source() == self._control.viewport():
355 self._filter_drag = True
372 self._filter_drag = True
356 elif etype == QtCore.QEvent.DragLeave and \
373 elif etype == QtCore.QEvent.DragLeave and \
357 obj == self._control.viewport() and \
374 obj == self._control.viewport() and \
358 self._filter_drag:
375 self._filter_drag:
359 cursor = self._control.textCursor()
376 cursor = self._control.textCursor()
360 cursor.clearSelection()
377 cursor.clearSelection()
361 self._control.setTextCursor(cursor)
378 self._control.setTextCursor(cursor)
362 self._filter_drag = False
379 self._filter_drag = False
363
380
364 # Ensure that drops are safe.
381 # Ensure that drops are safe.
365 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
382 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
366 cursor = self._control.cursorForPosition(event.pos())
383 cursor = self._control.cursorForPosition(event.pos())
367 if self._in_buffer(cursor.position()):
384 if self._in_buffer(cursor.position()):
368 text = event.mimeData().text()
385 text = event.mimeData().text()
369 self._insert_plain_text_into_buffer(cursor, text)
386 self._insert_plain_text_into_buffer(cursor, text)
370
387
371 # Qt is expecting to get something here--drag and drop occurs in its
388 # Qt is expecting to get something here--drag and drop occurs in its
372 # own event loop. Send a DragLeave event to end it.
389 # own event loop. Send a DragLeave event to end it.
373 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
390 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
374 return True
391 return True
375
392
376 # Handle scrolling of the vsplit pager. This hack attempts to solve
393 # Handle scrolling of the vsplit pager. This hack attempts to solve
377 # problems with tearing of the help text inside the pager window. This
394 # problems with tearing of the help text inside the pager window. This
378 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
395 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
379 # perfect but makes the pager more usable.
396 # perfect but makes the pager more usable.
380 elif etype in self._pager_scroll_events and \
397 elif etype in self._pager_scroll_events and \
381 obj == self._page_control:
398 obj == self._page_control:
382 self._page_control.repaint()
399 self._page_control.repaint()
383 return True
400 return True
384 return super(ConsoleWidget, self).eventFilter(obj, event)
401 return super(ConsoleWidget, self).eventFilter(obj, event)
385
402
386 #---------------------------------------------------------------------------
403 #---------------------------------------------------------------------------
387 # 'QWidget' interface
404 # 'QWidget' interface
388 #---------------------------------------------------------------------------
405 #---------------------------------------------------------------------------
389
406
390 def sizeHint(self):
407 def sizeHint(self):
391 """ Reimplemented to suggest a size that is 80 characters wide and
408 """ Reimplemented to suggest a size that is 80 characters wide and
392 25 lines high.
409 25 lines high.
393 """
410 """
394 font_metrics = QtGui.QFontMetrics(self.font)
411 font_metrics = QtGui.QFontMetrics(self.font)
395 margin = (self._control.frameWidth() +
412 margin = (self._control.frameWidth() +
396 self._control.document().documentMargin()) * 2
413 self._control.document().documentMargin()) * 2
397 style = self.style()
414 style = self.style()
398 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
415 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
399
416
400 # Note 1: Despite my best efforts to take the various margins into
417 # Note 1: Despite my best efforts to take the various margins into
401 # account, the width is still coming out a bit too small, so we include
418 # account, the width is still coming out a bit too small, so we include
402 # a fudge factor of one character here.
419 # a fudge factor of one character here.
403 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
420 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
404 # to a Qt bug on certain Mac OS systems where it returns 0.
421 # to a Qt bug on certain Mac OS systems where it returns 0.
405 width = font_metrics.width(' ') * 81 + margin
422 width = font_metrics.width(' ') * 81 + margin
406 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
423 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
407 if self.paging == 'hsplit':
424 if self.paging == 'hsplit':
408 width = width * 2 + splitwidth
425 width = width * 2 + splitwidth
409
426
410 height = font_metrics.height() * 25 + margin
427 height = font_metrics.height() * 25 + margin
411 if self.paging == 'vsplit':
428 if self.paging == 'vsplit':
412 height = height * 2 + splitwidth
429 height = height * 2 + splitwidth
413
430
414 return QtCore.QSize(width, height)
431 return QtCore.QSize(width, height)
415
432
416 #---------------------------------------------------------------------------
433 #---------------------------------------------------------------------------
417 # 'ConsoleWidget' public interface
434 # 'ConsoleWidget' public interface
418 #---------------------------------------------------------------------------
435 #---------------------------------------------------------------------------
419
436
420 def can_copy(self):
437 def can_copy(self):
421 """ Returns whether text can be copied to the clipboard.
438 """ Returns whether text can be copied to the clipboard.
422 """
439 """
423 return self._control.textCursor().hasSelection()
440 return self._control.textCursor().hasSelection()
424
441
425 def can_cut(self):
442 def can_cut(self):
426 """ Returns whether text can be cut to the clipboard.
443 """ Returns whether text can be cut to the clipboard.
427 """
444 """
428 cursor = self._control.textCursor()
445 cursor = self._control.textCursor()
429 return (cursor.hasSelection() and
446 return (cursor.hasSelection() and
430 self._in_buffer(cursor.anchor()) and
447 self._in_buffer(cursor.anchor()) and
431 self._in_buffer(cursor.position()))
448 self._in_buffer(cursor.position()))
432
449
433 def can_paste(self):
450 def can_paste(self):
434 """ Returns whether text can be pasted from the clipboard.
451 """ Returns whether text can be pasted from the clipboard.
435 """
452 """
436 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
453 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
437 return bool(QtGui.QApplication.clipboard().text())
454 return bool(QtGui.QApplication.clipboard().text())
438 return False
455 return False
439
456
440 def clear(self, keep_input=True):
457 def clear(self, keep_input=True):
441 """ Clear the console.
458 """ Clear the console.
442
459
443 Parameters:
460 Parameters:
444 -----------
461 -----------
445 keep_input : bool, optional (default True)
462 keep_input : bool, optional (default True)
446 If set, restores the old input buffer if a new prompt is written.
463 If set, restores the old input buffer if a new prompt is written.
447 """
464 """
448 if self._executing:
465 if self._executing:
449 self._control.clear()
466 self._control.clear()
450 else:
467 else:
451 if keep_input:
468 if keep_input:
452 input_buffer = self.input_buffer
469 input_buffer = self.input_buffer
453 self._control.clear()
470 self._control.clear()
454 self._show_prompt()
471 self._show_prompt()
455 if keep_input:
472 if keep_input:
456 self.input_buffer = input_buffer
473 self.input_buffer = input_buffer
457
474
458 def copy(self):
475 def copy(self):
459 """ Copy the currently selected text to the clipboard.
476 """ Copy the currently selected text to the clipboard.
460 """
477 """
461 self.layout().currentWidget().copy()
478 self.layout().currentWidget().copy()
462
479
463 def cut(self):
480 def cut(self):
464 """ Copy the currently selected text to the clipboard and delete it
481 """ Copy the currently selected text to the clipboard and delete it
465 if it's inside the input buffer.
482 if it's inside the input buffer.
466 """
483 """
467 self.copy()
484 self.copy()
468 if self.can_cut():
485 if self.can_cut():
469 self._control.textCursor().removeSelectedText()
486 self._control.textCursor().removeSelectedText()
470
487
471 def execute(self, source=None, hidden=False, interactive=False):
488 def execute(self, source=None, hidden=False, interactive=False):
472 """ Executes source or the input buffer, possibly prompting for more
489 """ Executes source or the input buffer, possibly prompting for more
473 input.
490 input.
474
491
475 Parameters:
492 Parameters:
476 -----------
493 -----------
477 source : str, optional
494 source : str, optional
478
495
479 The source to execute. If not specified, the input buffer will be
496 The source to execute. If not specified, the input buffer will be
480 used. If specified and 'hidden' is False, the input buffer will be
497 used. If specified and 'hidden' is False, the input buffer will be
481 replaced with the source before execution.
498 replaced with the source before execution.
482
499
483 hidden : bool, optional (default False)
500 hidden : bool, optional (default False)
484
501
485 If set, no output will be shown and the prompt will not be modified.
502 If set, no output will be shown and the prompt will not be modified.
486 In other words, it will be completely invisible to the user that
503 In other words, it will be completely invisible to the user that
487 an execution has occurred.
504 an execution has occurred.
488
505
489 interactive : bool, optional (default False)
506 interactive : bool, optional (default False)
490
507
491 Whether the console is to treat the source as having been manually
508 Whether the console is to treat the source as having been manually
492 entered by the user. The effect of this parameter depends on the
509 entered by the user. The effect of this parameter depends on the
493 subclass implementation.
510 subclass implementation.
494
511
495 Raises:
512 Raises:
496 -------
513 -------
497 RuntimeError
514 RuntimeError
498 If incomplete input is given and 'hidden' is True. In this case,
515 If incomplete input is given and 'hidden' is True. In this case,
499 it is not possible to prompt for more input.
516 it is not possible to prompt for more input.
500
517
501 Returns:
518 Returns:
502 --------
519 --------
503 A boolean indicating whether the source was executed.
520 A boolean indicating whether the source was executed.
504 """
521 """
505 # WARNING: The order in which things happen here is very particular, in
522 # WARNING: The order in which things happen here is very particular, in
506 # large part because our syntax highlighting is fragile. If you change
523 # large part because our syntax highlighting is fragile. If you change
507 # something, test carefully!
524 # something, test carefully!
508
525
509 # Decide what to execute.
526 # Decide what to execute.
510 if source is None:
527 if source is None:
511 source = self.input_buffer
528 source = self.input_buffer
512 if not hidden:
529 if not hidden:
513 # A newline is appended later, but it should be considered part
530 # A newline is appended later, but it should be considered part
514 # of the input buffer.
531 # of the input buffer.
515 source += '\n'
532 source += '\n'
516 elif not hidden:
533 elif not hidden:
517 self.input_buffer = source
534 self.input_buffer = source
518
535
519 # Execute the source or show a continuation prompt if it is incomplete.
536 # Execute the source or show a continuation prompt if it is incomplete.
520 complete = self._is_complete(source, interactive)
537 complete = self._is_complete(source, interactive)
521 if hidden:
538 if hidden:
522 if complete:
539 if complete:
523 self._execute(source, hidden)
540 self._execute(source, hidden)
524 else:
541 else:
525 error = 'Incomplete noninteractive input: "%s"'
542 error = 'Incomplete noninteractive input: "%s"'
526 raise RuntimeError(error % source)
543 raise RuntimeError(error % source)
527 else:
544 else:
528 if complete:
545 if complete:
529 self._append_plain_text('\n')
546 self._append_plain_text('\n')
530 self._input_buffer_executing = self.input_buffer
547 self._input_buffer_executing = self.input_buffer
531 self._executing = True
548 self._executing = True
532 self._prompt_finished()
549 self._prompt_finished()
533
550
534 # The maximum block count is only in effect during execution.
551 # The maximum block count is only in effect during execution.
535 # This ensures that _prompt_pos does not become invalid due to
552 # This ensures that _prompt_pos does not become invalid due to
536 # text truncation.
553 # text truncation.
537 self._control.document().setMaximumBlockCount(self.buffer_size)
554 self._control.document().setMaximumBlockCount(self.buffer_size)
538
555
539 # Setting a positive maximum block count will automatically
556 # Setting a positive maximum block count will automatically
540 # disable the undo/redo history, but just to be safe:
557 # disable the undo/redo history, but just to be safe:
541 self._control.setUndoRedoEnabled(False)
558 self._control.setUndoRedoEnabled(False)
542
559
543 # Perform actual execution.
560 # Perform actual execution.
544 self._execute(source, hidden)
561 self._execute(source, hidden)
545
562
546 else:
563 else:
547 # Do this inside an edit block so continuation prompts are
564 # Do this inside an edit block so continuation prompts are
548 # removed seamlessly via undo/redo.
565 # removed seamlessly via undo/redo.
549 cursor = self._get_end_cursor()
566 cursor = self._get_end_cursor()
550 cursor.beginEditBlock()
567 cursor.beginEditBlock()
551 cursor.insertText('\n')
568 cursor.insertText('\n')
552 self._insert_continuation_prompt(cursor)
569 self._insert_continuation_prompt(cursor)
553 cursor.endEditBlock()
570 cursor.endEditBlock()
554
571
555 # Do not do this inside the edit block. It works as expected
572 # Do not do this inside the edit block. It works as expected
556 # when using a QPlainTextEdit control, but does not have an
573 # when using a QPlainTextEdit control, but does not have an
557 # effect when using a QTextEdit. I believe this is a Qt bug.
574 # effect when using a QTextEdit. I believe this is a Qt bug.
558 self._control.moveCursor(QtGui.QTextCursor.End)
575 self._control.moveCursor(QtGui.QTextCursor.End)
559
576
560 return complete
577 return complete
561
578
562 def export_html(self):
579 def export_html(self):
563 """ Shows a dialog to export HTML/XML in various formats.
580 """ Shows a dialog to export HTML/XML in various formats.
564 """
581 """
565 self._html_exporter.export()
582 self._html_exporter.export()
566
583
567 def _get_input_buffer(self, force=False):
584 def _get_input_buffer(self, force=False):
568 """ The text that the user has entered entered at the current prompt.
585 """ The text that the user has entered entered at the current prompt.
569
586
570 If the console is currently executing, the text that is executing will
587 If the console is currently executing, the text that is executing will
571 always be returned.
588 always be returned.
572 """
589 """
573 # If we're executing, the input buffer may not even exist anymore due to
590 # If we're executing, the input buffer may not even exist anymore due to
574 # the limit imposed by 'buffer_size'. Therefore, we store it.
591 # the limit imposed by 'buffer_size'. Therefore, we store it.
575 if self._executing and not force:
592 if self._executing and not force:
576 return self._input_buffer_executing
593 return self._input_buffer_executing
577
594
578 cursor = self._get_end_cursor()
595 cursor = self._get_end_cursor()
579 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
596 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
580 input_buffer = cursor.selection().toPlainText()
597 input_buffer = cursor.selection().toPlainText()
581
598
582 # Strip out continuation prompts.
599 # Strip out continuation prompts.
583 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
600 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
584
601
585 def _set_input_buffer(self, string):
602 def _set_input_buffer(self, string):
586 """ Sets the text in the input buffer.
603 """ Sets the text in the input buffer.
587
604
588 If the console is currently executing, this call has no *immediate*
605 If the console is currently executing, this call has no *immediate*
589 effect. When the execution is finished, the input buffer will be updated
606 effect. When the execution is finished, the input buffer will be updated
590 appropriately.
607 appropriately.
591 """
608 """
592 # If we're executing, store the text for later.
609 # If we're executing, store the text for later.
593 if self._executing:
610 if self._executing:
594 self._input_buffer_pending = string
611 self._input_buffer_pending = string
595 return
612 return
596
613
597 # Remove old text.
614 # Remove old text.
598 cursor = self._get_end_cursor()
615 cursor = self._get_end_cursor()
599 cursor.beginEditBlock()
616 cursor.beginEditBlock()
600 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
617 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
601 cursor.removeSelectedText()
618 cursor.removeSelectedText()
602
619
603 # Insert new text with continuation prompts.
620 # Insert new text with continuation prompts.
604 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
621 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
605 cursor.endEditBlock()
622 cursor.endEditBlock()
606 self._control.moveCursor(QtGui.QTextCursor.End)
623 self._control.moveCursor(QtGui.QTextCursor.End)
607
624
608 input_buffer = property(_get_input_buffer, _set_input_buffer)
625 input_buffer = property(_get_input_buffer, _set_input_buffer)
609
626
610 def _get_font(self):
627 def _get_font(self):
611 """ The base font being used by the ConsoleWidget.
628 """ The base font being used by the ConsoleWidget.
612 """
629 """
613 return self._control.document().defaultFont()
630 return self._control.document().defaultFont()
614
631
615 def _set_font(self, font):
632 def _set_font(self, font):
616 """ Sets the base font for the ConsoleWidget to the specified QFont.
633 """ Sets the base font for the ConsoleWidget to the specified QFont.
617 """
634 """
618 font_metrics = QtGui.QFontMetrics(font)
635 font_metrics = QtGui.QFontMetrics(font)
619 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
636 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
620
637
621 self._completion_widget.setFont(font)
638 self._completion_widget.setFont(font)
622 self._control.document().setDefaultFont(font)
639 self._control.document().setDefaultFont(font)
623 if self._page_control:
640 if self._page_control:
624 self._page_control.document().setDefaultFont(font)
641 self._page_control.document().setDefaultFont(font)
625
642
626 self.font_changed.emit(font)
643 self.font_changed.emit(font)
627
644
628 font = property(_get_font, _set_font)
645 font = property(_get_font, _set_font)
629
646
630 def paste(self, mode=QtGui.QClipboard.Clipboard):
647 def paste(self, mode=QtGui.QClipboard.Clipboard):
631 """ Paste the contents of the clipboard into the input region.
648 """ Paste the contents of the clipboard into the input region.
632
649
633 Parameters:
650 Parameters:
634 -----------
651 -----------
635 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
652 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
636
653
637 Controls which part of the system clipboard is used. This can be
654 Controls which part of the system clipboard is used. This can be
638 used to access the selection clipboard in X11 and the Find buffer
655 used to access the selection clipboard in X11 and the Find buffer
639 in Mac OS. By default, the regular clipboard is used.
656 in Mac OS. By default, the regular clipboard is used.
640 """
657 """
641 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
658 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
642 # Make sure the paste is safe.
659 # Make sure the paste is safe.
643 self._keep_cursor_in_buffer()
660 self._keep_cursor_in_buffer()
644 cursor = self._control.textCursor()
661 cursor = self._control.textCursor()
645
662
646 # Remove any trailing newline, which confuses the GUI and forces the
663 # Remove any trailing newline, which confuses the GUI and forces the
647 # user to backspace.
664 # user to backspace.
648 text = QtGui.QApplication.clipboard().text(mode).rstrip()
665 text = QtGui.QApplication.clipboard().text(mode).rstrip()
649 self._insert_plain_text_into_buffer(cursor, dedent(text))
666 self._insert_plain_text_into_buffer(cursor, dedent(text))
650
667
651 def print_(self, printer = None):
668 def print_(self, printer = None):
652 """ Print the contents of the ConsoleWidget to the specified QPrinter.
669 """ Print the contents of the ConsoleWidget to the specified QPrinter.
653 """
670 """
654 if (not printer):
671 if (not printer):
655 printer = QtGui.QPrinter()
672 printer = QtGui.QPrinter()
656 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
673 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
657 return
674 return
658 self._control.print_(printer)
675 self._control.print_(printer)
659
676
660 def prompt_to_top(self):
677 def prompt_to_top(self):
661 """ Moves the prompt to the top of the viewport.
678 """ Moves the prompt to the top of the viewport.
662 """
679 """
663 if not self._executing:
680 if not self._executing:
664 prompt_cursor = self._get_prompt_cursor()
681 prompt_cursor = self._get_prompt_cursor()
665 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
682 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
666 self._set_cursor(prompt_cursor)
683 self._set_cursor(prompt_cursor)
667 self._set_top_cursor(prompt_cursor)
684 self._set_top_cursor(prompt_cursor)
668
685
669 def redo(self):
686 def redo(self):
670 """ Redo the last operation. If there is no operation to redo, nothing
687 """ Redo the last operation. If there is no operation to redo, nothing
671 happens.
688 happens.
672 """
689 """
673 self._control.redo()
690 self._control.redo()
674
691
675 def reset_font(self):
692 def reset_font(self):
676 """ Sets the font to the default fixed-width font for this platform.
693 """ Sets the font to the default fixed-width font for this platform.
677 """
694 """
678 if sys.platform == 'win32':
695 if sys.platform == 'win32':
679 # Consolas ships with Vista/Win7, fallback to Courier if needed
696 # Consolas ships with Vista/Win7, fallback to Courier if needed
680 fallback = 'Courier'
697 fallback = 'Courier'
681 elif sys.platform == 'darwin':
698 elif sys.platform == 'darwin':
682 # OSX always has Monaco
699 # OSX always has Monaco
683 fallback = 'Monaco'
700 fallback = 'Monaco'
684 else:
701 else:
685 # Monospace should always exist
702 # Monospace should always exist
686 fallback = 'Monospace'
703 fallback = 'Monospace'
687 font = get_font(self.font_family, fallback)
704 font = get_font(self.font_family, fallback)
688 if self.font_size:
705 if self.font_size:
689 font.setPointSize(self.font_size)
706 font.setPointSize(self.font_size)
690 else:
707 else:
691 font.setPointSize(QtGui.qApp.font().pointSize())
708 font.setPointSize(QtGui.qApp.font().pointSize())
692 font.setStyleHint(QtGui.QFont.TypeWriter)
709 font.setStyleHint(QtGui.QFont.TypeWriter)
693 self._set_font(font)
710 self._set_font(font)
694
711
695 def change_font_size(self, delta):
712 def change_font_size(self, delta):
696 """Change the font size by the specified amount (in points).
713 """Change the font size by the specified amount (in points).
697 """
714 """
698 font = self.font
715 font = self.font
699 size = max(font.pointSize() + delta, 1) # minimum 1 point
716 size = max(font.pointSize() + delta, 1) # minimum 1 point
700 font.setPointSize(size)
717 font.setPointSize(size)
701 self._set_font(font)
718 self._set_font(font)
702
719
703 def _increase_font_size(self):
720 def _increase_font_size(self):
704 self.change_font_size(1)
721 self.change_font_size(1)
705
722
706 def _decrease_font_size(self):
723 def _decrease_font_size(self):
707 self.change_font_size(-1)
724 self.change_font_size(-1)
708
725
709 def select_all(self):
726 def select_all(self):
710 """ Selects all the text in the buffer.
727 """ Selects all the text in the buffer.
711 """
728 """
712 self._control.selectAll()
729 self._control.selectAll()
713
730
714 def _get_tab_width(self):
731 def _get_tab_width(self):
715 """ The width (in terms of space characters) for tab characters.
732 """ The width (in terms of space characters) for tab characters.
716 """
733 """
717 return self._tab_width
734 return self._tab_width
718
735
719 def _set_tab_width(self, tab_width):
736 def _set_tab_width(self, tab_width):
720 """ Sets the width (in terms of space characters) for tab characters.
737 """ Sets the width (in terms of space characters) for tab characters.
721 """
738 """
722 font_metrics = QtGui.QFontMetrics(self.font)
739 font_metrics = QtGui.QFontMetrics(self.font)
723 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
740 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
724
741
725 self._tab_width = tab_width
742 self._tab_width = tab_width
726
743
727 tab_width = property(_get_tab_width, _set_tab_width)
744 tab_width = property(_get_tab_width, _set_tab_width)
728
745
729 def undo(self):
746 def undo(self):
730 """ Undo the last operation. If there is no operation to undo, nothing
747 """ Undo the last operation. If there is no operation to undo, nothing
731 happens.
748 happens.
732 """
749 """
733 self._control.undo()
750 self._control.undo()
734
751
735 #---------------------------------------------------------------------------
752 #---------------------------------------------------------------------------
736 # 'ConsoleWidget' abstract interface
753 # 'ConsoleWidget' abstract interface
737 #---------------------------------------------------------------------------
754 #---------------------------------------------------------------------------
738
755
739 def _is_complete(self, source, interactive):
756 def _is_complete(self, source, interactive):
740 """ Returns whether 'source' can be executed. When triggered by an
757 """ Returns whether 'source' can be executed. When triggered by an
741 Enter/Return key press, 'interactive' is True; otherwise, it is
758 Enter/Return key press, 'interactive' is True; otherwise, it is
742 False.
759 False.
743 """
760 """
744 raise NotImplementedError
761 raise NotImplementedError
745
762
746 def _execute(self, source, hidden):
763 def _execute(self, source, hidden):
747 """ Execute 'source'. If 'hidden', do not show any output.
764 """ Execute 'source'. If 'hidden', do not show any output.
748 """
765 """
749 raise NotImplementedError
766 raise NotImplementedError
750
767
751 def _prompt_started_hook(self):
768 def _prompt_started_hook(self):
752 """ Called immediately after a new prompt is displayed.
769 """ Called immediately after a new prompt is displayed.
753 """
770 """
754 pass
771 pass
755
772
756 def _prompt_finished_hook(self):
773 def _prompt_finished_hook(self):
757 """ Called immediately after a prompt is finished, i.e. when some input
774 """ Called immediately after a prompt is finished, i.e. when some input
758 will be processed and a new prompt displayed.
775 will be processed and a new prompt displayed.
759 """
776 """
760 pass
777 pass
761
778
762 def _up_pressed(self, shift_modifier):
779 def _up_pressed(self, shift_modifier):
763 """ Called when the up key is pressed. Returns whether to continue
780 """ Called when the up key is pressed. Returns whether to continue
764 processing the event.
781 processing the event.
765 """
782 """
766 return True
783 return True
767
784
768 def _down_pressed(self, shift_modifier):
785 def _down_pressed(self, shift_modifier):
769 """ Called when the down key is pressed. Returns whether to continue
786 """ Called when the down key is pressed. Returns whether to continue
770 processing the event.
787 processing the event.
771 """
788 """
772 return True
789 return True
773
790
774 def _tab_pressed(self):
791 def _tab_pressed(self):
775 """ Called when the tab key is pressed. Returns whether to continue
792 """ Called when the tab key is pressed. Returns whether to continue
776 processing the event.
793 processing the event.
777 """
794 """
778 return False
795 return False
779
796
780 #--------------------------------------------------------------------------
797 #--------------------------------------------------------------------------
781 # 'ConsoleWidget' protected interface
798 # 'ConsoleWidget' protected interface
782 #--------------------------------------------------------------------------
799 #--------------------------------------------------------------------------
783
800
784 def _append_custom(self, insert, input, before_prompt=False):
801 def _append_custom(self, insert, input, before_prompt=False):
785 """ A low-level method for appending content to the end of the buffer.
802 """ A low-level method for appending content to the end of the buffer.
786
803
787 If 'before_prompt' is enabled, the content will be inserted before the
804 If 'before_prompt' is enabled, the content will be inserted before the
788 current prompt, if there is one.
805 current prompt, if there is one.
789 """
806 """
790 # Determine where to insert the content.
807 # Determine where to insert the content.
791 cursor = self._control.textCursor()
808 cursor = self._control.textCursor()
792 if before_prompt and (self._reading or not self._executing):
809 if before_prompt and (self._reading or not self._executing):
793 cursor.setPosition(self._append_before_prompt_pos)
810 cursor.setPosition(self._append_before_prompt_pos)
794 else:
811 else:
795 cursor.movePosition(QtGui.QTextCursor.End)
812 cursor.movePosition(QtGui.QTextCursor.End)
796 start_pos = cursor.position()
813 start_pos = cursor.position()
797
814
798 # Perform the insertion.
815 # Perform the insertion.
799 result = insert(cursor, input)
816 result = insert(cursor, input)
800
817
801 # Adjust the prompt position if we have inserted before it. This is safe
818 # Adjust the prompt position if we have inserted before it. This is safe
802 # because buffer truncation is disabled when not executing.
819 # because buffer truncation is disabled when not executing.
803 if before_prompt and not self._executing:
820 if before_prompt and not self._executing:
804 diff = cursor.position() - start_pos
821 diff = cursor.position() - start_pos
805 self._append_before_prompt_pos += diff
822 self._append_before_prompt_pos += diff
806 self._prompt_pos += diff
823 self._prompt_pos += diff
807
824
808 return result
825 return result
809
826
810 def _append_html(self, html, before_prompt=False):
827 def _append_html(self, html, before_prompt=False):
811 """ Appends HTML at the end of the console buffer.
828 """ Appends HTML at the end of the console buffer.
812 """
829 """
813 self._append_custom(self._insert_html, html, before_prompt)
830 self._append_custom(self._insert_html, html, before_prompt)
814
831
815 def _append_html_fetching_plain_text(self, html, before_prompt=False):
832 def _append_html_fetching_plain_text(self, html, before_prompt=False):
816 """ Appends HTML, then returns the plain text version of it.
833 """ Appends HTML, then returns the plain text version of it.
817 """
834 """
818 return self._append_custom(self._insert_html_fetching_plain_text,
835 return self._append_custom(self._insert_html_fetching_plain_text,
819 html, before_prompt)
836 html, before_prompt)
820
837
821 def _append_plain_text(self, text, before_prompt=False):
838 def _append_plain_text(self, text, before_prompt=False):
822 """ Appends plain text, processing ANSI codes if enabled.
839 """ Appends plain text, processing ANSI codes if enabled.
823 """
840 """
824 self._append_custom(self._insert_plain_text, text, before_prompt)
841 self._append_custom(self._insert_plain_text, text, before_prompt)
825
842
826 def _cancel_text_completion(self):
843 def _cancel_completion(self):
827 """ If text completion is progress, cancel it.
844 """ If text completion is progress, cancel it.
828 """
845 """
829 if self._text_completing_pos:
846 self._completion_widget._cancel_completion()
830 self._clear_temporary_buffer()
831 self._text_completing_pos = 0
832
847
833 def _clear_temporary_buffer(self):
848 def _clear_temporary_buffer(self):
834 """ Clears the "temporary text" buffer, i.e. all the text following
849 """ Clears the "temporary text" buffer, i.e. all the text following
835 the prompt region.
850 the prompt region.
836 """
851 """
837 # Select and remove all text below the input buffer.
852 # Select and remove all text below the input buffer.
838 cursor = self._get_prompt_cursor()
853 cursor = self._get_prompt_cursor()
839 prompt = self._continuation_prompt.lstrip()
854 prompt = self._continuation_prompt.lstrip()
840 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
855 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
841 temp_cursor = QtGui.QTextCursor(cursor)
856 temp_cursor = QtGui.QTextCursor(cursor)
842 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
857 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
843 text = temp_cursor.selection().toPlainText().lstrip()
858 text = temp_cursor.selection().toPlainText().lstrip()
844 if not text.startswith(prompt):
859 if not text.startswith(prompt):
845 break
860 break
846 else:
861 else:
847 # We've reached the end of the input buffer and no text follows.
862 # We've reached the end of the input buffer and no text follows.
848 return
863 return
849 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
864 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
850 cursor.movePosition(QtGui.QTextCursor.End,
865 cursor.movePosition(QtGui.QTextCursor.End,
851 QtGui.QTextCursor.KeepAnchor)
866 QtGui.QTextCursor.KeepAnchor)
852 cursor.removeSelectedText()
867 cursor.removeSelectedText()
853
868
854 # After doing this, we have no choice but to clear the undo/redo
869 # After doing this, we have no choice but to clear the undo/redo
855 # history. Otherwise, the text is not "temporary" at all, because it
870 # history. Otherwise, the text is not "temporary" at all, because it
856 # can be recalled with undo/redo. Unfortunately, Qt does not expose
871 # can be recalled with undo/redo. Unfortunately, Qt does not expose
857 # fine-grained control to the undo/redo system.
872 # fine-grained control to the undo/redo system.
858 if self._control.isUndoRedoEnabled():
873 if self._control.isUndoRedoEnabled():
859 self._control.setUndoRedoEnabled(False)
874 self._control.setUndoRedoEnabled(False)
860 self._control.setUndoRedoEnabled(True)
875 self._control.setUndoRedoEnabled(True)
861
876
862 def _complete_with_items(self, cursor, items):
877 def _complete_with_items(self, cursor, items):
863 """ Performs completion with 'items' at the specified cursor location.
878 """ Performs completion with 'items' at the specified cursor location.
864 """
879 """
865 self._cancel_text_completion()
880 self._cancel_completion()
866
881
867 if len(items) == 1:
882 if len(items) == 1:
868 cursor.setPosition(self._control.textCursor().position(),
883 cursor.setPosition(self._control.textCursor().position(),
869 QtGui.QTextCursor.KeepAnchor)
884 QtGui.QTextCursor.KeepAnchor)
870 cursor.insertText(items[0])
885 cursor.insertText(items[0])
871
886
872 elif len(items) > 1:
887 elif len(items) > 1:
873 current_pos = self._control.textCursor().position()
888 current_pos = self._control.textCursor().position()
874 prefix = commonprefix(items)
889 prefix = commonprefix(items)
875 if prefix:
890 if prefix:
876 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
891 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
877 cursor.insertText(prefix)
892 cursor.insertText(prefix)
878 current_pos = cursor.position()
893 current_pos = cursor.position()
879
894
880 if self.gui_completion:
895 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
881 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
896 self._completion_widget.show_items(cursor, items)
882 self._completion_widget.show_items(cursor, items)
897 self._is_completing = True
883 else:
898
884 cursor.beginEditBlock()
899
885 self._append_plain_text('\n')
900 def _fill_temporary_buffer(self, cursor, text, html=False):
886 self._page(self._format_as_columns(items))
901 """fill the area below the active editting zone with text"""
887 cursor.endEditBlock()
902
903 current_pos = self._control.textCursor().position()
904
905 cursor.beginEditBlock()
906 self._append_plain_text('\n')
907 self._page(text, html=html)
908 cursor.endEditBlock()
909
910 cursor.setPosition(current_pos)
911 self._control.moveCursor(QtGui.QTextCursor.End)
912 self._control.setTextCursor(cursor)
888
913
889 cursor.setPosition(current_pos)
890 self._control.moveCursor(QtGui.QTextCursor.End)
891 self._control.setTextCursor(cursor)
892 self._text_completing_pos = current_pos
893
914
894 def _context_menu_make(self, pos):
915 def _context_menu_make(self, pos):
895 """ Creates a context menu for the given QPoint (in widget coordinates).
916 """ Creates a context menu for the given QPoint (in widget coordinates).
896 """
917 """
897 menu = QtGui.QMenu(self)
918 menu = QtGui.QMenu(self)
898
919
899 self.cut_action = menu.addAction('Cut', self.cut)
920 self.cut_action = menu.addAction('Cut', self.cut)
900 self.cut_action.setEnabled(self.can_cut())
921 self.cut_action.setEnabled(self.can_cut())
901 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
922 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
902
923
903 self.copy_action = menu.addAction('Copy', self.copy)
924 self.copy_action = menu.addAction('Copy', self.copy)
904 self.copy_action.setEnabled(self.can_copy())
925 self.copy_action.setEnabled(self.can_copy())
905 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
926 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
906
927
907 self.paste_action = menu.addAction('Paste', self.paste)
928 self.paste_action = menu.addAction('Paste', self.paste)
908 self.paste_action.setEnabled(self.can_paste())
929 self.paste_action.setEnabled(self.can_paste())
909 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
930 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
910
931
911 menu.addSeparator()
932 menu.addSeparator()
912 menu.addAction(self.select_all_action)
933 menu.addAction(self.select_all_action)
913
934
914 menu.addSeparator()
935 menu.addSeparator()
915 menu.addAction(self.export_action)
936 menu.addAction(self.export_action)
916 menu.addAction(self.print_action)
937 menu.addAction(self.print_action)
917
938
918 return menu
939 return menu
919
940
920 def _control_key_down(self, modifiers, include_command=False):
941 def _control_key_down(self, modifiers, include_command=False):
921 """ Given a KeyboardModifiers flags object, return whether the Control
942 """ Given a KeyboardModifiers flags object, return whether the Control
922 key is down.
943 key is down.
923
944
924 Parameters:
945 Parameters:
925 -----------
946 -----------
926 include_command : bool, optional (default True)
947 include_command : bool, optional (default True)
927 Whether to treat the Command key as a (mutually exclusive) synonym
948 Whether to treat the Command key as a (mutually exclusive) synonym
928 for Control when in Mac OS.
949 for Control when in Mac OS.
929 """
950 """
930 # Note that on Mac OS, ControlModifier corresponds to the Command key
951 # Note that on Mac OS, ControlModifier corresponds to the Command key
931 # while MetaModifier corresponds to the Control key.
952 # while MetaModifier corresponds to the Control key.
932 if sys.platform == 'darwin':
953 if sys.platform == 'darwin':
933 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
954 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
934 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
955 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
935 else:
956 else:
936 return bool(modifiers & QtCore.Qt.ControlModifier)
957 return bool(modifiers & QtCore.Qt.ControlModifier)
937
958
938 def _create_control(self):
959 def _create_control(self):
939 """ Creates and connects the underlying text widget.
960 """ Creates and connects the underlying text widget.
940 """
961 """
941 # Create the underlying control.
962 # Create the underlying control.
942 if self.kind == 'plain':
963 if self.kind == 'plain':
943 control = QtGui.QPlainTextEdit()
964 control = QtGui.QPlainTextEdit()
944 elif self.kind == 'rich':
965 elif self.kind == 'rich':
945 control = QtGui.QTextEdit()
966 control = QtGui.QTextEdit()
946 control.setAcceptRichText(False)
967 control.setAcceptRichText(False)
947
968
948 # Install event filters. The filter on the viewport is needed for
969 # Install event filters. The filter on the viewport is needed for
949 # mouse events and drag events.
970 # mouse events and drag events.
950 control.installEventFilter(self)
971 control.installEventFilter(self)
951 control.viewport().installEventFilter(self)
972 control.viewport().installEventFilter(self)
952
973
953 # Connect signals.
974 # Connect signals.
954 control.cursorPositionChanged.connect(self._cursor_position_changed)
955 control.customContextMenuRequested.connect(
975 control.customContextMenuRequested.connect(
956 self._custom_context_menu_requested)
976 self._custom_context_menu_requested)
957 control.copyAvailable.connect(self.copy_available)
977 control.copyAvailable.connect(self.copy_available)
958 control.redoAvailable.connect(self.redo_available)
978 control.redoAvailable.connect(self.redo_available)
959 control.undoAvailable.connect(self.undo_available)
979 control.undoAvailable.connect(self.undo_available)
960
980
961 # Hijack the document size change signal to prevent Qt from adjusting
981 # Hijack the document size change signal to prevent Qt from adjusting
962 # the viewport's scrollbar. We are relying on an implementation detail
982 # the viewport's scrollbar. We are relying on an implementation detail
963 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
983 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
964 # this functionality we cannot create a nice terminal interface.
984 # this functionality we cannot create a nice terminal interface.
965 layout = control.document().documentLayout()
985 layout = control.document().documentLayout()
966 layout.documentSizeChanged.disconnect()
986 layout.documentSizeChanged.disconnect()
967 layout.documentSizeChanged.connect(self._adjust_scrollbars)
987 layout.documentSizeChanged.connect(self._adjust_scrollbars)
968
988
969 # Configure the control.
989 # Configure the control.
970 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
990 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
971 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
991 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
972 control.setReadOnly(True)
992 control.setReadOnly(True)
973 control.setUndoRedoEnabled(False)
993 control.setUndoRedoEnabled(False)
974 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
994 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
975 return control
995 return control
976
996
977 def _create_page_control(self):
997 def _create_page_control(self):
978 """ Creates and connects the underlying paging widget.
998 """ Creates and connects the underlying paging widget.
979 """
999 """
980 if self.kind == 'plain':
1000 if self.kind == 'plain':
981 control = QtGui.QPlainTextEdit()
1001 control = QtGui.QPlainTextEdit()
982 elif self.kind == 'rich':
1002 elif self.kind == 'rich':
983 control = QtGui.QTextEdit()
1003 control = QtGui.QTextEdit()
984 control.installEventFilter(self)
1004 control.installEventFilter(self)
985 viewport = control.viewport()
1005 viewport = control.viewport()
986 viewport.installEventFilter(self)
1006 viewport.installEventFilter(self)
987 control.setReadOnly(True)
1007 control.setReadOnly(True)
988 control.setUndoRedoEnabled(False)
1008 control.setUndoRedoEnabled(False)
989 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1009 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
990 return control
1010 return control
991
1011
992 def _event_filter_console_keypress(self, event):
1012 def _event_filter_console_keypress(self, event):
993 """ Filter key events for the underlying text widget to create a
1013 """ Filter key events for the underlying text widget to create a
994 console-like interface.
1014 console-like interface.
995 """
1015 """
996 intercepted = False
1016 intercepted = False
997 cursor = self._control.textCursor()
1017 cursor = self._control.textCursor()
998 position = cursor.position()
1018 position = cursor.position()
999 key = event.key()
1019 key = event.key()
1000 ctrl_down = self._control_key_down(event.modifiers())
1020 ctrl_down = self._control_key_down(event.modifiers())
1001 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1021 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1002 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1022 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1003
1023
1004 #------ Special sequences ----------------------------------------------
1024 #------ Special sequences ----------------------------------------------
1005
1025
1006 if event.matches(QtGui.QKeySequence.Copy):
1026 if event.matches(QtGui.QKeySequence.Copy):
1007 self.copy()
1027 self.copy()
1008 intercepted = True
1028 intercepted = True
1009
1029
1010 elif event.matches(QtGui.QKeySequence.Cut):
1030 elif event.matches(QtGui.QKeySequence.Cut):
1011 self.cut()
1031 self.cut()
1012 intercepted = True
1032 intercepted = True
1013
1033
1014 elif event.matches(QtGui.QKeySequence.Paste):
1034 elif event.matches(QtGui.QKeySequence.Paste):
1015 self.paste()
1035 self.paste()
1016 intercepted = True
1036 intercepted = True
1017
1037
1018 #------ Special modifier logic -----------------------------------------
1038 #------ Special modifier logic -----------------------------------------
1019
1039
1020 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1040 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1021 intercepted = True
1041 intercepted = True
1022
1042
1023 # Special handling when tab completing in text mode.
1043 # Special handling when tab completing in text mode.
1024 self._cancel_text_completion()
1044 self._cancel_completion()
1025
1045
1026 if self._in_buffer(position):
1046 if self._in_buffer(position):
1027 # Special handling when a reading a line of raw input.
1047 # Special handling when a reading a line of raw input.
1028 if self._reading:
1048 if self._reading:
1029 self._append_plain_text('\n')
1049 self._append_plain_text('\n')
1030 self._reading = False
1050 self._reading = False
1031 if self._reading_callback:
1051 if self._reading_callback:
1032 self._reading_callback()
1052 self._reading_callback()
1033
1053
1034 # If the input buffer is a single line or there is only
1054 # If the input buffer is a single line or there is only
1035 # whitespace after the cursor, execute. Otherwise, split the
1055 # whitespace after the cursor, execute. Otherwise, split the
1036 # line with a continuation prompt.
1056 # line with a continuation prompt.
1037 elif not self._executing:
1057 elif not self._executing:
1038 cursor.movePosition(QtGui.QTextCursor.End,
1058 cursor.movePosition(QtGui.QTextCursor.End,
1039 QtGui.QTextCursor.KeepAnchor)
1059 QtGui.QTextCursor.KeepAnchor)
1040 at_end = len(cursor.selectedText().strip()) == 0
1060 at_end = len(cursor.selectedText().strip()) == 0
1041 single_line = (self._get_end_cursor().blockNumber() ==
1061 single_line = (self._get_end_cursor().blockNumber() ==
1042 self._get_prompt_cursor().blockNumber())
1062 self._get_prompt_cursor().blockNumber())
1043 if (at_end or shift_down or single_line) and not ctrl_down:
1063 if (at_end or shift_down or single_line) and not ctrl_down:
1044 self.execute(interactive = not shift_down)
1064 self.execute(interactive = not shift_down)
1045 else:
1065 else:
1046 # Do this inside an edit block for clean undo/redo.
1066 # Do this inside an edit block for clean undo/redo.
1047 cursor.beginEditBlock()
1067 cursor.beginEditBlock()
1048 cursor.setPosition(position)
1068 cursor.setPosition(position)
1049 cursor.insertText('\n')
1069 cursor.insertText('\n')
1050 self._insert_continuation_prompt(cursor)
1070 self._insert_continuation_prompt(cursor)
1051 cursor.endEditBlock()
1071 cursor.endEditBlock()
1052
1072
1053 # Ensure that the whole input buffer is visible.
1073 # Ensure that the whole input buffer is visible.
1054 # FIXME: This will not be usable if the input buffer is
1074 # FIXME: This will not be usable if the input buffer is
1055 # taller than the console widget.
1075 # taller than the console widget.
1056 self._control.moveCursor(QtGui.QTextCursor.End)
1076 self._control.moveCursor(QtGui.QTextCursor.End)
1057 self._control.setTextCursor(cursor)
1077 self._control.setTextCursor(cursor)
1058
1078
1059 #------ Control/Cmd modifier -------------------------------------------
1079 #------ Control/Cmd modifier -------------------------------------------
1060
1080
1061 elif ctrl_down:
1081 elif ctrl_down:
1062 if key == QtCore.Qt.Key_G:
1082 if key == QtCore.Qt.Key_G:
1063 self._keyboard_quit()
1083 self._keyboard_quit()
1064 intercepted = True
1084 intercepted = True
1065
1085
1066 elif key == QtCore.Qt.Key_K:
1086 elif key == QtCore.Qt.Key_K:
1067 if self._in_buffer(position):
1087 if self._in_buffer(position):
1068 cursor.clearSelection()
1088 cursor.clearSelection()
1069 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1089 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1070 QtGui.QTextCursor.KeepAnchor)
1090 QtGui.QTextCursor.KeepAnchor)
1071 if not cursor.hasSelection():
1091 if not cursor.hasSelection():
1072 # Line deletion (remove continuation prompt)
1092 # Line deletion (remove continuation prompt)
1073 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1093 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1074 QtGui.QTextCursor.KeepAnchor)
1094 QtGui.QTextCursor.KeepAnchor)
1075 cursor.movePosition(QtGui.QTextCursor.Right,
1095 cursor.movePosition(QtGui.QTextCursor.Right,
1076 QtGui.QTextCursor.KeepAnchor,
1096 QtGui.QTextCursor.KeepAnchor,
1077 len(self._continuation_prompt))
1097 len(self._continuation_prompt))
1078 self._kill_ring.kill_cursor(cursor)
1098 self._kill_ring.kill_cursor(cursor)
1079 self._set_cursor(cursor)
1099 self._set_cursor(cursor)
1080 intercepted = True
1100 intercepted = True
1081
1101
1082 elif key == QtCore.Qt.Key_L:
1102 elif key == QtCore.Qt.Key_L:
1083 self.prompt_to_top()
1103 self.prompt_to_top()
1084 intercepted = True
1104 intercepted = True
1085
1105
1086 elif key == QtCore.Qt.Key_O:
1106 elif key == QtCore.Qt.Key_O:
1087 if self._page_control and self._page_control.isVisible():
1107 if self._page_control and self._page_control.isVisible():
1088 self._page_control.setFocus()
1108 self._page_control.setFocus()
1089 intercepted = True
1109 intercepted = True
1090
1110
1091 elif key == QtCore.Qt.Key_U:
1111 elif key == QtCore.Qt.Key_U:
1092 if self._in_buffer(position):
1112 if self._in_buffer(position):
1093 cursor.clearSelection()
1113 cursor.clearSelection()
1094 start_line = cursor.blockNumber()
1114 start_line = cursor.blockNumber()
1095 if start_line == self._get_prompt_cursor().blockNumber():
1115 if start_line == self._get_prompt_cursor().blockNumber():
1096 offset = len(self._prompt)
1116 offset = len(self._prompt)
1097 else:
1117 else:
1098 offset = len(self._continuation_prompt)
1118 offset = len(self._continuation_prompt)
1099 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1119 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1100 QtGui.QTextCursor.KeepAnchor)
1120 QtGui.QTextCursor.KeepAnchor)
1101 cursor.movePosition(QtGui.QTextCursor.Right,
1121 cursor.movePosition(QtGui.QTextCursor.Right,
1102 QtGui.QTextCursor.KeepAnchor, offset)
1122 QtGui.QTextCursor.KeepAnchor, offset)
1103 self._kill_ring.kill_cursor(cursor)
1123 self._kill_ring.kill_cursor(cursor)
1104 self._set_cursor(cursor)
1124 self._set_cursor(cursor)
1105 intercepted = True
1125 intercepted = True
1106
1126
1107 elif key == QtCore.Qt.Key_Y:
1127 elif key == QtCore.Qt.Key_Y:
1108 self._keep_cursor_in_buffer()
1128 self._keep_cursor_in_buffer()
1109 self._kill_ring.yank()
1129 self._kill_ring.yank()
1110 intercepted = True
1130 intercepted = True
1111
1131
1112 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1132 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1113 if key == QtCore.Qt.Key_Backspace:
1133 if key == QtCore.Qt.Key_Backspace:
1114 cursor = self._get_word_start_cursor(position)
1134 cursor = self._get_word_start_cursor(position)
1115 else: # key == QtCore.Qt.Key_Delete
1135 else: # key == QtCore.Qt.Key_Delete
1116 cursor = self._get_word_end_cursor(position)
1136 cursor = self._get_word_end_cursor(position)
1117 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1137 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1118 self._kill_ring.kill_cursor(cursor)
1138 self._kill_ring.kill_cursor(cursor)
1119 intercepted = True
1139 intercepted = True
1120
1140
1121 elif key == QtCore.Qt.Key_D:
1141 elif key == QtCore.Qt.Key_D:
1122 if len(self.input_buffer) == 0:
1142 if len(self.input_buffer) == 0:
1123 self.exit_requested.emit(self)
1143 self.exit_requested.emit(self)
1124 else:
1144 else:
1125 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1145 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1126 QtCore.Qt.Key_Delete,
1146 QtCore.Qt.Key_Delete,
1127 QtCore.Qt.NoModifier)
1147 QtCore.Qt.NoModifier)
1128 QtGui.qApp.sendEvent(self._control, new_event)
1148 QtGui.qApp.sendEvent(self._control, new_event)
1129 intercepted = True
1149 intercepted = True
1130
1150
1131 #------ Alt modifier ---------------------------------------------------
1151 #------ Alt modifier ---------------------------------------------------
1132
1152
1133 elif alt_down:
1153 elif alt_down:
1134 if key == QtCore.Qt.Key_B:
1154 if key == QtCore.Qt.Key_B:
1135 self._set_cursor(self._get_word_start_cursor(position))
1155 self._set_cursor(self._get_word_start_cursor(position))
1136 intercepted = True
1156 intercepted = True
1137
1157
1138 elif key == QtCore.Qt.Key_F:
1158 elif key == QtCore.Qt.Key_F:
1139 self._set_cursor(self._get_word_end_cursor(position))
1159 self._set_cursor(self._get_word_end_cursor(position))
1140 intercepted = True
1160 intercepted = True
1141
1161
1142 elif key == QtCore.Qt.Key_Y:
1162 elif key == QtCore.Qt.Key_Y:
1143 self._kill_ring.rotate()
1163 self._kill_ring.rotate()
1144 intercepted = True
1164 intercepted = True
1145
1165
1146 elif key == QtCore.Qt.Key_Backspace:
1166 elif key == QtCore.Qt.Key_Backspace:
1147 cursor = self._get_word_start_cursor(position)
1167 cursor = self._get_word_start_cursor(position)
1148 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1168 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1149 self._kill_ring.kill_cursor(cursor)
1169 self._kill_ring.kill_cursor(cursor)
1150 intercepted = True
1170 intercepted = True
1151
1171
1152 elif key == QtCore.Qt.Key_D:
1172 elif key == QtCore.Qt.Key_D:
1153 cursor = self._get_word_end_cursor(position)
1173 cursor = self._get_word_end_cursor(position)
1154 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1174 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1155 self._kill_ring.kill_cursor(cursor)
1175 self._kill_ring.kill_cursor(cursor)
1156 intercepted = True
1176 intercepted = True
1157
1177
1158 elif key == QtCore.Qt.Key_Delete:
1178 elif key == QtCore.Qt.Key_Delete:
1159 intercepted = True
1179 intercepted = True
1160
1180
1161 elif key == QtCore.Qt.Key_Greater:
1181 elif key == QtCore.Qt.Key_Greater:
1162 self._control.moveCursor(QtGui.QTextCursor.End)
1182 self._control.moveCursor(QtGui.QTextCursor.End)
1163 intercepted = True
1183 intercepted = True
1164
1184
1165 elif key == QtCore.Qt.Key_Less:
1185 elif key == QtCore.Qt.Key_Less:
1166 self._control.setTextCursor(self._get_prompt_cursor())
1186 self._control.setTextCursor(self._get_prompt_cursor())
1167 intercepted = True
1187 intercepted = True
1168
1188
1169 #------ No modifiers ---------------------------------------------------
1189 #------ No modifiers ---------------------------------------------------
1170
1190
1171 else:
1191 else:
1172 if shift_down:
1192 if shift_down:
1173 anchormode = QtGui.QTextCursor.KeepAnchor
1193 anchormode = QtGui.QTextCursor.KeepAnchor
1174 else:
1194 else:
1175 anchormode = QtGui.QTextCursor.MoveAnchor
1195 anchormode = QtGui.QTextCursor.MoveAnchor
1176
1196
1177 if key == QtCore.Qt.Key_Escape:
1197 if key == QtCore.Qt.Key_Escape:
1178 self._keyboard_quit()
1198 self._keyboard_quit()
1179 intercepted = True
1199 intercepted = True
1180
1200
1181 elif key == QtCore.Qt.Key_Up:
1201 elif key == QtCore.Qt.Key_Up:
1182 if self._reading or not self._up_pressed(shift_down):
1202 if self._reading or not self._up_pressed(shift_down):
1183 intercepted = True
1203 intercepted = True
1184 else:
1204 else:
1185 prompt_line = self._get_prompt_cursor().blockNumber()
1205 prompt_line = self._get_prompt_cursor().blockNumber()
1186 intercepted = cursor.blockNumber() <= prompt_line
1206 intercepted = cursor.blockNumber() <= prompt_line
1187
1207
1188 elif key == QtCore.Qt.Key_Down:
1208 elif key == QtCore.Qt.Key_Down:
1189 if self._reading or not self._down_pressed(shift_down):
1209 if self._reading or not self._down_pressed(shift_down):
1190 intercepted = True
1210 intercepted = True
1191 else:
1211 else:
1192 end_line = self._get_end_cursor().blockNumber()
1212 end_line = self._get_end_cursor().blockNumber()
1193 intercepted = cursor.blockNumber() == end_line
1213 intercepted = cursor.blockNumber() == end_line
1194
1214
1195 elif key == QtCore.Qt.Key_Tab:
1215 elif key == QtCore.Qt.Key_Tab:
1196 if not self._reading:
1216 if not self._reading:
1197 if self._tab_pressed():
1217 if self._tab_pressed():
1198 # real tab-key, insert four spaces
1218 # real tab-key, insert four spaces
1199 cursor.insertText(' '*4)
1219 cursor.insertText(' '*4)
1200 intercepted = True
1220 intercepted = True
1201
1221
1202 elif key == QtCore.Qt.Key_Left:
1222 elif key == QtCore.Qt.Key_Left:
1203
1223
1204 # Move to the previous line
1224 # Move to the previous line
1205 line, col = cursor.blockNumber(), cursor.columnNumber()
1225 line, col = cursor.blockNumber(), cursor.columnNumber()
1206 if line > self._get_prompt_cursor().blockNumber() and \
1226 if line > self._get_prompt_cursor().blockNumber() and \
1207 col == len(self._continuation_prompt):
1227 col == len(self._continuation_prompt):
1208 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1228 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1209 mode=anchormode)
1229 mode=anchormode)
1210 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1230 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1211 mode=anchormode)
1231 mode=anchormode)
1212 intercepted = True
1232 intercepted = True
1213
1233
1214 # Regular left movement
1234 # Regular left movement
1215 else:
1235 else:
1216 intercepted = not self._in_buffer(position - 1)
1236 intercepted = not self._in_buffer(position - 1)
1217
1237
1218 elif key == QtCore.Qt.Key_Right:
1238 elif key == QtCore.Qt.Key_Right:
1219 original_block_number = cursor.blockNumber()
1239 original_block_number = cursor.blockNumber()
1220 cursor.movePosition(QtGui.QTextCursor.Right,
1240 cursor.movePosition(QtGui.QTextCursor.Right,
1221 mode=anchormode)
1241 mode=anchormode)
1222 if cursor.blockNumber() != original_block_number:
1242 if cursor.blockNumber() != original_block_number:
1223 cursor.movePosition(QtGui.QTextCursor.Right,
1243 cursor.movePosition(QtGui.QTextCursor.Right,
1224 n=len(self._continuation_prompt),
1244 n=len(self._continuation_prompt),
1225 mode=anchormode)
1245 mode=anchormode)
1226 self._set_cursor(cursor)
1246 self._set_cursor(cursor)
1227 intercepted = True
1247 intercepted = True
1228
1248
1229 elif key == QtCore.Qt.Key_Home:
1249 elif key == QtCore.Qt.Key_Home:
1230 start_line = cursor.blockNumber()
1250 start_line = cursor.blockNumber()
1231 if start_line == self._get_prompt_cursor().blockNumber():
1251 if start_line == self._get_prompt_cursor().blockNumber():
1232 start_pos = self._prompt_pos
1252 start_pos = self._prompt_pos
1233 else:
1253 else:
1234 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1235 QtGui.QTextCursor.KeepAnchor)
1255 QtGui.QTextCursor.KeepAnchor)
1236 start_pos = cursor.position()
1256 start_pos = cursor.position()
1237 start_pos += len(self._continuation_prompt)
1257 start_pos += len(self._continuation_prompt)
1238 cursor.setPosition(position)
1258 cursor.setPosition(position)
1239 if shift_down and self._in_buffer(position):
1259 if shift_down and self._in_buffer(position):
1240 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1260 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1241 else:
1261 else:
1242 cursor.setPosition(start_pos)
1262 cursor.setPosition(start_pos)
1243 self._set_cursor(cursor)
1263 self._set_cursor(cursor)
1244 intercepted = True
1264 intercepted = True
1245
1265
1246 elif key == QtCore.Qt.Key_Backspace:
1266 elif key == QtCore.Qt.Key_Backspace:
1247
1267
1248 # Line deletion (remove continuation prompt)
1268 # Line deletion (remove continuation prompt)
1249 line, col = cursor.blockNumber(), cursor.columnNumber()
1269 line, col = cursor.blockNumber(), cursor.columnNumber()
1250 if not self._reading and \
1270 if not self._reading and \
1251 col == len(self._continuation_prompt) and \
1271 col == len(self._continuation_prompt) and \
1252 line > self._get_prompt_cursor().blockNumber():
1272 line > self._get_prompt_cursor().blockNumber():
1253 cursor.beginEditBlock()
1273 cursor.beginEditBlock()
1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1274 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1255 QtGui.QTextCursor.KeepAnchor)
1275 QtGui.QTextCursor.KeepAnchor)
1256 cursor.removeSelectedText()
1276 cursor.removeSelectedText()
1257 cursor.deletePreviousChar()
1277 cursor.deletePreviousChar()
1258 cursor.endEditBlock()
1278 cursor.endEditBlock()
1259 intercepted = True
1279 intercepted = True
1260
1280
1261 # Regular backwards deletion
1281 # Regular backwards deletion
1262 else:
1282 else:
1263 anchor = cursor.anchor()
1283 anchor = cursor.anchor()
1264 if anchor == position:
1284 if anchor == position:
1265 intercepted = not self._in_buffer(position - 1)
1285 intercepted = not self._in_buffer(position - 1)
1266 else:
1286 else:
1267 intercepted = not self._in_buffer(min(anchor, position))
1287 intercepted = not self._in_buffer(min(anchor, position))
1268
1288
1269 elif key == QtCore.Qt.Key_Delete:
1289 elif key == QtCore.Qt.Key_Delete:
1270
1290
1271 # Line deletion (remove continuation prompt)
1291 # Line deletion (remove continuation prompt)
1272 if not self._reading and self._in_buffer(position) and \
1292 if not self._reading and self._in_buffer(position) and \
1273 cursor.atBlockEnd() and not cursor.hasSelection():
1293 cursor.atBlockEnd() and not cursor.hasSelection():
1274 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1294 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1275 QtGui.QTextCursor.KeepAnchor)
1295 QtGui.QTextCursor.KeepAnchor)
1276 cursor.movePosition(QtGui.QTextCursor.Right,
1296 cursor.movePosition(QtGui.QTextCursor.Right,
1277 QtGui.QTextCursor.KeepAnchor,
1297 QtGui.QTextCursor.KeepAnchor,
1278 len(self._continuation_prompt))
1298 len(self._continuation_prompt))
1279 cursor.removeSelectedText()
1299 cursor.removeSelectedText()
1280 intercepted = True
1300 intercepted = True
1281
1301
1282 # Regular forwards deletion:
1302 # Regular forwards deletion:
1283 else:
1303 else:
1284 anchor = cursor.anchor()
1304 anchor = cursor.anchor()
1285 intercepted = (not self._in_buffer(anchor) or
1305 intercepted = (not self._in_buffer(anchor) or
1286 not self._in_buffer(position))
1306 not self._in_buffer(position))
1287
1307
1288 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1308 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1289 # using the keyboard in any part of the buffer. Also, permit scrolling
1309 # using the keyboard in any part of the buffer. Also, permit scrolling
1290 # with Page Up/Down keys. Finally, if we're executing, don't move the
1310 # with Page Up/Down keys. Finally, if we're executing, don't move the
1291 # cursor (if even this made sense, we can't guarantee that the prompt
1311 # cursor (if even this made sense, we can't guarantee that the prompt
1292 # position is still valid due to text truncation).
1312 # position is still valid due to text truncation).
1293 if not (self._control_key_down(event.modifiers(), include_command=True)
1313 if not (self._control_key_down(event.modifiers(), include_command=True)
1294 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1314 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1295 or (self._executing and not self._reading)):
1315 or (self._executing and not self._reading)):
1296 self._keep_cursor_in_buffer()
1316 self._keep_cursor_in_buffer()
1297
1317
1298 return intercepted
1318 return intercepted
1299
1319
1300 def _event_filter_page_keypress(self, event):
1320 def _event_filter_page_keypress(self, event):
1301 """ Filter key events for the paging widget to create console-like
1321 """ Filter key events for the paging widget to create console-like
1302 interface.
1322 interface.
1303 """
1323 """
1304 key = event.key()
1324 key = event.key()
1305 ctrl_down = self._control_key_down(event.modifiers())
1325 ctrl_down = self._control_key_down(event.modifiers())
1306 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1326 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1307
1327
1308 if ctrl_down:
1328 if ctrl_down:
1309 if key == QtCore.Qt.Key_O:
1329 if key == QtCore.Qt.Key_O:
1310 self._control.setFocus()
1330 self._control.setFocus()
1311 intercept = True
1331 intercept = True
1312
1332
1313 elif alt_down:
1333 elif alt_down:
1314 if key == QtCore.Qt.Key_Greater:
1334 if key == QtCore.Qt.Key_Greater:
1315 self._page_control.moveCursor(QtGui.QTextCursor.End)
1335 self._page_control.moveCursor(QtGui.QTextCursor.End)
1316 intercepted = True
1336 intercepted = True
1317
1337
1318 elif key == QtCore.Qt.Key_Less:
1338 elif key == QtCore.Qt.Key_Less:
1319 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1339 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1320 intercepted = True
1340 intercepted = True
1321
1341
1322 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1342 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1323 if self._splitter:
1343 if self._splitter:
1324 self._page_control.hide()
1344 self._page_control.hide()
1325 self._control.setFocus()
1345 self._control.setFocus()
1326 else:
1346 else:
1327 self.layout().setCurrentWidget(self._control)
1347 self.layout().setCurrentWidget(self._control)
1328 return True
1348 return True
1329
1349
1330 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1350 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1331 QtCore.Qt.Key_Tab):
1351 QtCore.Qt.Key_Tab):
1332 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1352 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1333 QtCore.Qt.Key_PageDown,
1353 QtCore.Qt.Key_PageDown,
1334 QtCore.Qt.NoModifier)
1354 QtCore.Qt.NoModifier)
1335 QtGui.qApp.sendEvent(self._page_control, new_event)
1355 QtGui.qApp.sendEvent(self._page_control, new_event)
1336 return True
1356 return True
1337
1357
1338 elif key == QtCore.Qt.Key_Backspace:
1358 elif key == QtCore.Qt.Key_Backspace:
1339 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1359 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1340 QtCore.Qt.Key_PageUp,
1360 QtCore.Qt.Key_PageUp,
1341 QtCore.Qt.NoModifier)
1361 QtCore.Qt.NoModifier)
1342 QtGui.qApp.sendEvent(self._page_control, new_event)
1362 QtGui.qApp.sendEvent(self._page_control, new_event)
1343 return True
1363 return True
1344
1364
1345 return False
1365 return False
1346
1366
1347 def _format_as_columns(self, items, separator=' '):
1367 def _format_as_columns(self, items, separator=' '):
1348 """ Transform a list of strings into a single string with columns.
1368 """ Transform a list of strings into a single string with columns.
1349
1369
1350 Parameters
1370 Parameters
1351 ----------
1371 ----------
1352 items : sequence of strings
1372 items : sequence of strings
1353 The strings to process.
1373 The strings to process.
1354
1374
1355 separator : str, optional [default is two spaces]
1375 separator : str, optional [default is two spaces]
1356 The string that separates columns.
1376 The string that separates columns.
1357
1377
1358 Returns
1378 Returns
1359 -------
1379 -------
1360 The formatted string.
1380 The formatted string.
1361 """
1381 """
1362 # Calculate the number of characters available.
1382 # Calculate the number of characters available.
1363 width = self._control.viewport().width()
1383 width = self._control.viewport().width()
1364 char_width = QtGui.QFontMetrics(self.font).width(' ')
1384 char_width = QtGui.QFontMetrics(self.font).width(' ')
1365 displaywidth = max(10, (width / char_width) - 1)
1385 displaywidth = max(10, (width / char_width) - 1)
1366
1386
1367 return columnize(items, separator, displaywidth)
1387 return columnize(items, separator, displaywidth)
1368
1388
1369 def _get_block_plain_text(self, block):
1389 def _get_block_plain_text(self, block):
1370 """ Given a QTextBlock, return its unformatted text.
1390 """ Given a QTextBlock, return its unformatted text.
1371 """
1391 """
1372 cursor = QtGui.QTextCursor(block)
1392 cursor = QtGui.QTextCursor(block)
1373 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1393 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1374 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1394 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1375 QtGui.QTextCursor.KeepAnchor)
1395 QtGui.QTextCursor.KeepAnchor)
1376 return cursor.selection().toPlainText()
1396 return cursor.selection().toPlainText()
1377
1397
1378 def _get_cursor(self):
1398 def _get_cursor(self):
1379 """ Convenience method that returns a cursor for the current position.
1399 """ Convenience method that returns a cursor for the current position.
1380 """
1400 """
1381 return self._control.textCursor()
1401 return self._control.textCursor()
1382
1402
1383 def _get_end_cursor(self):
1403 def _get_end_cursor(self):
1384 """ Convenience method that returns a cursor for the last character.
1404 """ Convenience method that returns a cursor for the last character.
1385 """
1405 """
1386 cursor = self._control.textCursor()
1406 cursor = self._control.textCursor()
1387 cursor.movePosition(QtGui.QTextCursor.End)
1407 cursor.movePosition(QtGui.QTextCursor.End)
1388 return cursor
1408 return cursor
1389
1409
1390 def _get_input_buffer_cursor_column(self):
1410 def _get_input_buffer_cursor_column(self):
1391 """ Returns the column of the cursor in the input buffer, excluding the
1411 """ Returns the column of the cursor in the input buffer, excluding the
1392 contribution by the prompt, or -1 if there is no such column.
1412 contribution by the prompt, or -1 if there is no such column.
1393 """
1413 """
1394 prompt = self._get_input_buffer_cursor_prompt()
1414 prompt = self._get_input_buffer_cursor_prompt()
1395 if prompt is None:
1415 if prompt is None:
1396 return -1
1416 return -1
1397 else:
1417 else:
1398 cursor = self._control.textCursor()
1418 cursor = self._control.textCursor()
1399 return cursor.columnNumber() - len(prompt)
1419 return cursor.columnNumber() - len(prompt)
1400
1420
1401 def _get_input_buffer_cursor_line(self):
1421 def _get_input_buffer_cursor_line(self):
1402 """ Returns the text of the line of the input buffer that contains the
1422 """ Returns the text of the line of the input buffer that contains the
1403 cursor, or None if there is no such line.
1423 cursor, or None if there is no such line.
1404 """
1424 """
1405 prompt = self._get_input_buffer_cursor_prompt()
1425 prompt = self._get_input_buffer_cursor_prompt()
1406 if prompt is None:
1426 if prompt is None:
1407 return None
1427 return None
1408 else:
1428 else:
1409 cursor = self._control.textCursor()
1429 cursor = self._control.textCursor()
1410 text = self._get_block_plain_text(cursor.block())
1430 text = self._get_block_plain_text(cursor.block())
1411 return text[len(prompt):]
1431 return text[len(prompt):]
1412
1432
1413 def _get_input_buffer_cursor_prompt(self):
1433 def _get_input_buffer_cursor_prompt(self):
1414 """ Returns the (plain text) prompt for line of the input buffer that
1434 """ Returns the (plain text) prompt for line of the input buffer that
1415 contains the cursor, or None if there is no such line.
1435 contains the cursor, or None if there is no such line.
1416 """
1436 """
1417 if self._executing:
1437 if self._executing:
1418 return None
1438 return None
1419 cursor = self._control.textCursor()
1439 cursor = self._control.textCursor()
1420 if cursor.position() >= self._prompt_pos:
1440 if cursor.position() >= self._prompt_pos:
1421 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1441 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1422 return self._prompt
1442 return self._prompt
1423 else:
1443 else:
1424 return self._continuation_prompt
1444 return self._continuation_prompt
1425 else:
1445 else:
1426 return None
1446 return None
1427
1447
1428 def _get_prompt_cursor(self):
1448 def _get_prompt_cursor(self):
1429 """ Convenience method that returns a cursor for the prompt position.
1449 """ Convenience method that returns a cursor for the prompt position.
1430 """
1450 """
1431 cursor = self._control.textCursor()
1451 cursor = self._control.textCursor()
1432 cursor.setPosition(self._prompt_pos)
1452 cursor.setPosition(self._prompt_pos)
1433 return cursor
1453 return cursor
1434
1454
1435 def _get_selection_cursor(self, start, end):
1455 def _get_selection_cursor(self, start, end):
1436 """ Convenience method that returns a cursor with text selected between
1456 """ Convenience method that returns a cursor with text selected between
1437 the positions 'start' and 'end'.
1457 the positions 'start' and 'end'.
1438 """
1458 """
1439 cursor = self._control.textCursor()
1459 cursor = self._control.textCursor()
1440 cursor.setPosition(start)
1460 cursor.setPosition(start)
1441 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1461 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1442 return cursor
1462 return cursor
1443
1463
1444 def _get_word_start_cursor(self, position):
1464 def _get_word_start_cursor(self, position):
1445 """ Find the start of the word to the left the given position. If a
1465 """ Find the start of the word to the left the given position. If a
1446 sequence of non-word characters precedes the first word, skip over
1466 sequence of non-word characters precedes the first word, skip over
1447 them. (This emulates the behavior of bash, emacs, etc.)
1467 them. (This emulates the behavior of bash, emacs, etc.)
1448 """
1468 """
1449 document = self._control.document()
1469 document = self._control.document()
1450 position -= 1
1470 position -= 1
1451 while position >= self._prompt_pos and \
1471 while position >= self._prompt_pos and \
1452 not is_letter_or_number(document.characterAt(position)):
1472 not is_letter_or_number(document.characterAt(position)):
1453 position -= 1
1473 position -= 1
1454 while position >= self._prompt_pos and \
1474 while position >= self._prompt_pos and \
1455 is_letter_or_number(document.characterAt(position)):
1475 is_letter_or_number(document.characterAt(position)):
1456 position -= 1
1476 position -= 1
1457 cursor = self._control.textCursor()
1477 cursor = self._control.textCursor()
1458 cursor.setPosition(position + 1)
1478 cursor.setPosition(position + 1)
1459 return cursor
1479 return cursor
1460
1480
1461 def _get_word_end_cursor(self, position):
1481 def _get_word_end_cursor(self, position):
1462 """ Find the end of the word to the right the given position. If a
1482 """ Find the end of the word to the right the given position. If a
1463 sequence of non-word characters precedes the first word, skip over
1483 sequence of non-word characters precedes the first word, skip over
1464 them. (This emulates the behavior of bash, emacs, etc.)
1484 them. (This emulates the behavior of bash, emacs, etc.)
1465 """
1485 """
1466 document = self._control.document()
1486 document = self._control.document()
1467 end = self._get_end_cursor().position()
1487 end = self._get_end_cursor().position()
1468 while position < end and \
1488 while position < end and \
1469 not is_letter_or_number(document.characterAt(position)):
1489 not is_letter_or_number(document.characterAt(position)):
1470 position += 1
1490 position += 1
1471 while position < end and \
1491 while position < end and \
1472 is_letter_or_number(document.characterAt(position)):
1492 is_letter_or_number(document.characterAt(position)):
1473 position += 1
1493 position += 1
1474 cursor = self._control.textCursor()
1494 cursor = self._control.textCursor()
1475 cursor.setPosition(position)
1495 cursor.setPosition(position)
1476 return cursor
1496 return cursor
1477
1497
1478 def _insert_continuation_prompt(self, cursor):
1498 def _insert_continuation_prompt(self, cursor):
1479 """ Inserts new continuation prompt using the specified cursor.
1499 """ Inserts new continuation prompt using the specified cursor.
1480 """
1500 """
1481 if self._continuation_prompt_html is None:
1501 if self._continuation_prompt_html is None:
1482 self._insert_plain_text(cursor, self._continuation_prompt)
1502 self._insert_plain_text(cursor, self._continuation_prompt)
1483 else:
1503 else:
1484 self._continuation_prompt = self._insert_html_fetching_plain_text(
1504 self._continuation_prompt = self._insert_html_fetching_plain_text(
1485 cursor, self._continuation_prompt_html)
1505 cursor, self._continuation_prompt_html)
1486
1506
1487 def _insert_html(self, cursor, html):
1507 def _insert_html(self, cursor, html):
1488 """ Inserts HTML using the specified cursor in such a way that future
1508 """ Inserts HTML using the specified cursor in such a way that future
1489 formatting is unaffected.
1509 formatting is unaffected.
1490 """
1510 """
1491 cursor.beginEditBlock()
1511 cursor.beginEditBlock()
1492 cursor.insertHtml(html)
1512 cursor.insertHtml(html)
1493
1513
1494 # After inserting HTML, the text document "remembers" it's in "html
1514 # After inserting HTML, the text document "remembers" it's in "html
1495 # mode", which means that subsequent calls adding plain text will result
1515 # mode", which means that subsequent calls adding plain text will result
1496 # in unwanted formatting, lost tab characters, etc. The following code
1516 # in unwanted formatting, lost tab characters, etc. The following code
1497 # hacks around this behavior, which I consider to be a bug in Qt, by
1517 # hacks around this behavior, which I consider to be a bug in Qt, by
1498 # (crudely) resetting the document's style state.
1518 # (crudely) resetting the document's style state.
1499 cursor.movePosition(QtGui.QTextCursor.Left,
1519 cursor.movePosition(QtGui.QTextCursor.Left,
1500 QtGui.QTextCursor.KeepAnchor)
1520 QtGui.QTextCursor.KeepAnchor)
1501 if cursor.selection().toPlainText() == ' ':
1521 if cursor.selection().toPlainText() == ' ':
1502 cursor.removeSelectedText()
1522 cursor.removeSelectedText()
1503 else:
1523 else:
1504 cursor.movePosition(QtGui.QTextCursor.Right)
1524 cursor.movePosition(QtGui.QTextCursor.Right)
1505 cursor.insertText(' ', QtGui.QTextCharFormat())
1525 cursor.insertText(' ', QtGui.QTextCharFormat())
1506 cursor.endEditBlock()
1526 cursor.endEditBlock()
1507
1527
1508 def _insert_html_fetching_plain_text(self, cursor, html):
1528 def _insert_html_fetching_plain_text(self, cursor, html):
1509 """ Inserts HTML using the specified cursor, then returns its plain text
1529 """ Inserts HTML using the specified cursor, then returns its plain text
1510 version.
1530 version.
1511 """
1531 """
1512 cursor.beginEditBlock()
1532 cursor.beginEditBlock()
1513 cursor.removeSelectedText()
1533 cursor.removeSelectedText()
1514
1534
1515 start = cursor.position()
1535 start = cursor.position()
1516 self._insert_html(cursor, html)
1536 self._insert_html(cursor, html)
1517 end = cursor.position()
1537 end = cursor.position()
1518 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1538 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1519 text = cursor.selection().toPlainText()
1539 text = cursor.selection().toPlainText()
1520
1540
1521 cursor.setPosition(end)
1541 cursor.setPosition(end)
1522 cursor.endEditBlock()
1542 cursor.endEditBlock()
1523 return text
1543 return text
1524
1544
1525 def _insert_plain_text(self, cursor, text):
1545 def _insert_plain_text(self, cursor, text):
1526 """ Inserts plain text using the specified cursor, processing ANSI codes
1546 """ Inserts plain text using the specified cursor, processing ANSI codes
1527 if enabled.
1547 if enabled.
1528 """
1548 """
1529 cursor.beginEditBlock()
1549 cursor.beginEditBlock()
1530 if self.ansi_codes:
1550 if self.ansi_codes:
1531 for substring in self._ansi_processor.split_string(text):
1551 for substring in self._ansi_processor.split_string(text):
1532 for act in self._ansi_processor.actions:
1552 for act in self._ansi_processor.actions:
1533
1553
1534 # Unlike real terminal emulators, we don't distinguish
1554 # Unlike real terminal emulators, we don't distinguish
1535 # between the screen and the scrollback buffer. A screen
1555 # between the screen and the scrollback buffer. A screen
1536 # erase request clears everything.
1556 # erase request clears everything.
1537 if act.action == 'erase' and act.area == 'screen':
1557 if act.action == 'erase' and act.area == 'screen':
1538 cursor.select(QtGui.QTextCursor.Document)
1558 cursor.select(QtGui.QTextCursor.Document)
1539 cursor.removeSelectedText()
1559 cursor.removeSelectedText()
1540
1560
1541 # Simulate a form feed by scrolling just past the last line.
1561 # Simulate a form feed by scrolling just past the last line.
1542 elif act.action == 'scroll' and act.unit == 'page':
1562 elif act.action == 'scroll' and act.unit == 'page':
1543 cursor.insertText('\n')
1563 cursor.insertText('\n')
1544 cursor.endEditBlock()
1564 cursor.endEditBlock()
1545 self._set_top_cursor(cursor)
1565 self._set_top_cursor(cursor)
1546 cursor.joinPreviousEditBlock()
1566 cursor.joinPreviousEditBlock()
1547 cursor.deletePreviousChar()
1567 cursor.deletePreviousChar()
1548
1568
1549 elif act.action == 'carriage-return':
1569 elif act.action == 'carriage-return':
1550 cursor.movePosition(
1570 cursor.movePosition(
1551 cursor.StartOfLine, cursor.KeepAnchor)
1571 cursor.StartOfLine, cursor.KeepAnchor)
1552
1572
1553 elif act.action == 'beep':
1573 elif act.action == 'beep':
1554 QtGui.qApp.beep()
1574 QtGui.qApp.beep()
1555
1575
1556 elif act.action == 'backspace':
1576 elif act.action == 'backspace':
1557 if not cursor.atBlockStart():
1577 if not cursor.atBlockStart():
1558 cursor.movePosition(
1578 cursor.movePosition(
1559 cursor.PreviousCharacter, cursor.KeepAnchor)
1579 cursor.PreviousCharacter, cursor.KeepAnchor)
1560
1580
1561 elif act.action == 'newline':
1581 elif act.action == 'newline':
1562 cursor.movePosition(cursor.EndOfLine)
1582 cursor.movePosition(cursor.EndOfLine)
1563
1583
1564 format = self._ansi_processor.get_format()
1584 format = self._ansi_processor.get_format()
1565
1585
1566 selection = cursor.selectedText()
1586 selection = cursor.selectedText()
1567 if len(selection) == 0:
1587 if len(selection) == 0:
1568 cursor.insertText(substring, format)
1588 cursor.insertText(substring, format)
1569 elif substring is not None:
1589 elif substring is not None:
1570 # BS and CR are treated as a change in print
1590 # BS and CR are treated as a change in print
1571 # position, rather than a backwards character
1591 # position, rather than a backwards character
1572 # deletion for output equivalence with (I)Python
1592 # deletion for output equivalence with (I)Python
1573 # terminal.
1593 # terminal.
1574 if len(substring) >= len(selection):
1594 if len(substring) >= len(selection):
1575 cursor.insertText(substring, format)
1595 cursor.insertText(substring, format)
1576 else:
1596 else:
1577 old_text = selection[len(substring):]
1597 old_text = selection[len(substring):]
1578 cursor.insertText(substring + old_text, format)
1598 cursor.insertText(substring + old_text, format)
1579 cursor.movePosition(cursor.PreviousCharacter,
1599 cursor.movePosition(cursor.PreviousCharacter,
1580 cursor.KeepAnchor, len(old_text))
1600 cursor.KeepAnchor, len(old_text))
1581 else:
1601 else:
1582 cursor.insertText(text)
1602 cursor.insertText(text)
1583 cursor.endEditBlock()
1603 cursor.endEditBlock()
1584
1604
1585 def _insert_plain_text_into_buffer(self, cursor, text):
1605 def _insert_plain_text_into_buffer(self, cursor, text):
1586 """ Inserts text into the input buffer using the specified cursor (which
1606 """ Inserts text into the input buffer using the specified cursor (which
1587 must be in the input buffer), ensuring that continuation prompts are
1607 must be in the input buffer), ensuring that continuation prompts are
1588 inserted as necessary.
1608 inserted as necessary.
1589 """
1609 """
1590 lines = text.splitlines(True)
1610 lines = text.splitlines(True)
1591 if lines:
1611 if lines:
1592 cursor.beginEditBlock()
1612 cursor.beginEditBlock()
1593 cursor.insertText(lines[0])
1613 cursor.insertText(lines[0])
1594 for line in lines[1:]:
1614 for line in lines[1:]:
1595 if self._continuation_prompt_html is None:
1615 if self._continuation_prompt_html is None:
1596 cursor.insertText(self._continuation_prompt)
1616 cursor.insertText(self._continuation_prompt)
1597 else:
1617 else:
1598 self._continuation_prompt = \
1618 self._continuation_prompt = \
1599 self._insert_html_fetching_plain_text(
1619 self._insert_html_fetching_plain_text(
1600 cursor, self._continuation_prompt_html)
1620 cursor, self._continuation_prompt_html)
1601 cursor.insertText(line)
1621 cursor.insertText(line)
1602 cursor.endEditBlock()
1622 cursor.endEditBlock()
1603
1623
1604 def _in_buffer(self, position=None):
1624 def _in_buffer(self, position=None):
1605 """ Returns whether the current cursor (or, if specified, a position) is
1625 """ Returns whether the current cursor (or, if specified, a position) is
1606 inside the editing region.
1626 inside the editing region.
1607 """
1627 """
1608 cursor = self._control.textCursor()
1628 cursor = self._control.textCursor()
1609 if position is None:
1629 if position is None:
1610 position = cursor.position()
1630 position = cursor.position()
1611 else:
1631 else:
1612 cursor.setPosition(position)
1632 cursor.setPosition(position)
1613 line = cursor.blockNumber()
1633 line = cursor.blockNumber()
1614 prompt_line = self._get_prompt_cursor().blockNumber()
1634 prompt_line = self._get_prompt_cursor().blockNumber()
1615 if line == prompt_line:
1635 if line == prompt_line:
1616 return position >= self._prompt_pos
1636 return position >= self._prompt_pos
1617 elif line > prompt_line:
1637 elif line > prompt_line:
1618 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1638 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1619 prompt_pos = cursor.position() + len(self._continuation_prompt)
1639 prompt_pos = cursor.position() + len(self._continuation_prompt)
1620 return position >= prompt_pos
1640 return position >= prompt_pos
1621 return False
1641 return False
1622
1642
1623 def _keep_cursor_in_buffer(self):
1643 def _keep_cursor_in_buffer(self):
1624 """ Ensures that the cursor is inside the editing region. Returns
1644 """ Ensures that the cursor is inside the editing region. Returns
1625 whether the cursor was moved.
1645 whether the cursor was moved.
1626 """
1646 """
1627 moved = not self._in_buffer()
1647 moved = not self._in_buffer()
1628 if moved:
1648 if moved:
1629 cursor = self._control.textCursor()
1649 cursor = self._control.textCursor()
1630 cursor.movePosition(QtGui.QTextCursor.End)
1650 cursor.movePosition(QtGui.QTextCursor.End)
1631 self._control.setTextCursor(cursor)
1651 self._control.setTextCursor(cursor)
1632 return moved
1652 return moved
1633
1653
1634 def _keyboard_quit(self):
1654 def _keyboard_quit(self):
1635 """ Cancels the current editing task ala Ctrl-G in Emacs.
1655 """ Cancels the current editing task ala Ctrl-G in Emacs.
1636 """
1656 """
1637 if self._text_completing_pos:
1657 if self._is_completing:
1638 self._cancel_text_completion()
1658 self._cancel_completion()
1639 else:
1659 else:
1640 self.input_buffer = ''
1660 self.input_buffer = ''
1641
1661
1642 def _page(self, text, html=False):
1662 def _page(self, text, html=False):
1643 """ Displays text using the pager if it exceeds the height of the
1663 """ Displays text using the pager if it exceeds the height of the
1644 viewport.
1664 viewport.
1645
1665
1646 Parameters:
1666 Parameters:
1647 -----------
1667 -----------
1648 html : bool, optional (default False)
1668 html : bool, optional (default False)
1649 If set, the text will be interpreted as HTML instead of plain text.
1669 If set, the text will be interpreted as HTML instead of plain text.
1650 """
1670 """
1651 line_height = QtGui.QFontMetrics(self.font).height()
1671 line_height = QtGui.QFontMetrics(self.font).height()
1652 minlines = self._control.viewport().height() / line_height
1672 minlines = self._control.viewport().height() / line_height
1653 if self.paging != 'none' and \
1673 if self.paging != 'none' and \
1654 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1674 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1655 if self.paging == 'custom':
1675 if self.paging == 'custom':
1656 self.custom_page_requested.emit(text)
1676 self.custom_page_requested.emit(text)
1657 else:
1677 else:
1658 self._page_control.clear()
1678 self._page_control.clear()
1659 cursor = self._page_control.textCursor()
1679 cursor = self._page_control.textCursor()
1660 if html:
1680 if html:
1661 self._insert_html(cursor, text)
1681 self._insert_html(cursor, text)
1662 else:
1682 else:
1663 self._insert_plain_text(cursor, text)
1683 self._insert_plain_text(cursor, text)
1664 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1684 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1665
1685
1666 self._page_control.viewport().resize(self._control.size())
1686 self._page_control.viewport().resize(self._control.size())
1667 if self._splitter:
1687 if self._splitter:
1668 self._page_control.show()
1688 self._page_control.show()
1669 self._page_control.setFocus()
1689 self._page_control.setFocus()
1670 else:
1690 else:
1671 self.layout().setCurrentWidget(self._page_control)
1691 self.layout().setCurrentWidget(self._page_control)
1672 elif html:
1692 elif html:
1673 self._append_html(text)
1693 self._append_html(text)
1674 else:
1694 else:
1675 self._append_plain_text(text)
1695 self._append_plain_text(text)
1676
1696
1677 def _prompt_finished(self):
1697 def _prompt_finished(self):
1678 """ Called immediately after a prompt is finished, i.e. when some input
1698 """ Called immediately after a prompt is finished, i.e. when some input
1679 will be processed and a new prompt displayed.
1699 will be processed and a new prompt displayed.
1680 """
1700 """
1681 self._control.setReadOnly(True)
1701 self._control.setReadOnly(True)
1682 self._prompt_finished_hook()
1702 self._prompt_finished_hook()
1683
1703
1684 def _prompt_started(self):
1704 def _prompt_started(self):
1685 """ Called immediately after a new prompt is displayed.
1705 """ Called immediately after a new prompt is displayed.
1686 """
1706 """
1687 # Temporarily disable the maximum block count to permit undo/redo and
1707 # Temporarily disable the maximum block count to permit undo/redo and
1688 # to ensure that the prompt position does not change due to truncation.
1708 # to ensure that the prompt position does not change due to truncation.
1689 self._control.document().setMaximumBlockCount(0)
1709 self._control.document().setMaximumBlockCount(0)
1690 self._control.setUndoRedoEnabled(True)
1710 self._control.setUndoRedoEnabled(True)
1691
1711
1692 # Work around bug in QPlainTextEdit: input method is not re-enabled
1712 # Work around bug in QPlainTextEdit: input method is not re-enabled
1693 # when read-only is disabled.
1713 # when read-only is disabled.
1694 self._control.setReadOnly(False)
1714 self._control.setReadOnly(False)
1695 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1715 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1696
1716
1697 if not self._reading:
1717 if not self._reading:
1698 self._executing = False
1718 self._executing = False
1699 self._prompt_started_hook()
1719 self._prompt_started_hook()
1700
1720
1701 # If the input buffer has changed while executing, load it.
1721 # If the input buffer has changed while executing, load it.
1702 if self._input_buffer_pending:
1722 if self._input_buffer_pending:
1703 self.input_buffer = self._input_buffer_pending
1723 self.input_buffer = self._input_buffer_pending
1704 self._input_buffer_pending = ''
1724 self._input_buffer_pending = ''
1705
1725
1706 self._control.moveCursor(QtGui.QTextCursor.End)
1726 self._control.moveCursor(QtGui.QTextCursor.End)
1707
1727
1708 def _readline(self, prompt='', callback=None):
1728 def _readline(self, prompt='', callback=None):
1709 """ Reads one line of input from the user.
1729 """ Reads one line of input from the user.
1710
1730
1711 Parameters
1731 Parameters
1712 ----------
1732 ----------
1713 prompt : str, optional
1733 prompt : str, optional
1714 The prompt to print before reading the line.
1734 The prompt to print before reading the line.
1715
1735
1716 callback : callable, optional
1736 callback : callable, optional
1717 A callback to execute with the read line. If not specified, input is
1737 A callback to execute with the read line. If not specified, input is
1718 read *synchronously* and this method does not return until it has
1738 read *synchronously* and this method does not return until it has
1719 been read.
1739 been read.
1720
1740
1721 Returns
1741 Returns
1722 -------
1742 -------
1723 If a callback is specified, returns nothing. Otherwise, returns the
1743 If a callback is specified, returns nothing. Otherwise, returns the
1724 input string with the trailing newline stripped.
1744 input string with the trailing newline stripped.
1725 """
1745 """
1726 if self._reading:
1746 if self._reading:
1727 raise RuntimeError('Cannot read a line. Widget is already reading.')
1747 raise RuntimeError('Cannot read a line. Widget is already reading.')
1728
1748
1729 if not callback and not self.isVisible():
1749 if not callback and not self.isVisible():
1730 # If the user cannot see the widget, this function cannot return.
1750 # If the user cannot see the widget, this function cannot return.
1731 raise RuntimeError('Cannot synchronously read a line if the widget '
1751 raise RuntimeError('Cannot synchronously read a line if the widget '
1732 'is not visible!')
1752 'is not visible!')
1733
1753
1734 self._reading = True
1754 self._reading = True
1735 self._show_prompt(prompt, newline=False)
1755 self._show_prompt(prompt, newline=False)
1736
1756
1737 if callback is None:
1757 if callback is None:
1738 self._reading_callback = None
1758 self._reading_callback = None
1739 while self._reading:
1759 while self._reading:
1740 QtCore.QCoreApplication.processEvents()
1760 QtCore.QCoreApplication.processEvents()
1741 return self._get_input_buffer(force=True).rstrip('\n')
1761 return self._get_input_buffer(force=True).rstrip('\n')
1742
1762
1743 else:
1763 else:
1744 self._reading_callback = lambda: \
1764 self._reading_callback = lambda: \
1745 callback(self._get_input_buffer(force=True).rstrip('\n'))
1765 callback(self._get_input_buffer(force=True).rstrip('\n'))
1746
1766
1747 def _set_continuation_prompt(self, prompt, html=False):
1767 def _set_continuation_prompt(self, prompt, html=False):
1748 """ Sets the continuation prompt.
1768 """ Sets the continuation prompt.
1749
1769
1750 Parameters
1770 Parameters
1751 ----------
1771 ----------
1752 prompt : str
1772 prompt : str
1753 The prompt to show when more input is needed.
1773 The prompt to show when more input is needed.
1754
1774
1755 html : bool, optional (default False)
1775 html : bool, optional (default False)
1756 If set, the prompt will be inserted as formatted HTML. Otherwise,
1776 If set, the prompt will be inserted as formatted HTML. Otherwise,
1757 the prompt will be treated as plain text, though ANSI color codes
1777 the prompt will be treated as plain text, though ANSI color codes
1758 will be handled.
1778 will be handled.
1759 """
1779 """
1760 if html:
1780 if html:
1761 self._continuation_prompt_html = prompt
1781 self._continuation_prompt_html = prompt
1762 else:
1782 else:
1763 self._continuation_prompt = prompt
1783 self._continuation_prompt = prompt
1764 self._continuation_prompt_html = None
1784 self._continuation_prompt_html = None
1765
1785
1766 def _set_cursor(self, cursor):
1786 def _set_cursor(self, cursor):
1767 """ Convenience method to set the current cursor.
1787 """ Convenience method to set the current cursor.
1768 """
1788 """
1769 self._control.setTextCursor(cursor)
1789 self._control.setTextCursor(cursor)
1770
1790
1771 def _set_top_cursor(self, cursor):
1791 def _set_top_cursor(self, cursor):
1772 """ Scrolls the viewport so that the specified cursor is at the top.
1792 """ Scrolls the viewport so that the specified cursor is at the top.
1773 """
1793 """
1774 scrollbar = self._control.verticalScrollBar()
1794 scrollbar = self._control.verticalScrollBar()
1775 scrollbar.setValue(scrollbar.maximum())
1795 scrollbar.setValue(scrollbar.maximum())
1776 original_cursor = self._control.textCursor()
1796 original_cursor = self._control.textCursor()
1777 self._control.setTextCursor(cursor)
1797 self._control.setTextCursor(cursor)
1778 self._control.ensureCursorVisible()
1798 self._control.ensureCursorVisible()
1779 self._control.setTextCursor(original_cursor)
1799 self._control.setTextCursor(original_cursor)
1780
1800
1781 def _show_prompt(self, prompt=None, html=False, newline=True):
1801 def _show_prompt(self, prompt=None, html=False, newline=True):
1782 """ Writes a new prompt at the end of the buffer.
1802 """ Writes a new prompt at the end of the buffer.
1783
1803
1784 Parameters
1804 Parameters
1785 ----------
1805 ----------
1786 prompt : str, optional
1806 prompt : str, optional
1787 The prompt to show. If not specified, the previous prompt is used.
1807 The prompt to show. If not specified, the previous prompt is used.
1788
1808
1789 html : bool, optional (default False)
1809 html : bool, optional (default False)
1790 Only relevant when a prompt is specified. If set, the prompt will
1810 Only relevant when a prompt is specified. If set, the prompt will
1791 be inserted as formatted HTML. Otherwise, the prompt will be treated
1811 be inserted as formatted HTML. Otherwise, the prompt will be treated
1792 as plain text, though ANSI color codes will be handled.
1812 as plain text, though ANSI color codes will be handled.
1793
1813
1794 newline : bool, optional (default True)
1814 newline : bool, optional (default True)
1795 If set, a new line will be written before showing the prompt if
1815 If set, a new line will be written before showing the prompt if
1796 there is not already a newline at the end of the buffer.
1816 there is not already a newline at the end of the buffer.
1797 """
1817 """
1798 # Save the current end position to support _append*(before_prompt=True).
1818 # Save the current end position to support _append*(before_prompt=True).
1799 cursor = self._get_end_cursor()
1819 cursor = self._get_end_cursor()
1800 self._append_before_prompt_pos = cursor.position()
1820 self._append_before_prompt_pos = cursor.position()
1801
1821
1802 # Insert a preliminary newline, if necessary.
1822 # Insert a preliminary newline, if necessary.
1803 if newline and cursor.position() > 0:
1823 if newline and cursor.position() > 0:
1804 cursor.movePosition(QtGui.QTextCursor.Left,
1824 cursor.movePosition(QtGui.QTextCursor.Left,
1805 QtGui.QTextCursor.KeepAnchor)
1825 QtGui.QTextCursor.KeepAnchor)
1806 if cursor.selection().toPlainText() != '\n':
1826 if cursor.selection().toPlainText() != '\n':
1807 self._append_plain_text('\n')
1827 self._append_plain_text('\n')
1808
1828
1809 # Write the prompt.
1829 # Write the prompt.
1810 self._append_plain_text(self._prompt_sep)
1830 self._append_plain_text(self._prompt_sep)
1811 if prompt is None:
1831 if prompt is None:
1812 if self._prompt_html is None:
1832 if self._prompt_html is None:
1813 self._append_plain_text(self._prompt)
1833 self._append_plain_text(self._prompt)
1814 else:
1834 else:
1815 self._append_html(self._prompt_html)
1835 self._append_html(self._prompt_html)
1816 else:
1836 else:
1817 if html:
1837 if html:
1818 self._prompt = self._append_html_fetching_plain_text(prompt)
1838 self._prompt = self._append_html_fetching_plain_text(prompt)
1819 self._prompt_html = prompt
1839 self._prompt_html = prompt
1820 else:
1840 else:
1821 self._append_plain_text(prompt)
1841 self._append_plain_text(prompt)
1822 self._prompt = prompt
1842 self._prompt = prompt
1823 self._prompt_html = None
1843 self._prompt_html = None
1824
1844
1825 self._prompt_pos = self._get_end_cursor().position()
1845 self._prompt_pos = self._get_end_cursor().position()
1826 self._prompt_started()
1846 self._prompt_started()
1827
1847
1828 #------ Signal handlers ----------------------------------------------------
1848 #------ Signal handlers ----------------------------------------------------
1829
1849
1830 def _adjust_scrollbars(self):
1850 def _adjust_scrollbars(self):
1831 """ Expands the vertical scrollbar beyond the range set by Qt.
1851 """ Expands the vertical scrollbar beyond the range set by Qt.
1832 """
1852 """
1833 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1853 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1834 # and qtextedit.cpp.
1854 # and qtextedit.cpp.
1835 document = self._control.document()
1855 document = self._control.document()
1836 scrollbar = self._control.verticalScrollBar()
1856 scrollbar = self._control.verticalScrollBar()
1837 viewport_height = self._control.viewport().height()
1857 viewport_height = self._control.viewport().height()
1838 if isinstance(self._control, QtGui.QPlainTextEdit):
1858 if isinstance(self._control, QtGui.QPlainTextEdit):
1839 maximum = max(0, document.lineCount() - 1)
1859 maximum = max(0, document.lineCount() - 1)
1840 step = viewport_height / self._control.fontMetrics().lineSpacing()
1860 step = viewport_height / self._control.fontMetrics().lineSpacing()
1841 else:
1861 else:
1842 # QTextEdit does not do line-based layout and blocks will not in
1862 # QTextEdit does not do line-based layout and blocks will not in
1843 # general have the same height. Therefore it does not make sense to
1863 # general have the same height. Therefore it does not make sense to
1844 # attempt to scroll in line height increments.
1864 # attempt to scroll in line height increments.
1845 maximum = document.size().height()
1865 maximum = document.size().height()
1846 step = viewport_height
1866 step = viewport_height
1847 diff = maximum - scrollbar.maximum()
1867 diff = maximum - scrollbar.maximum()
1848 scrollbar.setRange(0, maximum)
1868 scrollbar.setRange(0, maximum)
1849 scrollbar.setPageStep(step)
1869 scrollbar.setPageStep(step)
1850
1870
1851 # Compensate for undesirable scrolling that occurs automatically due to
1871 # Compensate for undesirable scrolling that occurs automatically due to
1852 # maximumBlockCount() text truncation.
1872 # maximumBlockCount() text truncation.
1853 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1873 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1854 scrollbar.setValue(scrollbar.value() + diff)
1874 scrollbar.setValue(scrollbar.value() + diff)
1855
1875
1856 def _cursor_position_changed(self):
1857 """ Clears the temporary buffer based on the cursor position.
1858 """
1859 if self._text_completing_pos:
1860 document = self._control.document()
1861 if self._text_completing_pos < document.characterCount():
1862 cursor = self._control.textCursor()
1863 pos = cursor.position()
1864 text_cursor = self._control.textCursor()
1865 text_cursor.setPosition(self._text_completing_pos)
1866 if pos < self._text_completing_pos or \
1867 cursor.blockNumber() > text_cursor.blockNumber():
1868 self._clear_temporary_buffer()
1869 self._text_completing_pos = 0
1870 else:
1871 self._clear_temporary_buffer()
1872 self._text_completing_pos = 0
1873
1874 def _custom_context_menu_requested(self, pos):
1876 def _custom_context_menu_requested(self, pos):
1875 """ Shows a context menu at the given QPoint (in widget coordinates).
1877 """ Shows a context menu at the given QPoint (in widget coordinates).
1876 """
1878 """
1877 menu = self._context_menu_make(pos)
1879 menu = self._context_menu_make(pos)
1878 menu.exec_(self._control.mapToGlobal(pos))
1880 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,372 +1,375 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 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14 * Paul Ivanov
14 * Paul Ivanov
15
15
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 # stdlib imports
22 # stdlib imports
23 import json
23 import json
24 import os
24 import os
25 import signal
25 import signal
26 import sys
26 import sys
27 import uuid
27 import uuid
28
28
29 # If run on Windows, install an exception hook which pops up a
29 # If run on Windows, install an exception hook which pops up a
30 # message box. Pythonw.exe hides the console, so without this
30 # message box. Pythonw.exe hides the console, so without this
31 # the application silently fails to load.
31 # the application silently fails to load.
32 #
32 #
33 # We always install this handler, because the expectation is for
33 # We always install this handler, because the expectation is for
34 # qtconsole to bring up a GUI even if called from the console.
34 # qtconsole to bring up a GUI even if called from the console.
35 # The old handler is called, so the exception is printed as well.
35 # The old handler is called, so the exception is printed as well.
36 # If desired, check for pythonw with an additional condition
36 # If desired, check for pythonw with an additional condition
37 # (sys.executable.lower().find('pythonw.exe') >= 0).
37 # (sys.executable.lower().find('pythonw.exe') >= 0).
38 if os.name == 'nt':
38 if os.name == 'nt':
39 old_excepthook = sys.excepthook
39 old_excepthook = sys.excepthook
40
40
41 def gui_excepthook(exctype, value, tb):
41 def gui_excepthook(exctype, value, tb):
42 try:
42 try:
43 import ctypes, traceback
43 import ctypes, traceback
44 MB_ICONERROR = 0x00000010L
44 MB_ICONERROR = 0x00000010L
45 title = u'Error starting IPython QtConsole'
45 title = u'Error starting IPython QtConsole'
46 msg = u''.join(traceback.format_exception(exctype, value, tb))
46 msg = u''.join(traceback.format_exception(exctype, value, tb))
47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
48 finally:
48 finally:
49 # Also call the old exception hook to let it do
49 # Also call the old exception hook to let it do
50 # its thing too.
50 # its thing too.
51 old_excepthook(exctype, value, tb)
51 old_excepthook(exctype, value, tb)
52
52
53 sys.excepthook = gui_excepthook
53 sys.excepthook = gui_excepthook
54
54
55 # System library imports
55 # System library imports
56 from IPython.external.qt import QtCore, QtGui
56 from IPython.external.qt import QtCore, QtGui
57
57
58 # Local imports
58 # Local imports
59 from IPython.config.application import boolean_flag, catch_config_error
59 from IPython.config.application import boolean_flag, catch_config_error
60 from IPython.core.application import BaseIPythonApplication
60 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.profiledir import ProfileDir
61 from IPython.core.profiledir import ProfileDir
62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
66 from IPython.frontend.qt.console import styles
66 from IPython.frontend.qt.console import styles
67 from IPython.frontend.qt.console.mainwindow import MainWindow
67 from IPython.frontend.qt.console.mainwindow import MainWindow
68 from IPython.frontend.qt.kernelmanager import QtKernelManager
68 from IPython.frontend.qt.kernelmanager import QtKernelManager
69 from IPython.utils.path import filefind
69 from IPython.utils.path import filefind
70 from IPython.utils.py3compat import str_to_bytes
70 from IPython.utils.py3compat import str_to_bytes
71 from IPython.utils.traitlets import (
71 from IPython.utils.traitlets import (
72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
73 )
73 )
74 from IPython.zmq.ipkernel import IPKernelApp
74 from IPython.zmq.ipkernel import IPKernelApp
75 from IPython.zmq.session import Session, default_secure
75 from IPython.zmq.session import Session, default_secure
76 from IPython.zmq.zmqshell import ZMQInteractiveShell
76 from IPython.zmq.zmqshell import ZMQInteractiveShell
77
77
78 from IPython.frontend.consoleapp import (
78 from IPython.frontend.consoleapp import (
79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
80 )
80 )
81
81
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83 # Network Constants
83 # Network Constants
84 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
85
85
86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Globals
89 # Globals
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 _examples = """
92 _examples = """
93 ipython qtconsole # start the qtconsole
93 ipython qtconsole # start the qtconsole
94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
95 """
95 """
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Aliases and Flags
98 # Aliases and Flags
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 # start with copy of flags
101 # start with copy of flags
102 flags = dict(flags)
102 flags = dict(flags)
103 qt_flags = {
103 qt_flags = {
104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
105 "Disable rich text support."),
105 "Disable rich text support."),
106 }
106 }
107 qt_flags.update(boolean_flag(
107
108 'gui-completion', 'ConsoleWidget.gui_completion',
108 # not quite sure on how this works
109 "use a GUI widget for tab completion",
109 #qt_flags.update(boolean_flag(
110 "use plaintext output for completion"
110 # 'gui-completion', 'ConsoleWidget.gui_completion',
111 ))
111 # "use a GUI widget for tab completion",
112 # "use plaintext output for completion"
113 #))
114
112 # and app_flags from the Console Mixin
115 # and app_flags from the Console Mixin
113 qt_flags.update(app_flags)
116 qt_flags.update(app_flags)
114 # add frontend flags to the full set
117 # add frontend flags to the full set
115 flags.update(qt_flags)
118 flags.update(qt_flags)
116
119
117 # start with copy of front&backend aliases list
120 # start with copy of front&backend aliases list
118 aliases = dict(aliases)
121 aliases = dict(aliases)
119 qt_aliases = dict(
122 qt_aliases = dict(
120
121 style = 'IPythonWidget.syntax_style',
123 style = 'IPythonWidget.syntax_style',
122 stylesheet = 'IPythonQtConsoleApp.stylesheet',
124 stylesheet = 'IPythonQtConsoleApp.stylesheet',
123 colors = 'ZMQInteractiveShell.colors',
125 colors = 'ZMQInteractiveShell.colors',
124
126
125 editor = 'IPythonWidget.editor',
127 editor = 'IPythonWidget.editor',
126 paging = 'ConsoleWidget.paging',
128 paging = 'ConsoleWidget.paging',
127 )
129 )
128 # and app_aliases from the Console Mixin
130 # and app_aliases from the Console Mixin
129 qt_aliases.update(app_aliases)
131 qt_aliases.update(app_aliases)
132 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
130 # add frontend aliases to the full set
133 # add frontend aliases to the full set
131 aliases.update(qt_aliases)
134 aliases.update(qt_aliases)
132
135
133 # get flags&aliases into sets, and remove a couple that
136 # get flags&aliases into sets, and remove a couple that
134 # shouldn't be scrubbed from backend flags:
137 # shouldn't be scrubbed from backend flags:
135 qt_aliases = set(qt_aliases.keys())
138 qt_aliases = set(qt_aliases.keys())
136 qt_aliases.remove('colors')
139 qt_aliases.remove('colors')
137 qt_flags = set(qt_flags.keys())
140 qt_flags = set(qt_flags.keys())
138
141
139 #-----------------------------------------------------------------------------
142 #-----------------------------------------------------------------------------
140 # Classes
143 # Classes
141 #-----------------------------------------------------------------------------
144 #-----------------------------------------------------------------------------
142
145
143 #-----------------------------------------------------------------------------
146 #-----------------------------------------------------------------------------
144 # IPythonQtConsole
147 # IPythonQtConsole
145 #-----------------------------------------------------------------------------
148 #-----------------------------------------------------------------------------
146
149
147
150
148 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
151 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
149 name = 'ipython-qtconsole'
152 name = 'ipython-qtconsole'
150
153
151 description = """
154 description = """
152 The IPython QtConsole.
155 The IPython QtConsole.
153
156
154 This launches a Console-style application using Qt. It is not a full
157 This launches a Console-style application using Qt. It is not a full
155 console, in that launched terminal subprocesses will not be able to accept
158 console, in that launched terminal subprocesses will not be able to accept
156 input.
159 input.
157
160
158 The QtConsole supports various extra features beyond the Terminal IPython
161 The QtConsole supports various extra features beyond the Terminal IPython
159 shell, such as inline plotting with matplotlib, via:
162 shell, such as inline plotting with matplotlib, via:
160
163
161 ipython qtconsole --pylab=inline
164 ipython qtconsole --pylab=inline
162
165
163 as well as saving your session as HTML, and printing the output.
166 as well as saving your session as HTML, and printing the output.
164
167
165 """
168 """
166 examples = _examples
169 examples = _examples
167
170
168 classes = [IPythonWidget] + IPythonConsoleApp.classes
171 classes = [IPythonWidget] + IPythonConsoleApp.classes
169 flags = Dict(flags)
172 flags = Dict(flags)
170 aliases = Dict(aliases)
173 aliases = Dict(aliases)
171 frontend_flags = Any(qt_flags)
174 frontend_flags = Any(qt_flags)
172 frontend_aliases = Any(qt_aliases)
175 frontend_aliases = Any(qt_aliases)
173 kernel_manager_class = QtKernelManager
176 kernel_manager_class = QtKernelManager
174
177
175 stylesheet = Unicode('', config=True,
178 stylesheet = Unicode('', config=True,
176 help="path to a custom CSS stylesheet")
179 help="path to a custom CSS stylesheet")
177
180
178 plain = CBool(False, config=True,
181 plain = CBool(False, config=True,
179 help="Use a plaintext widget instead of rich text (plain can't print/save).")
182 help="Use a plaintext widget instead of rich text (plain can't print/save).")
180
183
181 def _plain_changed(self, name, old, new):
184 def _plain_changed(self, name, old, new):
182 kind = 'plain' if new else 'rich'
185 kind = 'plain' if new else 'rich'
183 self.config.ConsoleWidget.kind = kind
186 self.config.ConsoleWidget.kind = kind
184 if new:
187 if new:
185 self.widget_factory = IPythonWidget
188 self.widget_factory = IPythonWidget
186 else:
189 else:
187 self.widget_factory = RichIPythonWidget
190 self.widget_factory = RichIPythonWidget
188
191
189 # the factory for creating a widget
192 # the factory for creating a widget
190 widget_factory = Any(RichIPythonWidget)
193 widget_factory = Any(RichIPythonWidget)
191
194
192 def parse_command_line(self, argv=None):
195 def parse_command_line(self, argv=None):
193 super(IPythonQtConsoleApp, self).parse_command_line(argv)
196 super(IPythonQtConsoleApp, self).parse_command_line(argv)
194 self.build_kernel_argv(argv)
197 self.build_kernel_argv(argv)
195
198
196
199
197 def new_frontend_master(self):
200 def new_frontend_master(self):
198 """ Create and return new frontend attached to new kernel, launched on localhost.
201 """ Create and return new frontend attached to new kernel, launched on localhost.
199 """
202 """
200 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
203 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
201 kernel_manager = self.kernel_manager_class(
204 kernel_manager = self.kernel_manager_class(
202 ip=ip,
205 ip=ip,
203 connection_file=self._new_connection_file(),
206 connection_file=self._new_connection_file(),
204 config=self.config,
207 config=self.config,
205 )
208 )
206 # start the kernel
209 # start the kernel
207 kwargs = dict()
210 kwargs = dict()
208 kwargs['extra_arguments'] = self.kernel_argv
211 kwargs['extra_arguments'] = self.kernel_argv
209 kernel_manager.start_kernel(**kwargs)
212 kernel_manager.start_kernel(**kwargs)
210 kernel_manager.start_channels()
213 kernel_manager.start_channels()
211 widget = self.widget_factory(config=self.config,
214 widget = self.widget_factory(config=self.config,
212 local_kernel=True)
215 local_kernel=True)
213 self.init_colors(widget)
216 self.init_colors(widget)
214 widget.kernel_manager = kernel_manager
217 widget.kernel_manager = kernel_manager
215 widget._existing = False
218 widget._existing = False
216 widget._may_close = True
219 widget._may_close = True
217 widget._confirm_exit = self.confirm_exit
220 widget._confirm_exit = self.confirm_exit
218 return widget
221 return widget
219
222
220 def new_frontend_slave(self, current_widget):
223 def new_frontend_slave(self, current_widget):
221 """Create and return a new frontend attached to an existing kernel.
224 """Create and return a new frontend attached to an existing kernel.
222
225
223 Parameters
226 Parameters
224 ----------
227 ----------
225 current_widget : IPythonWidget
228 current_widget : IPythonWidget
226 The IPythonWidget whose kernel this frontend is to share
229 The IPythonWidget whose kernel this frontend is to share
227 """
230 """
228 kernel_manager = self.kernel_manager_class(
231 kernel_manager = self.kernel_manager_class(
229 connection_file=current_widget.kernel_manager.connection_file,
232 connection_file=current_widget.kernel_manager.connection_file,
230 config = self.config,
233 config = self.config,
231 )
234 )
232 kernel_manager.load_connection_file()
235 kernel_manager.load_connection_file()
233 kernel_manager.start_channels()
236 kernel_manager.start_channels()
234 widget = self.widget_factory(config=self.config,
237 widget = self.widget_factory(config=self.config,
235 local_kernel=False)
238 local_kernel=False)
236 self.init_colors(widget)
239 self.init_colors(widget)
237 widget._existing = True
240 widget._existing = True
238 widget._may_close = False
241 widget._may_close = False
239 widget._confirm_exit = False
242 widget._confirm_exit = False
240 widget.kernel_manager = kernel_manager
243 widget.kernel_manager = kernel_manager
241 return widget
244 return widget
242
245
243 def init_qt_elements(self):
246 def init_qt_elements(self):
244 # Create the widget.
247 # Create the widget.
245 self.app = QtGui.QApplication([])
248 self.app = QtGui.QApplication([])
246
249
247 base_path = os.path.abspath(os.path.dirname(__file__))
250 base_path = os.path.abspath(os.path.dirname(__file__))
248 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
251 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
249 self.app.icon = QtGui.QIcon(icon_path)
252 self.app.icon = QtGui.QIcon(icon_path)
250 QtGui.QApplication.setWindowIcon(self.app.icon)
253 QtGui.QApplication.setWindowIcon(self.app.icon)
251
254
252 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
255 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
253 self.widget = self.widget_factory(config=self.config,
256 self.widget = self.widget_factory(config=self.config,
254 local_kernel=local_kernel)
257 local_kernel=local_kernel)
255 self.init_colors(self.widget)
258 self.init_colors(self.widget)
256 self.widget._existing = self.existing
259 self.widget._existing = self.existing
257 self.widget._may_close = not self.existing
260 self.widget._may_close = not self.existing
258 self.widget._confirm_exit = self.confirm_exit
261 self.widget._confirm_exit = self.confirm_exit
259
262
260 self.widget.kernel_manager = self.kernel_manager
263 self.widget.kernel_manager = self.kernel_manager
261 self.window = MainWindow(self.app,
264 self.window = MainWindow(self.app,
262 confirm_exit=self.confirm_exit,
265 confirm_exit=self.confirm_exit,
263 new_frontend_factory=self.new_frontend_master,
266 new_frontend_factory=self.new_frontend_master,
264 slave_frontend_factory=self.new_frontend_slave,
267 slave_frontend_factory=self.new_frontend_slave,
265 )
268 )
266 self.window.log = self.log
269 self.window.log = self.log
267 self.window.add_tab_with_frontend(self.widget)
270 self.window.add_tab_with_frontend(self.widget)
268 self.window.init_menu_bar()
271 self.window.init_menu_bar()
269
272
270 self.window.setWindowTitle('IPython')
273 self.window.setWindowTitle('IPython')
271
274
272 def init_colors(self, widget):
275 def init_colors(self, widget):
273 """Configure the coloring of the widget"""
276 """Configure the coloring of the widget"""
274 # Note: This will be dramatically simplified when colors
277 # Note: This will be dramatically simplified when colors
275 # are removed from the backend.
278 # are removed from the backend.
276
279
277 # parse the colors arg down to current known labels
280 # parse the colors arg down to current known labels
278 try:
281 try:
279 colors = self.config.ZMQInteractiveShell.colors
282 colors = self.config.ZMQInteractiveShell.colors
280 except AttributeError:
283 except AttributeError:
281 colors = None
284 colors = None
282 try:
285 try:
283 style = self.config.IPythonWidget.syntax_style
286 style = self.config.IPythonWidget.syntax_style
284 except AttributeError:
287 except AttributeError:
285 style = None
288 style = None
286 try:
289 try:
287 sheet = self.config.IPythonWidget.style_sheet
290 sheet = self.config.IPythonWidget.style_sheet
288 except AttributeError:
291 except AttributeError:
289 sheet = None
292 sheet = None
290
293
291 # find the value for colors:
294 # find the value for colors:
292 if colors:
295 if colors:
293 colors=colors.lower()
296 colors=colors.lower()
294 if colors in ('lightbg', 'light'):
297 if colors in ('lightbg', 'light'):
295 colors='lightbg'
298 colors='lightbg'
296 elif colors in ('dark', 'linux'):
299 elif colors in ('dark', 'linux'):
297 colors='linux'
300 colors='linux'
298 else:
301 else:
299 colors='nocolor'
302 colors='nocolor'
300 elif style:
303 elif style:
301 if style=='bw':
304 if style=='bw':
302 colors='nocolor'
305 colors='nocolor'
303 elif styles.dark_style(style):
306 elif styles.dark_style(style):
304 colors='linux'
307 colors='linux'
305 else:
308 else:
306 colors='lightbg'
309 colors='lightbg'
307 else:
310 else:
308 colors=None
311 colors=None
309
312
310 # Configure the style
313 # Configure the style
311 if style:
314 if style:
312 widget.style_sheet = styles.sheet_from_template(style, colors)
315 widget.style_sheet = styles.sheet_from_template(style, colors)
313 widget.syntax_style = style
316 widget.syntax_style = style
314 widget._syntax_style_changed()
317 widget._syntax_style_changed()
315 widget._style_sheet_changed()
318 widget._style_sheet_changed()
316 elif colors:
319 elif colors:
317 # use a default dark/light/bw style
320 # use a default dark/light/bw style
318 widget.set_default_style(colors=colors)
321 widget.set_default_style(colors=colors)
319
322
320 if self.stylesheet:
323 if self.stylesheet:
321 # we got an explicit stylesheet
324 # we got an explicit stylesheet
322 if os.path.isfile(self.stylesheet):
325 if os.path.isfile(self.stylesheet):
323 with open(self.stylesheet) as f:
326 with open(self.stylesheet) as f:
324 sheet = f.read()
327 sheet = f.read()
325 else:
328 else:
326 raise IOError("Stylesheet %r not found." % self.stylesheet)
329 raise IOError("Stylesheet %r not found." % self.stylesheet)
327 if sheet:
330 if sheet:
328 widget.style_sheet = sheet
331 widget.style_sheet = sheet
329 widget._style_sheet_changed()
332 widget._style_sheet_changed()
330
333
331
334
332 def init_signal(self):
335 def init_signal(self):
333 """allow clean shutdown on sigint"""
336 """allow clean shutdown on sigint"""
334 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
337 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
335 # need a timer, so that QApplication doesn't block until a real
338 # need a timer, so that QApplication doesn't block until a real
336 # Qt event fires (can require mouse movement)
339 # Qt event fires (can require mouse movement)
337 # timer trick from http://stackoverflow.com/q/4938723/938949
340 # timer trick from http://stackoverflow.com/q/4938723/938949
338 timer = QtCore.QTimer()
341 timer = QtCore.QTimer()
339 # Let the interpreter run each 200 ms:
342 # Let the interpreter run each 200 ms:
340 timer.timeout.connect(lambda: None)
343 timer.timeout.connect(lambda: None)
341 timer.start(200)
344 timer.start(200)
342 # hold onto ref, so the timer doesn't get cleaned up
345 # hold onto ref, so the timer doesn't get cleaned up
343 self._sigint_timer = timer
346 self._sigint_timer = timer
344
347
345 @catch_config_error
348 @catch_config_error
346 def initialize(self, argv=None):
349 def initialize(self, argv=None):
347 super(IPythonQtConsoleApp, self).initialize(argv)
350 super(IPythonQtConsoleApp, self).initialize(argv)
348 IPythonConsoleApp.initialize(self,argv)
351 IPythonConsoleApp.initialize(self,argv)
349 self.init_qt_elements()
352 self.init_qt_elements()
350 self.init_signal()
353 self.init_signal()
351
354
352 def start(self):
355 def start(self):
353
356
354 # draw the window
357 # draw the window
355 self.window.show()
358 self.window.show()
356 self.window.raise_()
359 self.window.raise_()
357
360
358 # Start the application main loop.
361 # Start the application main loop.
359 self.app.exec_()
362 self.app.exec_()
360
363
361 #-----------------------------------------------------------------------------
364 #-----------------------------------------------------------------------------
362 # Main entry point
365 # Main entry point
363 #-----------------------------------------------------------------------------
366 #-----------------------------------------------------------------------------
364
367
365 def main():
368 def main():
366 app = IPythonQtConsoleApp()
369 app = IPythonQtConsoleApp()
367 app.initialize()
370 app.initialize()
368 app.start()
371 app.start()
369
372
370
373
371 if __name__ == '__main__':
374 if __name__ == '__main__':
372 main()
375 main()
@@ -1,119 +1,122 b''
1 """ Style utilities, templates, and defaults for syntax highlighting widgets.
1 """ Style utilities, templates, and defaults for syntax highlighting widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 from colorsys import rgb_to_hls
7 from colorsys import rgb_to_hls
8 from pygments.styles import get_style_by_name
8 from pygments.styles import get_style_by_name
9 from pygments.token import Token
9 from pygments.token import Token
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Constants
12 # Constants
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 # The default light style sheet: black text on a white background.
15 # The default light style sheet: black text on a white background.
16 default_light_style_template = '''
16 default_light_style_template = '''
17 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
17 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
18 color: %(fgcolor)s ;
18 color: %(fgcolor)s ;
19 selection-background-color: %(select)s}
19 selection-background-color: %(select)s}
20 .error { color: red; }
20 .error { color: red; }
21 .in-prompt { color: navy; }
21 .in-prompt { color: navy; }
22 .in-prompt-number { font-weight: bold; }
22 .in-prompt-number { font-weight: bold; }
23 .out-prompt { color: darkred; }
23 .out-prompt { color: darkred; }
24 .out-prompt-number { font-weight: bold; }
24 .out-prompt-number { font-weight: bold; }
25 .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;}
25 '''
26 '''
26 default_light_style_sheet = default_light_style_template%dict(
27 default_light_style_sheet = default_light_style_template%dict(
27 bgcolor='white', fgcolor='black', select="#ccc")
28 bgcolor='white', fgcolor='black', select="#ccc")
28 default_light_syntax_style = 'default'
29 default_light_syntax_style = 'default'
29
30
30 # The default dark style sheet: white text on a black background.
31 # The default dark style sheet: white text on a black background.
31 default_dark_style_template = '''
32 default_dark_style_template = '''
32 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
33 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
33 color: %(fgcolor)s ;
34 color: %(fgcolor)s ;
34 selection-background-color: %(select)s}
35 selection-background-color: %(select)s}
35 QFrame { border: 1px solid grey; }
36 QFrame { border: 1px solid grey; }
36 .error { color: red; }
37 .error { color: red; }
37 .in-prompt { color: lime; }
38 .in-prompt { color: lime; }
38 .in-prompt-number { color: lime; font-weight: bold; }
39 .in-prompt-number { color: lime; font-weight: bold; }
39 .out-prompt { color: red; }
40 .out-prompt { color: red; }
40 .out-prompt-number { color: red; font-weight: bold; }
41 .out-prompt-number { color: red; font-weight: bold; }
42 .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;}
41 '''
43 '''
42 default_dark_style_sheet = default_dark_style_template%dict(
44 default_dark_style_sheet = default_dark_style_template%dict(
43 bgcolor='black', fgcolor='white', select="#555")
45 bgcolor='black', fgcolor='white', select="#555")
44 default_dark_syntax_style = 'monokai'
46 default_dark_syntax_style = 'monokai'
45
47
46 # The default monochrome
48 # The default monochrome
47 default_bw_style_sheet = '''
49 default_bw_style_sheet = '''
48 QPlainTextEdit, QTextEdit { background-color: white;
50 QPlainTextEdit, QTextEdit { background-color: white;
49 color: black ;
51 color: black ;
50 selection-background-color: #cccccc}
52 selection-background-color: #cccccc}
51 .in-prompt-number { font-weight: bold; }
53 .in-prompt-number { font-weight: bold; }
52 .out-prompt-number { font-weight: bold; }
54 .out-prompt-number { font-weight: bold; }
55 .inverted { background-color: black ; color: white;}
53 '''
56 '''
54 default_bw_syntax_style = 'bw'
57 default_bw_syntax_style = 'bw'
55
58
56
59
57 def hex_to_rgb(color):
60 def hex_to_rgb(color):
58 """Convert a hex color to rgb integer tuple."""
61 """Convert a hex color to rgb integer tuple."""
59 if color.startswith('#'):
62 if color.startswith('#'):
60 color = color[1:]
63 color = color[1:]
61 if len(color) == 3:
64 if len(color) == 3:
62 color = ''.join([c*2 for c in color])
65 color = ''.join([c*2 for c in color])
63 if len(color) != 6:
66 if len(color) != 6:
64 return False
67 return False
65 try:
68 try:
66 r = int(color[:2],16)
69 r = int(color[:2],16)
67 g = int(color[2:4],16)
70 g = int(color[2:4],16)
68 b = int(color[4:],16)
71 b = int(color[4:],16)
69 except ValueError:
72 except ValueError:
70 return False
73 return False
71 else:
74 else:
72 return r,g,b
75 return r,g,b
73
76
74 def dark_color(color):
77 def dark_color(color):
75 """Check whether a color is 'dark'.
78 """Check whether a color is 'dark'.
76
79
77 Currently, this is simply whether the luminance is <50%"""
80 Currently, this is simply whether the luminance is <50%"""
78 rgb = hex_to_rgb(color)
81 rgb = hex_to_rgb(color)
79 if rgb:
82 if rgb:
80 return rgb_to_hls(*rgb)[1] < 128
83 return rgb_to_hls(*rgb)[1] < 128
81 else: # default to False
84 else: # default to False
82 return False
85 return False
83
86
84 def dark_style(stylename):
87 def dark_style(stylename):
85 """Guess whether the background of the style with name 'stylename'
88 """Guess whether the background of the style with name 'stylename'
86 counts as 'dark'."""
89 counts as 'dark'."""
87 return dark_color(get_style_by_name(stylename).background_color)
90 return dark_color(get_style_by_name(stylename).background_color)
88
91
89 def get_colors(stylename):
92 def get_colors(stylename):
90 """Construct the keys to be used building the base stylesheet
93 """Construct the keys to be used building the base stylesheet
91 from a templatee."""
94 from a templatee."""
92 style = get_style_by_name(stylename)
95 style = get_style_by_name(stylename)
93 fgcolor = style.style_for_token(Token.Text)['color'] or ''
96 fgcolor = style.style_for_token(Token.Text)['color'] or ''
94 if len(fgcolor) in (3,6):
97 if len(fgcolor) in (3,6):
95 # could be 'abcdef' or 'ace' hex, which needs '#' prefix
98 # could be 'abcdef' or 'ace' hex, which needs '#' prefix
96 try:
99 try:
97 int(fgcolor, 16)
100 int(fgcolor, 16)
98 except TypeError:
101 except TypeError:
99 pass
102 pass
100 else:
103 else:
101 fgcolor = "#"+fgcolor
104 fgcolor = "#"+fgcolor
102
105
103 return dict(
106 return dict(
104 bgcolor = style.background_color,
107 bgcolor = style.background_color,
105 select = style.highlight_color,
108 select = style.highlight_color,
106 fgcolor = fgcolor
109 fgcolor = fgcolor
107 )
110 )
108
111
109 def sheet_from_template(name, colors='lightbg'):
112 def sheet_from_template(name, colors='lightbg'):
110 """Use one of the base templates, and set bg/fg/select colors."""
113 """Use one of the base templates, and set bg/fg/select colors."""
111 colors = colors.lower()
114 colors = colors.lower()
112 if colors=='lightbg':
115 if colors=='lightbg':
113 return default_light_style_template%get_colors(name)
116 return default_light_style_template%get_colors(name)
114 elif colors=='linux':
117 elif colors=='linux':
115 return default_dark_style_template%get_colors(name)
118 return default_dark_style_template%get_colors(name)
116 elif colors=='nocolor':
119 elif colors=='nocolor':
117 return default_bw_style_sheet
120 return default_bw_style_sheet
118 else:
121 else:
119 raise KeyError("No such color scheme: %s"%colors)
122 raise KeyError("No such color scheme: %s"%colors)
General Comments 0
You need to be logged in to leave comments. Login now