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