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