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