##// END OF EJS Templates
Add Ctrl-+/- to increase/decrease font size....
Fernando Perez -
Show More
@@ -1,477 +1,478 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Usage information for the main IPython applications.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2008-2010 The IPython Development Team
6 6 # Copyright (C) 2001-2007 Fernando Perez. <fperez@colorado.edu>
7 7 #
8 8 # Distributed under the terms of the BSD License. The full license is in
9 9 # the file COPYING, distributed as part of this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 import sys
13 13 from IPython.core import release
14 14
15 15 cl_usage = """\
16 16 ipython [options] [files]
17 17
18 18 IPython: an enhanced interactive Python shell.
19 19
20 20 A Python shell with automatic history (input and output), dynamic object
21 21 introspection, easier configuration, command completion, access to the
22 22 system shell and more. IPython can also be embedded in running programs.
23 23
24 24 If invoked with no options, it executes all the files listed in sequence
25 25 and exits, use -i to enter interactive mode after running the files. Files
26 26 ending in .py will be treated as normal Python, but files ending in .ipy
27 27 can contain special IPython syntax (magic commands, shell expansions, etc.)
28 28
29 29 Please note that some of the configuration options are not available at the
30 30 command line, simply because they are not practical here. Look into your
31 31 ipython_config.py configuration file for details on those.
32 32
33 33 This file typically installed in the $HOME/.ipython directory. For Windows
34 34 users, $HOME resolves to C:\\Documents and Settings\\YourUserName in most
35 35 instances.
36 36
37 37 In IPython's documentation, we will refer to this directory as IPYTHON_DIR,
38 38 you can change its default location by setting any path you want in this
39 39 environment variable.
40 40
41 41 For more information, see the manual available in HTML and PDF in your
42 42 installation, or online at http://ipython.scipy.org.
43 43 """
44 44
45 45 interactive_usage = """
46 46 IPython -- An enhanced Interactive Python
47 47 =========================================
48 48
49 49 IPython offers a combination of convenient shell features, special commands
50 50 and a history mechanism for both input (command history) and output (results
51 51 caching, similar to Mathematica). It is intended to be a fully compatible
52 52 replacement for the standard Python interpreter, while offering vastly
53 53 improved functionality and flexibility.
54 54
55 55 At your system command line, type 'ipython -help' to see the command line
56 56 options available. This document only describes interactive features.
57 57
58 58 Warning: IPython relies on the existence of a global variable called __IP which
59 59 controls the shell itself. If you redefine __IP to anything, bizarre behavior
60 60 will quickly occur.
61 61
62 62 MAIN FEATURES
63 63
64 64 * Access to the standard Python help. As of Python 2.1, a help system is
65 65 available with access to object docstrings and the Python manuals. Simply
66 66 type 'help' (no quotes) to access it.
67 67
68 68 * Magic commands: type %magic for information on the magic subsystem.
69 69
70 70 * System command aliases, via the %alias command or the ipythonrc config file.
71 71
72 72 * Dynamic object information:
73 73
74 74 Typing ?word or word? prints detailed information about an object. If
75 75 certain strings in the object are too long (docstrings, code, etc.) they get
76 76 snipped in the center for brevity.
77 77
78 78 Typing ??word or word?? gives access to the full information without
79 79 snipping long strings. Long strings are sent to the screen through the less
80 80 pager if longer than the screen, printed otherwise.
81 81
82 82 The ?/?? system gives access to the full source code for any object (if
83 83 available), shows function prototypes and other useful information.
84 84
85 85 If you just want to see an object's docstring, type '%pdoc object' (without
86 86 quotes, and without % if you have automagic on).
87 87
88 88 Both %pdoc and ?/?? give you access to documentation even on things which are
89 89 not explicitely defined. Try for example typing {}.get? or after import os,
90 90 type os.path.abspath??. The magic functions %pdef, %source and %file operate
91 91 similarly.
92 92
93 93 * Completion in the local namespace, by typing TAB at the prompt.
94 94
95 95 At any time, hitting tab will complete any available python commands or
96 96 variable names, and show you a list of the possible completions if there's
97 97 no unambiguous one. It will also complete filenames in the current directory.
98 98
99 99 This feature requires the readline and rlcomplete modules, so it won't work
100 100 if your Python lacks readline support (such as under Windows).
101 101
102 102 * Search previous command history in two ways (also requires readline):
103 103
104 104 - Start typing, and then use Ctrl-p (previous,up) and Ctrl-n (next,down) to
105 105 search through only the history items that match what you've typed so
106 106 far. If you use Ctrl-p/Ctrl-n at a blank prompt, they just behave like
107 107 normal arrow keys.
108 108
109 109 - Hit Ctrl-r: opens a search prompt. Begin typing and the system searches
110 110 your history for lines that match what you've typed so far, completing as
111 111 much as it can.
112 112
113 113 * Persistent command history across sessions (readline required).
114 114
115 115 * Logging of input with the ability to save and restore a working session.
116 116
117 117 * System escape with !. Typing !ls will run 'ls' in the current directory.
118 118
119 119 * The reload command does a 'deep' reload of a module: changes made to the
120 120 module since you imported will actually be available without having to exit.
121 121
122 122 * Verbose and colored exception traceback printouts. See the magic xmode and
123 123 xcolor functions for details (just type %magic).
124 124
125 125 * Input caching system:
126 126
127 127 IPython offers numbered prompts (In/Out) with input and output caching. All
128 128 input is saved and can be retrieved as variables (besides the usual arrow
129 129 key recall).
130 130
131 131 The following GLOBAL variables always exist (so don't overwrite them!):
132 132 _i: stores previous input.
133 133 _ii: next previous.
134 134 _iii: next-next previous.
135 135 _ih : a list of all input _ih[n] is the input from line n.
136 136
137 137 Additionally, global variables named _i<n> are dynamically created (<n>
138 138 being the prompt counter), such that _i<n> == _ih[<n>]
139 139
140 140 For example, what you typed at prompt 14 is available as _i14 and _ih[14].
141 141
142 142 You can create macros which contain multiple input lines from this history,
143 143 for later re-execution, with the %macro function.
144 144
145 145 The history function %hist allows you to see any part of your input history
146 146 by printing a range of the _i variables. Note that inputs which contain
147 147 magic functions (%) appear in the history with a prepended comment. This is
148 148 because they aren't really valid Python code, so you can't exec them.
149 149
150 150 * Output caching system:
151 151
152 152 For output that is returned from actions, a system similar to the input
153 153 cache exists but using _ instead of _i. Only actions that produce a result
154 154 (NOT assignments, for example) are cached. If you are familiar with
155 155 Mathematica, IPython's _ variables behave exactly like Mathematica's %
156 156 variables.
157 157
158 158 The following GLOBAL variables always exist (so don't overwrite them!):
159 159 _ (one underscore): previous output.
160 160 __ (two underscores): next previous.
161 161 ___ (three underscores): next-next previous.
162 162
163 163 Global variables named _<n> are dynamically created (<n> being the prompt
164 164 counter), such that the result of output <n> is always available as _<n>.
165 165
166 166 Finally, a global dictionary named _oh exists with entries for all lines
167 167 which generated output.
168 168
169 169 * Directory history:
170 170
171 171 Your history of visited directories is kept in the global list _dh, and the
172 172 magic %cd command can be used to go to any entry in that list.
173 173
174 174 * Auto-parentheses and auto-quotes (adapted from Nathan Gray's LazyPython)
175 175
176 176 1. Auto-parentheses
177 177 Callable objects (i.e. functions, methods, etc) can be invoked like
178 178 this (notice the commas between the arguments):
179 179 >>> callable_ob arg1, arg2, arg3
180 180 and the input will be translated to this:
181 181 --> callable_ob(arg1, arg2, arg3)
182 182 You can force auto-parentheses by using '/' as the first character
183 183 of a line. For example:
184 184 >>> /globals # becomes 'globals()'
185 185 Note that the '/' MUST be the first character on the line! This
186 186 won't work:
187 187 >>> print /globals # syntax error
188 188
189 189 In most cases the automatic algorithm should work, so you should
190 190 rarely need to explicitly invoke /. One notable exception is if you
191 191 are trying to call a function with a list of tuples as arguments (the
192 192 parenthesis will confuse IPython):
193 193 In [1]: zip (1,2,3),(4,5,6) # won't work
194 194 but this will work:
195 195 In [2]: /zip (1,2,3),(4,5,6)
196 196 ------> zip ((1,2,3),(4,5,6))
197 197 Out[2]= [(1, 4), (2, 5), (3, 6)]
198 198
199 199 IPython tells you that it has altered your command line by
200 200 displaying the new command line preceded by -->. e.g.:
201 201 In [18]: callable list
202 202 -------> callable (list)
203 203
204 204 2. Auto-Quoting
205 205 You can force auto-quoting of a function's arguments by using ',' as
206 206 the first character of a line. For example:
207 207 >>> ,my_function /home/me # becomes my_function("/home/me")
208 208
209 209 If you use ';' instead, the whole argument is quoted as a single
210 210 string (while ',' splits on whitespace):
211 211 >>> ,my_function a b c # becomes my_function("a","b","c")
212 212 >>> ;my_function a b c # becomes my_function("a b c")
213 213
214 214 Note that the ',' MUST be the first character on the line! This
215 215 won't work:
216 216 >>> x = ,my_function /home/me # syntax error
217 217 """
218 218
219 219 interactive_usage_min = """\
220 220 An enhanced console for Python.
221 221 Some of its features are:
222 222 - Readline support if the readline library is present.
223 223 - Tab completion in the local namespace.
224 224 - Logging of input, see command-line options.
225 225 - System shell escape via ! , eg !ls.
226 226 - Magic commands, starting with a % (like %ls, %pwd, %cd, etc.)
227 227 - Keeps track of locally defined variables via %who, %whos.
228 228 - Show object information with a ? eg ?x or x? (use ?? for more info).
229 229 """
230 230
231 231 quick_reference = r"""
232 232 IPython -- An enhanced Interactive Python - Quick Reference Card
233 233 ================================================================
234 234
235 235 obj?, obj?? : Get help, or more help for object (also works as
236 236 ?obj, ??obj).
237 237 ?foo.*abc* : List names in 'foo' containing 'abc' in them.
238 238 %magic : Information about IPython's 'magic' % functions.
239 239
240 240 Magic functions are prefixed by %, and typically take their arguments without
241 241 parentheses, quotes or even commas for convenience.
242 242
243 243 Example magic function calls:
244 244
245 245 %alias d ls -F : 'd' is now an alias for 'ls -F'
246 246 alias d ls -F : Works if 'alias' not a python name
247 247 alist = %alias : Get list of aliases to 'alist'
248 248 cd /usr/share : Obvious. cd -<tab> to choose from visited dirs.
249 249 %cd?? : See help AND source for magic %cd
250 250
251 251 System commands:
252 252
253 253 !cp a.txt b/ : System command escape, calls os.system()
254 254 cp a.txt b/ : after %rehashx, most system commands work without !
255 255 cp ${f}.txt $bar : Variable expansion in magics and system commands
256 256 files = !ls /usr : Capture sytem command output
257 257 files.s, files.l, files.n: "a b c", ['a','b','c'], 'a\nb\nc'
258 258
259 259 History:
260 260
261 261 _i, _ii, _iii : Previous, next previous, next next previous input
262 262 _i4, _ih[2:5] : Input history line 4, lines 2-4
263 263 exec _i81 : Execute input history line #81 again
264 264 %rep 81 : Edit input history line #81
265 265 _, __, ___ : previous, next previous, next next previous output
266 266 _dh : Directory history
267 267 _oh : Output history
268 268 %hist : Command history. '%hist -g foo' search history for 'foo'
269 269
270 270 Autocall:
271 271
272 272 f 1,2 : f(1,2)
273 273 /f 1,2 : f(1,2) (forced autoparen)
274 274 ,f 1 2 : f("1","2")
275 275 ;f 1 2 : f("1 2")
276 276
277 277 Remember: TAB completion works in many contexts, not just file names
278 278 or python names.
279 279
280 280 The following magic functions are currently available:
281 281
282 282 """
283 283
284 284 gui_reference = """\
285 285 ===============================
286 286 The graphical IPython console
287 287 ===============================
288 288
289 289 This console is designed to emulate the look, feel and workflow of a terminal
290 290 environment, while adding a number of enhancements that are simply not possible
291 291 in a real terminal, such as inline syntax highlighting, true multiline editing,
292 292 inline graphics and much more.
293 293
294 294 This quick reference document contains the basic information you'll need to
295 295 know to make the most efficient use of it. For the various command line
296 296 options available at startup, type ``--help`` at the command line.
297 297
298 298
299 299 Multiline editing
300 300 =================
301 301
302 302 The graphical console is capable of true multiline editing, but it also tries
303 303 to behave intuitively like a terminal when possible. If you are used to
304 304 IPyhton's old terminal behavior, you should find the transition painless, and
305 305 once you learn a few basic keybindings it will be a much more efficient
306 306 environment.
307 307
308 308 For single expressions or indented blocks, the console behaves almost like the
309 309 terminal IPython: single expressions are immediately evaluated, and indented
310 310 blocks are evaluated once a single blank line is entered::
311 311
312 312 In [1]: print "Hello IPython!" # Enter was pressed at the end of the line
313 313 Hello IPython!
314 314
315 315 In [2]: for i in range(10):
316 316 ...: print i,
317 317 ...:
318 318 0 1 2 3 4 5 6 7 8 9
319 319
320 320 If you want to enter more than one expression in a single input block
321 321 (something not possible in the terminal), you can use ``Control-Enter`` at the
322 322 end of your first line instead of ``Enter``. At that point the console goes
323 323 into 'cell mode' and even if your inputs are not indented, it will continue
324 324 accepting arbitrarily many lines until either you enter an extra blank line or
325 325 you hit ``Shift-Enter`` (the key binding that forces execution). When a
326 326 multiline cell is entered, IPython analyzes it and executes its code producing
327 327 an ``Out[n]`` prompt only for the last expression in it, while the rest of the
328 328 cell is executed as if it was a script. An example should clarify this::
329 329
330 330 In [3]: x=1 # Hit C-Enter here
331 331 ...: y=2 # from now on, regular Enter is sufficient
332 332 ...: z=3
333 333 ...: x**2 # This does *not* produce an Out[] value
334 334 ...: x+y+z # Only the last expression does
335 335 ...:
336 336 Out[3]: 6
337 337
338 338 The behavior where an extra blank line forces execution is only active if you
339 339 are actually typing at the keyboard each line, and is meant to make it mimic
340 340 the IPython terminal behavior. If you paste a long chunk of input (for example
341 341 a long script copied form an editor or web browser), it can contain arbitrarily
342 342 many intermediate blank lines and they won't cause any problems. As always,
343 343 you can then make it execute by appending a blank line *at the end* or hitting
344 344 ``Shift-Enter`` anywhere within the cell.
345 345
346 346 With the up arrow key, you can retrieve previous blocks of input that contain
347 347 multiple lines. You can move inside of a multiline cell like you would in any
348 348 text editor. When you want it executed, the simplest thing to do is to hit the
349 349 force execution key, ``Shift-Enter`` (though you can also navigate to the end
350 350 and append a blank line by using ``Enter`` twice).
351 351
352 352 If you've edited a multiline cell and accidentally navigate out of it with the
353 353 up or down arrow keys, IPython will clear the cell and replace it with the
354 354 contents of the one above or below that you navigated to. If this was an
355 355 accident and you want to retrieve the cell you were editing, use the Undo
356 356 keybinding, ``Control-z``.
357 357
358 358
359 359 Key bindings
360 360 ============
361 361
362 362 The IPython console supports most of the basic Emacs line-oriented keybindings,
363 363 in addition to some of its own.
364 364
365 365 The keybinding prefixes mean:
366 366
367 367 - ``C``: Control
368 368 - ``S``: Shift
369 369 - ``M``: Meta (typically the Alt key)
370 370
371 371 The keybindings themselves are:
372 372
373 373 - ``Enter``: insert new line (may cause execution, see above).
374 374 - ``C-Enter``: force new line, *never* causes execution.
375 375 - ``S-Enter``: *force* execution regardless of where cursor is, no newline added.
376 376 - ``C-c``: copy highlighted text to clipboard (prompts are automatically stripped).
377 377 - ``C-S-c``: copy highlighted text to clipboard (prompts are not stripped).
378 378 - ``C-v``: paste text from clipboard.
379 379 - ``C-z``: undo (retrieves lost text if you move out of a cell with the arrows).
380 380 - ``C-S-z``: redo.
381 381 - ``C-o``: move to 'other' area, between pager and terminal.
382 382 - ``C-l``: clear terminal.
383 383 - ``C-a``: go to beginning of line.
384 384 - ``C-e``: go to end of line.
385 385 - ``C-k``: kill from cursor to the end of the line.
386 386 - ``C-y``: yank (paste)
387 387 - ``C-p``: previous line (like up arrow)
388 388 - ``C-n``: next line (like down arrow)
389 389 - ``C-f``: forward (like right arrow)
390 390 - ``C-b``: back (like left arrow)
391 391 - ``C-d``: delete next character.
392 392 - ``M-<``: move to the beginning of the input region.
393 393 - ``M->``: move to the end of the input region.
394 394 - ``M-d``: delete next word.
395 395 - ``M-Backspace``: delete previous word.
396 396 - ``C-.``: force a kernel restart (a confirmation dialog appears).
397
397 - ``C-+``: increase font size.
398 - ``C--``: decrease font size.
398 399
399 400 The IPython pager
400 401 =================
401 402
402 403 IPython will show long blocks of text from many sources using a builtin pager.
403 404 You can control where this pager appears with the ``--paging`` command-line
404 405 flag:
405 406
406 407 - default: it is overlaid on top of the main terminal. You must quit the pager
407 408 to get back to the terminal (similar to how a pager such as ``less`` or
408 409 ``more`` works).
409 410
410 411 - vertical: the console is made double-tall, and the pager appears on the
411 412 bottom area when needed. You can view its contents while using the terminal.
412 413
413 414 - horizontal: the console is made double-wide, and the pager appears on the
414 415 right area when needed. You can view its contents while using the terminal.
415 416
416 417 If you use the vertical or horizontal paging modes, you can navigate between
417 418 terminal and pager as follows:
418 419
419 420 - Tab key: goes from pager to terminal (but not the other way around).
420 421 - Control-o: goes from one to another always.
421 422 - Mouse: click on either.
422 423
423 424 In all cases, the ``q`` or ``Escape`` keys quit the pager (when used with the
424 425 focus on the pager area).
425 426
426 427
427 428 Running subprocesses
428 429 ====================
429 430
430 431 The graphical IPython console uses the ``pexpect`` module to run subprocesses
431 432 when you type ``!command``. This has a number of advantages (true asynchronous
432 433 output from subprocesses as well as very robust termination of rogue
433 434 subprocesses with ``Control-C``), as well as some limitations. The main
434 435 limitation is that you can *not* interact back with the subprocess, so anything
435 436 that invokes a pager or expects you to type input into it will block and hang
436 437 (you can kill it with ``Control-C``).
437 438
438 439 We have provided as magics ``%less`` to page files (aliased to ``%more``),
439 440 ``%clear`` to clear the terminal, and ``%man`` on Linux/OSX. These cover the
440 441 most common commands you'd want to call in your subshell and that would cause
441 442 problems if invoked via ``!cmd``, but you need to be aware of this limitation.
442 443
443 444
444 445 Inline matplotlib graphics
445 446 ==========================
446 447
447 448 The IPython console is capable of displaying matplotlib figures inline, in SVG
448 449 format. If started with the ``--pylab inline`` flag, then all figures are
449 450 rendered inline automatically. If started with ``--pylab`` or ``--pylab <your
450 451 backend>``, then a GUI backend will be used, but the ``pastefig()`` function is
451 452 added to the global and ``plt`` namespaces. You can paste any figure that is
452 453 currently open in a window with this function; type ``pastefig?`` for
453 454 additional details."""
454 455
455 456 quick_guide = """\
456 457 ? -> Introduction and overview of IPython's features.
457 458 %quickref -> Quick reference.
458 459 help -> Python's own help system.
459 460 object? -> Details about 'object', use 'object??' for extra details.
460 461 """
461 462
462 463 gui_note = """\
463 464 %guiref -> A brief reference about the graphical user interface.
464 465 """
465 466
466 467 default_banner_parts = [
467 468 'Python %s\n' % (sys.version.split('\n')[0],),
468 469 'Type "copyright", "credits" or "license" for more information.\n\n',
469 470 'IPython %s -- An enhanced Interactive Python.\n' % (release.version,),
470 471 quick_guide
471 472 ]
472 473
473 474 default_gui_banner_parts = default_banner_parts + [gui_note]
474 475
475 476 default_banner = ''.join(default_banner_parts)
476 477
477 478 default_gui_banner = ''.join(default_gui_banner_parts)
@@ -1,1638 +1,1653 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 from os.path import commonprefix
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12
13 13 # System library imports
14 14 from PyQt4 import QtCore, QtGui
15 15
16 16 # Local imports
17 17 from IPython.config.configurable import Configurable
18 18 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
19 19 from IPython.utils.traitlets import Bool, Enum, Int
20 20 from ansi_code_processor import QtAnsiCodeProcessor
21 21 from completion_widget import CompletionWidget
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Classes
25 25 #-----------------------------------------------------------------------------
26 26
27 27 class ConsoleWidget(Configurable, QtGui.QWidget):
28 28 """ An abstract base class for console-type widgets. This class has
29 29 functionality for:
30 30
31 31 * Maintaining a prompt and editing region
32 32 * Providing the traditional Unix-style console keyboard shortcuts
33 33 * Performing tab completion
34 34 * Paging text
35 35 * Handling ANSI escape codes
36 36
37 37 ConsoleWidget also provides a number of utility methods that will be
38 38 convenient to implementors of a console-style widget.
39 39 """
40 40 __metaclass__ = MetaQObjectHasTraits
41 41
42 42 #------ Configuration ------------------------------------------------------
43 43
44 44 # Whether to process ANSI escape codes.
45 45 ansi_codes = Bool(True, config=True)
46 46
47 47 # The maximum number of lines of text before truncation. Specifying a
48 48 # non-positive number disables text truncation (not recommended).
49 49 buffer_size = Int(500, config=True)
50 50
51 51 # Whether to use a list widget or plain text output for tab completion.
52 52 gui_completion = Bool(False, config=True)
53 53
54 54 # The type of underlying text widget to use. Valid values are 'plain', which
55 55 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
56 56 # NOTE: this value can only be specified during initialization.
57 57 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
58 58
59 59 # The type of paging to use. Valid values are:
60 60 # 'inside' : The widget pages like a traditional terminal.
61 61 # 'hsplit' : When paging is requested, the widget is split
62 62 # horizontally. The top pane contains the console, and the
63 63 # bottom pane contains the paged text.
64 64 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
65 65 # 'custom' : No action is taken by the widget beyond emitting a
66 66 # 'custom_page_requested(str)' signal.
67 67 # 'none' : The text is written directly to the console.
68 68 # NOTE: this value can only be specified during initialization.
69 69 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
70 70 default_value='inside', config=True)
71 71
72 72 # Whether to override ShortcutEvents for the keybindings defined by this
73 73 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
74 74 # priority (when it has focus) over, e.g., window-level menu shortcuts.
75 75 override_shortcuts = Bool(False)
76 76
77 77 #------ Signals ------------------------------------------------------------
78 78
79 79 # Signals that indicate ConsoleWidget state.
80 80 copy_available = QtCore.pyqtSignal(bool)
81 81 redo_available = QtCore.pyqtSignal(bool)
82 82 undo_available = QtCore.pyqtSignal(bool)
83 83
84 84 # Signal emitted when paging is needed and the paging style has been
85 85 # specified as 'custom'.
86 86 custom_page_requested = QtCore.pyqtSignal(object)
87 87
88 88 # Signal emitted when the font is changed.
89 89 font_changed = QtCore.pyqtSignal(QtGui.QFont)
90 90
91 91 #------ Protected class variables ------------------------------------------
92 92
93 93 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
94 94 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
95 95 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
96 96 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
97 97 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
98 98 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
99 99 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
100 100
101 101 _shortcuts = set(_ctrl_down_remap.keys() +
102 102 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
103 103 QtCore.Qt.Key_V ])
104 104
105 105 #---------------------------------------------------------------------------
106 106 # 'QObject' interface
107 107 #---------------------------------------------------------------------------
108 108
109 109 def __init__(self, parent=None, **kw):
110 110 """ Create a ConsoleWidget.
111 111
112 112 Parameters:
113 113 -----------
114 114 parent : QWidget, optional [default None]
115 115 The parent for this widget.
116 116 """
117 117 QtGui.QWidget.__init__(self, parent)
118 118 Configurable.__init__(self, **kw)
119 119
120 120 # Create the layout and underlying text widget.
121 121 layout = QtGui.QStackedLayout(self)
122 122 layout.setContentsMargins(0, 0, 0, 0)
123 123 self._control = self._create_control()
124 124 self._page_control = None
125 125 self._splitter = None
126 126 if self.paging in ('hsplit', 'vsplit'):
127 127 self._splitter = QtGui.QSplitter()
128 128 if self.paging == 'hsplit':
129 129 self._splitter.setOrientation(QtCore.Qt.Horizontal)
130 130 else:
131 131 self._splitter.setOrientation(QtCore.Qt.Vertical)
132 132 self._splitter.addWidget(self._control)
133 133 layout.addWidget(self._splitter)
134 134 else:
135 135 layout.addWidget(self._control)
136 136
137 137 # Create the paging widget, if necessary.
138 138 if self.paging in ('inside', 'hsplit', 'vsplit'):
139 139 self._page_control = self._create_page_control()
140 140 if self._splitter:
141 141 self._page_control.hide()
142 142 self._splitter.addWidget(self._page_control)
143 143 else:
144 144 layout.addWidget(self._page_control)
145 145
146 146 # Initialize protected variables. Some variables contain useful state
147 147 # information for subclasses; they should be considered read-only.
148 148 self._ansi_processor = QtAnsiCodeProcessor()
149 149 self._completion_widget = CompletionWidget(self._control)
150 150 self._continuation_prompt = '> '
151 151 self._continuation_prompt_html = None
152 152 self._executing = False
153 153 self._filter_drag = False
154 154 self._filter_resize = False
155 155 self._prompt = ''
156 156 self._prompt_html = None
157 157 self._prompt_pos = 0
158 158 self._prompt_sep = ''
159 159 self._reading = False
160 160 self._reading_callback = None
161 161 self._tab_width = 8
162 162 self._text_completing_pos = 0
163 163
164 164 # Set a monospaced font.
165 165 self.reset_font()
166 166
167 167 def eventFilter(self, obj, event):
168 168 """ Reimplemented to ensure a console-like behavior in the underlying
169 169 text widgets.
170 170 """
171 171 etype = event.type()
172 172 if etype == QtCore.QEvent.KeyPress:
173 173
174 174 # Re-map keys for all filtered widgets.
175 175 key = event.key()
176 176 if self._control_key_down(event.modifiers()) and \
177 177 key in self._ctrl_down_remap:
178 178 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
179 179 self._ctrl_down_remap[key],
180 180 QtCore.Qt.NoModifier)
181 181 QtGui.qApp.sendEvent(obj, new_event)
182 182 return True
183 183
184 184 elif obj == self._control:
185 185 return self._event_filter_console_keypress(event)
186 186
187 187 elif obj == self._page_control:
188 188 return self._event_filter_page_keypress(event)
189 189
190 190 # Make middle-click paste safe.
191 191 elif etype == QtCore.QEvent.MouseButtonRelease and \
192 192 event.button() == QtCore.Qt.MidButton and \
193 193 obj == self._control.viewport():
194 194 cursor = self._control.cursorForPosition(event.pos())
195 195 self._control.setTextCursor(cursor)
196 196 self.paste(QtGui.QClipboard.Selection)
197 197 return True
198 198
199 199 # Manually adjust the scrollbars *after* a resize event is dispatched.
200 200 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
201 201 self._filter_resize = True
202 202 QtGui.qApp.sendEvent(obj, event)
203 203 self._adjust_scrollbars()
204 204 self._filter_resize = False
205 205 return True
206 206
207 207 # Override shortcuts for all filtered widgets.
208 208 elif etype == QtCore.QEvent.ShortcutOverride and \
209 209 self.override_shortcuts and \
210 210 self._control_key_down(event.modifiers()) and \
211 211 event.key() in self._shortcuts:
212 212 event.accept()
213 213
214 214 # Ensure that drags are safe. The problem is that the drag starting
215 215 # logic, which determines whether the drag is a Copy or Move, is locked
216 216 # down in QTextControl. If the widget is editable, which it must be if
217 217 # we're not executing, the drag will be a Move. The following hack
218 218 # prevents QTextControl from deleting the text by clearing the selection
219 219 # when a drag leave event originating from this widget is dispatched.
220 220 # The fact that we have to clear the user's selection is unfortunate,
221 221 # but the alternative--trying to prevent Qt from using its hardwired
222 222 # drag logic and writing our own--is worse.
223 223 elif etype == QtCore.QEvent.DragEnter and \
224 224 obj == self._control.viewport() and \
225 225 event.source() == self._control.viewport():
226 226 self._filter_drag = True
227 227 elif etype == QtCore.QEvent.DragLeave and \
228 228 obj == self._control.viewport() and \
229 229 self._filter_drag:
230 230 cursor = self._control.textCursor()
231 231 cursor.clearSelection()
232 232 self._control.setTextCursor(cursor)
233 233 self._filter_drag = False
234 234
235 235 # Ensure that drops are safe.
236 236 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
237 237 cursor = self._control.cursorForPosition(event.pos())
238 238 if self._in_buffer(cursor.position()):
239 239 text = unicode(event.mimeData().text())
240 240 self._insert_plain_text_into_buffer(cursor, text)
241 241
242 242 # Qt is expecting to get something here--drag and drop occurs in its
243 243 # own event loop. Send a DragLeave event to end it.
244 244 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
245 245 return True
246 246
247 247 return super(ConsoleWidget, self).eventFilter(obj, event)
248 248
249 249 #---------------------------------------------------------------------------
250 250 # 'QWidget' interface
251 251 #---------------------------------------------------------------------------
252 252
253 253 def sizeHint(self):
254 254 """ Reimplemented to suggest a size that is 80 characters wide and
255 255 25 lines high.
256 256 """
257 257 font_metrics = QtGui.QFontMetrics(self.font)
258 258 margin = (self._control.frameWidth() +
259 259 self._control.document().documentMargin()) * 2
260 260 style = self.style()
261 261 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
262 262
263 263 # Note 1: Despite my best efforts to take the various margins into
264 264 # account, the width is still coming out a bit too small, so we include
265 265 # a fudge factor of one character here.
266 266 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
267 267 # to a Qt bug on certain Mac OS systems where it returns 0.
268 268 width = font_metrics.width(' ') * 81 + margin
269 269 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
270 270 if self.paging == 'hsplit':
271 271 width = width * 2 + splitwidth
272 272
273 273 height = font_metrics.height() * 25 + margin
274 274 if self.paging == 'vsplit':
275 275 height = height * 2 + splitwidth
276 276
277 277 return QtCore.QSize(width, height)
278 278
279 279 #---------------------------------------------------------------------------
280 280 # 'ConsoleWidget' public interface
281 281 #---------------------------------------------------------------------------
282 282
283 283 def can_copy(self):
284 284 """ Returns whether text can be copied to the clipboard.
285 285 """
286 286 return self._control.textCursor().hasSelection()
287 287
288 288 def can_cut(self):
289 289 """ Returns whether text can be cut to the clipboard.
290 290 """
291 291 cursor = self._control.textCursor()
292 292 return (cursor.hasSelection() and
293 293 self._in_buffer(cursor.anchor()) and
294 294 self._in_buffer(cursor.position()))
295 295
296 296 def can_paste(self):
297 297 """ Returns whether text can be pasted from the clipboard.
298 298 """
299 299 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
300 300 return not QtGui.QApplication.clipboard().text().isEmpty()
301 301 return False
302 302
303 303 def clear(self, keep_input=True):
304 304 """ Clear the console.
305 305
306 306 Parameters:
307 307 -----------
308 308 keep_input : bool, optional (default True)
309 309 If set, restores the old input buffer if a new prompt is written.
310 310 """
311 311 if self._executing:
312 312 self._control.clear()
313 313 else:
314 314 if keep_input:
315 315 input_buffer = self.input_buffer
316 316 self._control.clear()
317 317 self._show_prompt()
318 318 if keep_input:
319 319 self.input_buffer = input_buffer
320 320
321 321 def copy(self):
322 322 """ Copy the currently selected text to the clipboard.
323 323 """
324 324 self._control.copy()
325 325
326 326 def cut(self):
327 327 """ Copy the currently selected text to the clipboard and delete it
328 328 if it's inside the input buffer.
329 329 """
330 330 self.copy()
331 331 if self.can_cut():
332 332 self._control.textCursor().removeSelectedText()
333 333
334 334 def execute(self, source=None, hidden=False, interactive=False):
335 335 """ Executes source or the input buffer, possibly prompting for more
336 336 input.
337 337
338 338 Parameters:
339 339 -----------
340 340 source : str, optional
341 341
342 342 The source to execute. If not specified, the input buffer will be
343 343 used. If specified and 'hidden' is False, the input buffer will be
344 344 replaced with the source before execution.
345 345
346 346 hidden : bool, optional (default False)
347 347
348 348 If set, no output will be shown and the prompt will not be modified.
349 349 In other words, it will be completely invisible to the user that
350 350 an execution has occurred.
351 351
352 352 interactive : bool, optional (default False)
353 353
354 354 Whether the console is to treat the source as having been manually
355 355 entered by the user. The effect of this parameter depends on the
356 356 subclass implementation.
357 357
358 358 Raises:
359 359 -------
360 360 RuntimeError
361 361 If incomplete input is given and 'hidden' is True. In this case,
362 362 it is not possible to prompt for more input.
363 363
364 364 Returns:
365 365 --------
366 366 A boolean indicating whether the source was executed.
367 367 """
368 368 # WARNING: The order in which things happen here is very particular, in
369 369 # large part because our syntax highlighting is fragile. If you change
370 370 # something, test carefully!
371 371
372 372 # Decide what to execute.
373 373 if source is None:
374 374 source = self.input_buffer
375 375 if not hidden:
376 376 # A newline is appended later, but it should be considered part
377 377 # of the input buffer.
378 378 source += '\n'
379 379 elif not hidden:
380 380 self.input_buffer = source
381 381
382 382 # Execute the source or show a continuation prompt if it is incomplete.
383 383 complete = self._is_complete(source, interactive)
384 384 if hidden:
385 385 if complete:
386 386 self._execute(source, hidden)
387 387 else:
388 388 error = 'Incomplete noninteractive input: "%s"'
389 389 raise RuntimeError(error % source)
390 390 else:
391 391 if complete:
392 392 self._append_plain_text('\n')
393 393 self._executing_input_buffer = self.input_buffer
394 394 self._executing = True
395 395 self._prompt_finished()
396 396
397 397 # The maximum block count is only in effect during execution.
398 398 # This ensures that _prompt_pos does not become invalid due to
399 399 # text truncation.
400 400 self._control.document().setMaximumBlockCount(self.buffer_size)
401 401
402 402 # Setting a positive maximum block count will automatically
403 403 # disable the undo/redo history, but just to be safe:
404 404 self._control.setUndoRedoEnabled(False)
405 405
406 406 # Perform actual execution.
407 407 self._execute(source, hidden)
408 408
409 409 else:
410 410 # Do this inside an edit block so continuation prompts are
411 411 # removed seamlessly via undo/redo.
412 412 cursor = self._get_end_cursor()
413 413 cursor.beginEditBlock()
414 414 cursor.insertText('\n')
415 415 self._insert_continuation_prompt(cursor)
416 416 cursor.endEditBlock()
417 417
418 418 # Do not do this inside the edit block. It works as expected
419 419 # when using a QPlainTextEdit control, but does not have an
420 420 # effect when using a QTextEdit. I believe this is a Qt bug.
421 421 self._control.moveCursor(QtGui.QTextCursor.End)
422 422
423 423 return complete
424 424
425 425 def _get_input_buffer(self):
426 426 """ The text that the user has entered entered at the current prompt.
427 427 """
428 428 # If we're executing, the input buffer may not even exist anymore due to
429 429 # the limit imposed by 'buffer_size'. Therefore, we store it.
430 430 if self._executing:
431 431 return self._executing_input_buffer
432 432
433 433 cursor = self._get_end_cursor()
434 434 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
435 435 input_buffer = unicode(cursor.selection().toPlainText())
436 436
437 437 # Strip out continuation prompts.
438 438 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
439 439
440 440 def _set_input_buffer(self, string):
441 441 """ Replaces the text in the input buffer with 'string'.
442 442 """
443 443 # For now, it is an error to modify the input buffer during execution.
444 444 if self._executing:
445 445 raise RuntimeError("Cannot change input buffer during execution.")
446 446
447 447 # Remove old text.
448 448 cursor = self._get_end_cursor()
449 449 cursor.beginEditBlock()
450 450 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
451 451 cursor.removeSelectedText()
452 452
453 453 # Insert new text with continuation prompts.
454 454 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
455 455 cursor.endEditBlock()
456 456 self._control.moveCursor(QtGui.QTextCursor.End)
457 457
458 458 input_buffer = property(_get_input_buffer, _set_input_buffer)
459 459
460 460 def _get_font(self):
461 461 """ The base font being used by the ConsoleWidget.
462 462 """
463 463 return self._control.document().defaultFont()
464 464
465 465 def _set_font(self, font):
466 466 """ Sets the base font for the ConsoleWidget to the specified QFont.
467 467 """
468 468 font_metrics = QtGui.QFontMetrics(font)
469 469 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
470 470
471 471 self._completion_widget.setFont(font)
472 472 self._control.document().setDefaultFont(font)
473 473 if self._page_control:
474 474 self._page_control.document().setDefaultFont(font)
475 475
476 476 self.font_changed.emit(font)
477 477
478 478 font = property(_get_font, _set_font)
479 479
480 480 def paste(self, mode=QtGui.QClipboard.Clipboard):
481 481 """ Paste the contents of the clipboard into the input region.
482 482
483 483 Parameters:
484 484 -----------
485 485 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
486 486
487 487 Controls which part of the system clipboard is used. This can be
488 488 used to access the selection clipboard in X11 and the Find buffer
489 489 in Mac OS. By default, the regular clipboard is used.
490 490 """
491 491 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
492 492 # Make sure the paste is safe.
493 493 self._keep_cursor_in_buffer()
494 494 cursor = self._control.textCursor()
495 495
496 496 # Remove any trailing newline, which confuses the GUI and forces the
497 497 # user to backspace.
498 498 text = unicode(QtGui.QApplication.clipboard().text(mode)).rstrip()
499 499 self._insert_plain_text_into_buffer(cursor, dedent(text))
500 500
501 501 def print_(self, printer):
502 502 """ Print the contents of the ConsoleWidget to the specified QPrinter.
503 503 """
504 504 self._control.print_(printer)
505 505
506 506 def prompt_to_top(self):
507 507 """ Moves the prompt to the top of the viewport.
508 508 """
509 509 if not self._executing:
510 510 prompt_cursor = self._get_prompt_cursor()
511 511 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
512 512 self._set_cursor(prompt_cursor)
513 513 self._set_top_cursor(prompt_cursor)
514 514
515 515 def redo(self):
516 516 """ Redo the last operation. If there is no operation to redo, nothing
517 517 happens.
518 518 """
519 519 self._control.redo()
520 520
521 521 def reset_font(self):
522 522 """ Sets the font to the default fixed-width font for this platform.
523 523 """
524 524 if sys.platform == 'win32':
525 525 # Consolas ships with Vista/Win7, fallback to Courier if needed
526 526 family, fallback = 'Consolas', 'Courier'
527 527 elif sys.platform == 'darwin':
528 528 # OSX always has Monaco, no need for a fallback
529 529 family, fallback = 'Monaco', None
530 530 else:
531 531 # FIXME: remove Consolas as a default on Linux once our font
532 532 # selections are configurable by the user.
533 533 family, fallback = 'Consolas', 'Monospace'
534 534 font = get_font(family, fallback)
535 535 font.setPointSize(QtGui.qApp.font().pointSize())
536 536 font.setStyleHint(QtGui.QFont.TypeWriter)
537 537 self._set_font(font)
538 538
539 def change_font_size(self, delta):
540 """Change the font size by the specified amount (in points).
541 """
542 font = self.font
543 font.setPointSize(font.pointSize() + delta)
544 self._set_font(font)
545
539 546 def select_all(self):
540 547 """ Selects all the text in the buffer.
541 548 """
542 549 self._control.selectAll()
543 550
544 551 def _get_tab_width(self):
545 552 """ The width (in terms of space characters) for tab characters.
546 553 """
547 554 return self._tab_width
548 555
549 556 def _set_tab_width(self, tab_width):
550 557 """ Sets the width (in terms of space characters) for tab characters.
551 558 """
552 559 font_metrics = QtGui.QFontMetrics(self.font)
553 560 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
554 561
555 562 self._tab_width = tab_width
556 563
557 564 tab_width = property(_get_tab_width, _set_tab_width)
558 565
559 566 def undo(self):
560 567 """ Undo the last operation. If there is no operation to undo, nothing
561 568 happens.
562 569 """
563 570 self._control.undo()
564 571
565 572 #---------------------------------------------------------------------------
566 573 # 'ConsoleWidget' abstract interface
567 574 #---------------------------------------------------------------------------
568 575
569 576 def _is_complete(self, source, interactive):
570 577 """ Returns whether 'source' can be executed. When triggered by an
571 578 Enter/Return key press, 'interactive' is True; otherwise, it is
572 579 False.
573 580 """
574 581 raise NotImplementedError
575 582
576 583 def _execute(self, source, hidden):
577 584 """ Execute 'source'. If 'hidden', do not show any output.
578 585 """
579 586 raise NotImplementedError
580 587
581 588 def _prompt_started_hook(self):
582 589 """ Called immediately after a new prompt is displayed.
583 590 """
584 591 pass
585 592
586 593 def _prompt_finished_hook(self):
587 594 """ Called immediately after a prompt is finished, i.e. when some input
588 595 will be processed and a new prompt displayed.
589 596 """
590 597 pass
591 598
592 599 def _up_pressed(self):
593 600 """ Called when the up key is pressed. Returns whether to continue
594 601 processing the event.
595 602 """
596 603 return True
597 604
598 605 def _down_pressed(self):
599 606 """ Called when the down key is pressed. Returns whether to continue
600 607 processing the event.
601 608 """
602 609 return True
603 610
604 611 def _tab_pressed(self):
605 612 """ Called when the tab key is pressed. Returns whether to continue
606 613 processing the event.
607 614 """
608 615 return False
609 616
610 617 #--------------------------------------------------------------------------
611 618 # 'ConsoleWidget' protected interface
612 619 #--------------------------------------------------------------------------
613 620
614 621 def _append_html(self, html):
615 622 """ Appends html at the end of the console buffer.
616 623 """
617 624 cursor = self._get_end_cursor()
618 625 self._insert_html(cursor, html)
619 626
620 627 def _append_html_fetching_plain_text(self, html):
621 628 """ Appends 'html', then returns the plain text version of it.
622 629 """
623 630 cursor = self._get_end_cursor()
624 631 return self._insert_html_fetching_plain_text(cursor, html)
625 632
626 633 def _append_plain_text(self, text):
627 634 """ Appends plain text at the end of the console buffer, processing
628 635 ANSI codes if enabled.
629 636 """
630 637 cursor = self._get_end_cursor()
631 638 self._insert_plain_text(cursor, text)
632 639
633 640 def _append_plain_text_keeping_prompt(self, text):
634 641 """ Writes 'text' after the current prompt, then restores the old prompt
635 642 with its old input buffer.
636 643 """
637 644 input_buffer = self.input_buffer
638 645 self._append_plain_text('\n')
639 646 self._prompt_finished()
640 647
641 648 self._append_plain_text(text)
642 649 self._show_prompt()
643 650 self.input_buffer = input_buffer
644 651
645 652 def _cancel_text_completion(self):
646 653 """ If text completion is progress, cancel it.
647 654 """
648 655 if self._text_completing_pos:
649 656 self._clear_temporary_buffer()
650 657 self._text_completing_pos = 0
651 658
652 659 def _clear_temporary_buffer(self):
653 660 """ Clears the "temporary text" buffer, i.e. all the text following
654 661 the prompt region.
655 662 """
656 663 # Select and remove all text below the input buffer.
657 664 cursor = self._get_prompt_cursor()
658 665 prompt = self._continuation_prompt.lstrip()
659 666 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
660 667 temp_cursor = QtGui.QTextCursor(cursor)
661 668 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
662 669 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
663 670 if not text.startswith(prompt):
664 671 break
665 672 else:
666 673 # We've reached the end of the input buffer and no text follows.
667 674 return
668 675 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
669 676 cursor.movePosition(QtGui.QTextCursor.End,
670 677 QtGui.QTextCursor.KeepAnchor)
671 678 cursor.removeSelectedText()
672 679
673 680 # After doing this, we have no choice but to clear the undo/redo
674 681 # history. Otherwise, the text is not "temporary" at all, because it
675 682 # can be recalled with undo/redo. Unfortunately, Qt does not expose
676 683 # fine-grained control to the undo/redo system.
677 684 if self._control.isUndoRedoEnabled():
678 685 self._control.setUndoRedoEnabled(False)
679 686 self._control.setUndoRedoEnabled(True)
680 687
681 688 def _complete_with_items(self, cursor, items):
682 689 """ Performs completion with 'items' at the specified cursor location.
683 690 """
684 691 self._cancel_text_completion()
685 692
686 693 if len(items) == 1:
687 694 cursor.setPosition(self._control.textCursor().position(),
688 695 QtGui.QTextCursor.KeepAnchor)
689 696 cursor.insertText(items[0])
690 697
691 698 elif len(items) > 1:
692 699 current_pos = self._control.textCursor().position()
693 700 prefix = commonprefix(items)
694 701 if prefix:
695 702 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
696 703 cursor.insertText(prefix)
697 704 current_pos = cursor.position()
698 705
699 706 if self.gui_completion:
700 707 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
701 708 self._completion_widget.show_items(cursor, items)
702 709 else:
703 710 cursor.beginEditBlock()
704 711 self._append_plain_text('\n')
705 712 self._page(self._format_as_columns(items))
706 713 cursor.endEditBlock()
707 714
708 715 cursor.setPosition(current_pos)
709 716 self._control.moveCursor(QtGui.QTextCursor.End)
710 717 self._control.setTextCursor(cursor)
711 718 self._text_completing_pos = current_pos
712 719
713 720 def _context_menu_make(self, pos):
714 721 """ Creates a context menu for the given QPoint (in widget coordinates).
715 722 """
716 723 menu = QtGui.QMenu()
717 724
718 725 cut_action = menu.addAction('Cut', self.cut)
719 726 cut_action.setEnabled(self.can_cut())
720 727 cut_action.setShortcut(QtGui.QKeySequence.Cut)
721 728
722 729 copy_action = menu.addAction('Copy', self.copy)
723 730 copy_action.setEnabled(self.can_copy())
724 731 copy_action.setShortcut(QtGui.QKeySequence.Copy)
725 732
726 733 paste_action = menu.addAction('Paste', self.paste)
727 734 paste_action.setEnabled(self.can_paste())
728 735 paste_action.setShortcut(QtGui.QKeySequence.Paste)
729 736
730 737 menu.addSeparator()
731 738 menu.addAction('Select All', self.select_all)
732 739
733 740 return menu
734 741
735 742 def _control_key_down(self, modifiers, include_command=True):
736 743 """ Given a KeyboardModifiers flags object, return whether the Control
737 744 key is down.
738 745
739 746 Parameters:
740 747 -----------
741 748 include_command : bool, optional (default True)
742 749 Whether to treat the Command key as a (mutually exclusive) synonym
743 750 for Control when in Mac OS.
744 751 """
745 752 # Note that on Mac OS, ControlModifier corresponds to the Command key
746 753 # while MetaModifier corresponds to the Control key.
747 754 if sys.platform == 'darwin':
748 755 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
749 756 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
750 757 else:
751 758 return bool(modifiers & QtCore.Qt.ControlModifier)
752 759
753 760 def _create_control(self):
754 761 """ Creates and connects the underlying text widget.
755 762 """
756 763 # Create the underlying control.
757 764 if self.kind == 'plain':
758 765 control = QtGui.QPlainTextEdit()
759 766 elif self.kind == 'rich':
760 767 control = QtGui.QTextEdit()
761 768 control.setAcceptRichText(False)
762 769
763 770 # Install event filters. The filter on the viewport is needed for
764 771 # mouse events and drag events.
765 772 control.installEventFilter(self)
766 773 control.viewport().installEventFilter(self)
767 774
768 775 # Connect signals.
769 776 control.cursorPositionChanged.connect(self._cursor_position_changed)
770 777 control.customContextMenuRequested.connect(
771 778 self._custom_context_menu_requested)
772 779 control.copyAvailable.connect(self.copy_available)
773 780 control.redoAvailable.connect(self.redo_available)
774 781 control.undoAvailable.connect(self.undo_available)
775 782
776 783 # Hijack the document size change signal to prevent Qt from adjusting
777 784 # the viewport's scrollbar. We are relying on an implementation detail
778 785 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
779 786 # this functionality we cannot create a nice terminal interface.
780 787 layout = control.document().documentLayout()
781 788 layout.documentSizeChanged.disconnect()
782 789 layout.documentSizeChanged.connect(self._adjust_scrollbars)
783 790
784 791 # Configure the control.
785 792 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
786 793 control.setReadOnly(True)
787 794 control.setUndoRedoEnabled(False)
788 795 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
789 796 return control
790 797
791 798 def _create_page_control(self):
792 799 """ Creates and connects the underlying paging widget.
793 800 """
794 801 if self.kind == 'plain':
795 802 control = QtGui.QPlainTextEdit()
796 803 elif self.kind == 'rich':
797 804 control = QtGui.QTextEdit()
798 805 control.installEventFilter(self)
799 806 control.setReadOnly(True)
800 807 control.setUndoRedoEnabled(False)
801 808 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
802 809 return control
803 810
804 811 def _event_filter_console_keypress(self, event):
805 812 """ Filter key events for the underlying text widget to create a
806 813 console-like interface.
807 814 """
808 815 intercepted = False
809 816 cursor = self._control.textCursor()
810 817 position = cursor.position()
811 818 key = event.key()
812 819 ctrl_down = self._control_key_down(event.modifiers())
813 820 alt_down = event.modifiers() & QtCore.Qt.AltModifier
814 821 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
815 822
816 823 #------ Special sequences ----------------------------------------------
817 824
818 825 if event.matches(QtGui.QKeySequence.Copy):
819 826 self.copy()
820 827 intercepted = True
821 828
822 829 elif event.matches(QtGui.QKeySequence.Cut):
823 830 self.cut()
824 831 intercepted = True
825 832
826 833 elif event.matches(QtGui.QKeySequence.Paste):
827 834 self.paste()
828 835 intercepted = True
829 836
830 837 #------ Special modifier logic -----------------------------------------
831 838
832 839 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
833 840 intercepted = True
834 841
835 842 # Special handling when tab completing in text mode.
836 843 self._cancel_text_completion()
837 844
838 845 if self._in_buffer(position):
839 846 if self._reading:
840 847 self._append_plain_text('\n')
841 848 self._reading = False
842 849 if self._reading_callback:
843 850 self._reading_callback()
844 851
845 852 # If the input buffer is a single line or there is only
846 853 # whitespace after the cursor, execute. Otherwise, split the
847 854 # line with a continuation prompt.
848 855 elif not self._executing:
849 856 cursor.movePosition(QtGui.QTextCursor.End,
850 857 QtGui.QTextCursor.KeepAnchor)
851 858 at_end = cursor.selectedText().trimmed().isEmpty()
852 859 single_line = (self._get_end_cursor().blockNumber() ==
853 860 self._get_prompt_cursor().blockNumber())
854 861 if (at_end or shift_down or single_line) and not ctrl_down:
855 862 self.execute(interactive = not shift_down)
856 863 else:
857 864 # Do this inside an edit block for clean undo/redo.
858 865 cursor.beginEditBlock()
859 866 cursor.setPosition(position)
860 867 cursor.insertText('\n')
861 868 self._insert_continuation_prompt(cursor)
862 869 cursor.endEditBlock()
863 870
864 871 # Ensure that the whole input buffer is visible.
865 872 # FIXME: This will not be usable if the input buffer is
866 873 # taller than the console widget.
867 874 self._control.moveCursor(QtGui.QTextCursor.End)
868 875 self._control.setTextCursor(cursor)
869 876
870 877 #------ Control/Cmd modifier -------------------------------------------
871 878
872 879 elif ctrl_down:
873 880 if key == QtCore.Qt.Key_G:
874 881 self._keyboard_quit()
875 882 intercepted = True
876 883
877 884 elif key == QtCore.Qt.Key_K:
878 885 if self._in_buffer(position):
879 886 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
880 887 QtGui.QTextCursor.KeepAnchor)
881 888 if not cursor.hasSelection():
882 889 # Line deletion (remove continuation prompt)
883 890 cursor.movePosition(QtGui.QTextCursor.NextBlock,
884 891 QtGui.QTextCursor.KeepAnchor)
885 892 cursor.movePosition(QtGui.QTextCursor.Right,
886 893 QtGui.QTextCursor.KeepAnchor,
887 894 len(self._continuation_prompt))
888 895 cursor.removeSelectedText()
889 896 intercepted = True
890 897
891 898 elif key == QtCore.Qt.Key_L:
892 899 self.prompt_to_top()
893 900 intercepted = True
894 901
895 902 elif key == QtCore.Qt.Key_O:
896 903 if self._page_control and self._page_control.isVisible():
897 904 self._page_control.setFocus()
898 intercept = True
905 intercepted = True
899 906
900 907 elif key == QtCore.Qt.Key_Y:
901 908 self.paste()
902 909 intercepted = True
903 910
904 911 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
905 912 intercepted = True
906 913
914 elif key == QtCore.Qt.Key_Plus:
915 self.change_font_size(1)
916 intercepted = True
917
918 elif key == QtCore.Qt.Key_Minus:
919 self.change_font_size(-1)
920 intercepted = True
921
907 922 #------ Alt modifier ---------------------------------------------------
908 923
909 924 elif alt_down:
910 925 if key == QtCore.Qt.Key_B:
911 926 self._set_cursor(self._get_word_start_cursor(position))
912 927 intercepted = True
913 928
914 929 elif key == QtCore.Qt.Key_F:
915 930 self._set_cursor(self._get_word_end_cursor(position))
916 931 intercepted = True
917 932
918 933 elif key == QtCore.Qt.Key_Backspace:
919 934 cursor = self._get_word_start_cursor(position)
920 935 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
921 936 cursor.removeSelectedText()
922 937 intercepted = True
923 938
924 939 elif key == QtCore.Qt.Key_D:
925 940 cursor = self._get_word_end_cursor(position)
926 941 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
927 942 cursor.removeSelectedText()
928 943 intercepted = True
929 944
930 945 elif key == QtCore.Qt.Key_Delete:
931 946 intercepted = True
932 947
933 948 elif key == QtCore.Qt.Key_Greater:
934 949 self._control.moveCursor(QtGui.QTextCursor.End)
935 950 intercepted = True
936 951
937 952 elif key == QtCore.Qt.Key_Less:
938 953 self._control.setTextCursor(self._get_prompt_cursor())
939 954 intercepted = True
940 955
941 956 #------ No modifiers ---------------------------------------------------
942 957
943 958 else:
944 959 if key == QtCore.Qt.Key_Escape:
945 960 self._keyboard_quit()
946 961 intercepted = True
947 962
948 963 elif key == QtCore.Qt.Key_Up:
949 964 if self._reading or not self._up_pressed():
950 965 intercepted = True
951 966 else:
952 967 prompt_line = self._get_prompt_cursor().blockNumber()
953 968 intercepted = cursor.blockNumber() <= prompt_line
954 969
955 970 elif key == QtCore.Qt.Key_Down:
956 971 if self._reading or not self._down_pressed():
957 972 intercepted = True
958 973 else:
959 974 end_line = self._get_end_cursor().blockNumber()
960 975 intercepted = cursor.blockNumber() == end_line
961 976
962 977 elif key == QtCore.Qt.Key_Tab:
963 978 if not self._reading:
964 979 intercepted = not self._tab_pressed()
965 980
966 981 elif key == QtCore.Qt.Key_Left:
967 982
968 983 # Move to the previous line
969 984 line, col = cursor.blockNumber(), cursor.columnNumber()
970 985 if line > self._get_prompt_cursor().blockNumber() and \
971 986 col == len(self._continuation_prompt):
972 987 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock)
973 988 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock)
974 989 intercepted = True
975 990
976 991 # Regular left movement
977 992 else:
978 993 intercepted = not self._in_buffer(position - 1)
979 994
980 995 elif key == QtCore.Qt.Key_Right:
981 996 original_block_number = cursor.blockNumber()
982 997 cursor.movePosition(QtGui.QTextCursor.Right)
983 998 if cursor.blockNumber() != original_block_number:
984 999 cursor.movePosition(QtGui.QTextCursor.Right,
985 1000 n=len(self._continuation_prompt))
986 1001 self._set_cursor(cursor)
987 1002 intercepted = True
988 1003
989 1004 elif key == QtCore.Qt.Key_Home:
990 1005 start_line = cursor.blockNumber()
991 1006 if start_line == self._get_prompt_cursor().blockNumber():
992 1007 start_pos = self._prompt_pos
993 1008 else:
994 1009 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
995 1010 QtGui.QTextCursor.KeepAnchor)
996 1011 start_pos = cursor.position()
997 1012 start_pos += len(self._continuation_prompt)
998 1013 cursor.setPosition(position)
999 1014 if shift_down and self._in_buffer(position):
1000 1015 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1001 1016 else:
1002 1017 cursor.setPosition(start_pos)
1003 1018 self._set_cursor(cursor)
1004 1019 intercepted = True
1005 1020
1006 1021 elif key == QtCore.Qt.Key_Backspace:
1007 1022
1008 1023 # Line deletion (remove continuation prompt)
1009 1024 line, col = cursor.blockNumber(), cursor.columnNumber()
1010 1025 if not self._reading and \
1011 1026 col == len(self._continuation_prompt) and \
1012 1027 line > self._get_prompt_cursor().blockNumber():
1013 1028 cursor.beginEditBlock()
1014 1029 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1015 1030 QtGui.QTextCursor.KeepAnchor)
1016 1031 cursor.removeSelectedText()
1017 1032 cursor.deletePreviousChar()
1018 1033 cursor.endEditBlock()
1019 1034 intercepted = True
1020 1035
1021 1036 # Regular backwards deletion
1022 1037 else:
1023 1038 anchor = cursor.anchor()
1024 1039 if anchor == position:
1025 1040 intercepted = not self._in_buffer(position - 1)
1026 1041 else:
1027 1042 intercepted = not self._in_buffer(min(anchor, position))
1028 1043
1029 1044 elif key == QtCore.Qt.Key_Delete:
1030 1045
1031 1046 # Line deletion (remove continuation prompt)
1032 1047 if not self._reading and self._in_buffer(position) and \
1033 1048 cursor.atBlockEnd() and not cursor.hasSelection():
1034 1049 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1035 1050 QtGui.QTextCursor.KeepAnchor)
1036 1051 cursor.movePosition(QtGui.QTextCursor.Right,
1037 1052 QtGui.QTextCursor.KeepAnchor,
1038 1053 len(self._continuation_prompt))
1039 1054 cursor.removeSelectedText()
1040 1055 intercepted = True
1041 1056
1042 1057 # Regular forwards deletion:
1043 1058 else:
1044 1059 anchor = cursor.anchor()
1045 1060 intercepted = (not self._in_buffer(anchor) or
1046 1061 not self._in_buffer(position))
1047 1062
1048 1063 # Don't move the cursor if control is down to allow copy-paste using
1049 1064 # the keyboard in any part of the buffer.
1050 1065 if not ctrl_down:
1051 1066 self._keep_cursor_in_buffer()
1052 1067
1053 1068 return intercepted
1054 1069
1055 1070 def _event_filter_page_keypress(self, event):
1056 1071 """ Filter key events for the paging widget to create console-like
1057 1072 interface.
1058 1073 """
1059 1074 key = event.key()
1060 1075 ctrl_down = self._control_key_down(event.modifiers())
1061 1076 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1062 1077
1063 1078 if ctrl_down:
1064 1079 if key == QtCore.Qt.Key_O:
1065 1080 self._control.setFocus()
1066 1081 intercept = True
1067 1082
1068 1083 elif alt_down:
1069 1084 if key == QtCore.Qt.Key_Greater:
1070 1085 self._page_control.moveCursor(QtGui.QTextCursor.End)
1071 1086 intercepted = True
1072 1087
1073 1088 elif key == QtCore.Qt.Key_Less:
1074 1089 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1075 1090 intercepted = True
1076 1091
1077 1092 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1078 1093 if self._splitter:
1079 1094 self._page_control.hide()
1080 1095 else:
1081 1096 self.layout().setCurrentWidget(self._control)
1082 1097 return True
1083 1098
1084 1099 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1085 1100 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1086 1101 QtCore.Qt.Key_PageDown,
1087 1102 QtCore.Qt.NoModifier)
1088 1103 QtGui.qApp.sendEvent(self._page_control, new_event)
1089 1104 return True
1090 1105
1091 1106 elif key == QtCore.Qt.Key_Backspace:
1092 1107 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1093 1108 QtCore.Qt.Key_PageUp,
1094 1109 QtCore.Qt.NoModifier)
1095 1110 QtGui.qApp.sendEvent(self._page_control, new_event)
1096 1111 return True
1097 1112
1098 1113 return False
1099 1114
1100 1115 def _format_as_columns(self, items, separator=' '):
1101 1116 """ Transform a list of strings into a single string with columns.
1102 1117
1103 1118 Parameters
1104 1119 ----------
1105 1120 items : sequence of strings
1106 1121 The strings to process.
1107 1122
1108 1123 separator : str, optional [default is two spaces]
1109 1124 The string that separates columns.
1110 1125
1111 1126 Returns
1112 1127 -------
1113 1128 The formatted string.
1114 1129 """
1115 1130 # Note: this code is adapted from columnize 0.3.2.
1116 1131 # See http://code.google.com/p/pycolumnize/
1117 1132
1118 1133 # Calculate the number of characters available.
1119 1134 width = self._control.viewport().width()
1120 1135 char_width = QtGui.QFontMetrics(self.font).width(' ')
1121 1136 displaywidth = max(10, (width / char_width) - 1)
1122 1137
1123 1138 # Some degenerate cases.
1124 1139 size = len(items)
1125 1140 if size == 0:
1126 1141 return '\n'
1127 1142 elif size == 1:
1128 1143 return '%s\n' % items[0]
1129 1144
1130 1145 # Try every row count from 1 upwards
1131 1146 array_index = lambda nrows, row, col: nrows*col + row
1132 1147 for nrows in range(1, size):
1133 1148 ncols = (size + nrows - 1) // nrows
1134 1149 colwidths = []
1135 1150 totwidth = -len(separator)
1136 1151 for col in range(ncols):
1137 1152 # Get max column width for this column
1138 1153 colwidth = 0
1139 1154 for row in range(nrows):
1140 1155 i = array_index(nrows, row, col)
1141 1156 if i >= size: break
1142 1157 x = items[i]
1143 1158 colwidth = max(colwidth, len(x))
1144 1159 colwidths.append(colwidth)
1145 1160 totwidth += colwidth + len(separator)
1146 1161 if totwidth > displaywidth:
1147 1162 break
1148 1163 if totwidth <= displaywidth:
1149 1164 break
1150 1165
1151 1166 # The smallest number of rows computed and the max widths for each
1152 1167 # column has been obtained. Now we just have to format each of the rows.
1153 1168 string = ''
1154 1169 for row in range(nrows):
1155 1170 texts = []
1156 1171 for col in range(ncols):
1157 1172 i = row + nrows*col
1158 1173 if i >= size:
1159 1174 texts.append('')
1160 1175 else:
1161 1176 texts.append(items[i])
1162 1177 while texts and not texts[-1]:
1163 1178 del texts[-1]
1164 1179 for col in range(len(texts)):
1165 1180 texts[col] = texts[col].ljust(colwidths[col])
1166 1181 string += '%s\n' % separator.join(texts)
1167 1182 return string
1168 1183
1169 1184 def _get_block_plain_text(self, block):
1170 1185 """ Given a QTextBlock, return its unformatted text.
1171 1186 """
1172 1187 cursor = QtGui.QTextCursor(block)
1173 1188 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1174 1189 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1175 1190 QtGui.QTextCursor.KeepAnchor)
1176 1191 return unicode(cursor.selection().toPlainText())
1177 1192
1178 1193 def _get_cursor(self):
1179 1194 """ Convenience method that returns a cursor for the current position.
1180 1195 """
1181 1196 return self._control.textCursor()
1182 1197
1183 1198 def _get_end_cursor(self):
1184 1199 """ Convenience method that returns a cursor for the last character.
1185 1200 """
1186 1201 cursor = self._control.textCursor()
1187 1202 cursor.movePosition(QtGui.QTextCursor.End)
1188 1203 return cursor
1189 1204
1190 1205 def _get_input_buffer_cursor_column(self):
1191 1206 """ Returns the column of the cursor in the input buffer, excluding the
1192 1207 contribution by the prompt, or -1 if there is no such column.
1193 1208 """
1194 1209 prompt = self._get_input_buffer_cursor_prompt()
1195 1210 if prompt is None:
1196 1211 return -1
1197 1212 else:
1198 1213 cursor = self._control.textCursor()
1199 1214 return cursor.columnNumber() - len(prompt)
1200 1215
1201 1216 def _get_input_buffer_cursor_line(self):
1202 1217 """ Returns the text of the line of the input buffer that contains the
1203 1218 cursor, or None if there is no such line.
1204 1219 """
1205 1220 prompt = self._get_input_buffer_cursor_prompt()
1206 1221 if prompt is None:
1207 1222 return None
1208 1223 else:
1209 1224 cursor = self._control.textCursor()
1210 1225 text = self._get_block_plain_text(cursor.block())
1211 1226 return text[len(prompt):]
1212 1227
1213 1228 def _get_input_buffer_cursor_prompt(self):
1214 1229 """ Returns the (plain text) prompt for line of the input buffer that
1215 1230 contains the cursor, or None if there is no such line.
1216 1231 """
1217 1232 if self._executing:
1218 1233 return None
1219 1234 cursor = self._control.textCursor()
1220 1235 if cursor.position() >= self._prompt_pos:
1221 1236 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1222 1237 return self._prompt
1223 1238 else:
1224 1239 return self._continuation_prompt
1225 1240 else:
1226 1241 return None
1227 1242
1228 1243 def _get_prompt_cursor(self):
1229 1244 """ Convenience method that returns a cursor for the prompt position.
1230 1245 """
1231 1246 cursor = self._control.textCursor()
1232 1247 cursor.setPosition(self._prompt_pos)
1233 1248 return cursor
1234 1249
1235 1250 def _get_selection_cursor(self, start, end):
1236 1251 """ Convenience method that returns a cursor with text selected between
1237 1252 the positions 'start' and 'end'.
1238 1253 """
1239 1254 cursor = self._control.textCursor()
1240 1255 cursor.setPosition(start)
1241 1256 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1242 1257 return cursor
1243 1258
1244 1259 def _get_word_start_cursor(self, position):
1245 1260 """ Find the start of the word to the left the given position. If a
1246 1261 sequence of non-word characters precedes the first word, skip over
1247 1262 them. (This emulates the behavior of bash, emacs, etc.)
1248 1263 """
1249 1264 document = self._control.document()
1250 1265 position -= 1
1251 1266 while position >= self._prompt_pos and \
1252 1267 not document.characterAt(position).isLetterOrNumber():
1253 1268 position -= 1
1254 1269 while position >= self._prompt_pos and \
1255 1270 document.characterAt(position).isLetterOrNumber():
1256 1271 position -= 1
1257 1272 cursor = self._control.textCursor()
1258 1273 cursor.setPosition(position + 1)
1259 1274 return cursor
1260 1275
1261 1276 def _get_word_end_cursor(self, position):
1262 1277 """ Find the end of the word to the right the given position. If a
1263 1278 sequence of non-word characters precedes the first word, skip over
1264 1279 them. (This emulates the behavior of bash, emacs, etc.)
1265 1280 """
1266 1281 document = self._control.document()
1267 1282 end = self._get_end_cursor().position()
1268 1283 while position < end and \
1269 1284 not document.characterAt(position).isLetterOrNumber():
1270 1285 position += 1
1271 1286 while position < end and \
1272 1287 document.characterAt(position).isLetterOrNumber():
1273 1288 position += 1
1274 1289 cursor = self._control.textCursor()
1275 1290 cursor.setPosition(position)
1276 1291 return cursor
1277 1292
1278 1293 def _insert_continuation_prompt(self, cursor):
1279 1294 """ Inserts new continuation prompt using the specified cursor.
1280 1295 """
1281 1296 if self._continuation_prompt_html is None:
1282 1297 self._insert_plain_text(cursor, self._continuation_prompt)
1283 1298 else:
1284 1299 self._continuation_prompt = self._insert_html_fetching_plain_text(
1285 1300 cursor, self._continuation_prompt_html)
1286 1301
1287 1302 def _insert_html(self, cursor, html):
1288 1303 """ Inserts HTML using the specified cursor in such a way that future
1289 1304 formatting is unaffected.
1290 1305 """
1291 1306 cursor.beginEditBlock()
1292 1307 cursor.insertHtml(html)
1293 1308
1294 1309 # After inserting HTML, the text document "remembers" it's in "html
1295 1310 # mode", which means that subsequent calls adding plain text will result
1296 1311 # in unwanted formatting, lost tab characters, etc. The following code
1297 1312 # hacks around this behavior, which I consider to be a bug in Qt, by
1298 1313 # (crudely) resetting the document's style state.
1299 1314 cursor.movePosition(QtGui.QTextCursor.Left,
1300 1315 QtGui.QTextCursor.KeepAnchor)
1301 1316 if cursor.selection().toPlainText() == ' ':
1302 1317 cursor.removeSelectedText()
1303 1318 else:
1304 1319 cursor.movePosition(QtGui.QTextCursor.Right)
1305 1320 cursor.insertText(' ', QtGui.QTextCharFormat())
1306 1321 cursor.endEditBlock()
1307 1322
1308 1323 def _insert_html_fetching_plain_text(self, cursor, html):
1309 1324 """ Inserts HTML using the specified cursor, then returns its plain text
1310 1325 version.
1311 1326 """
1312 1327 cursor.beginEditBlock()
1313 1328 cursor.removeSelectedText()
1314 1329
1315 1330 start = cursor.position()
1316 1331 self._insert_html(cursor, html)
1317 1332 end = cursor.position()
1318 1333 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1319 1334 text = unicode(cursor.selection().toPlainText())
1320 1335
1321 1336 cursor.setPosition(end)
1322 1337 cursor.endEditBlock()
1323 1338 return text
1324 1339
1325 1340 def _insert_plain_text(self, cursor, text):
1326 1341 """ Inserts plain text using the specified cursor, processing ANSI codes
1327 1342 if enabled.
1328 1343 """
1329 1344 cursor.beginEditBlock()
1330 1345 if self.ansi_codes:
1331 1346 for substring in self._ansi_processor.split_string(text):
1332 1347 for act in self._ansi_processor.actions:
1333 1348
1334 1349 # Unlike real terminal emulators, we don't distinguish
1335 1350 # between the screen and the scrollback buffer. A screen
1336 1351 # erase request clears everything.
1337 1352 if act.action == 'erase' and act.area == 'screen':
1338 1353 cursor.select(QtGui.QTextCursor.Document)
1339 1354 cursor.removeSelectedText()
1340 1355
1341 1356 # Simulate a form feed by scrolling just past the last line.
1342 1357 elif act.action == 'scroll' and act.unit == 'page':
1343 1358 cursor.insertText('\n')
1344 1359 cursor.endEditBlock()
1345 1360 self._set_top_cursor(cursor)
1346 1361 cursor.joinPreviousEditBlock()
1347 1362 cursor.deletePreviousChar()
1348 1363
1349 1364 format = self._ansi_processor.get_format()
1350 1365 cursor.insertText(substring, format)
1351 1366 else:
1352 1367 cursor.insertText(text)
1353 1368 cursor.endEditBlock()
1354 1369
1355 1370 def _insert_plain_text_into_buffer(self, cursor, text):
1356 1371 """ Inserts text into the input buffer using the specified cursor (which
1357 1372 must be in the input buffer), ensuring that continuation prompts are
1358 1373 inserted as necessary.
1359 1374 """
1360 1375 lines = unicode(text).splitlines(True)
1361 1376 if lines:
1362 1377 cursor.beginEditBlock()
1363 1378 cursor.insertText(lines[0])
1364 1379 for line in lines[1:]:
1365 1380 if self._continuation_prompt_html is None:
1366 1381 cursor.insertText(self._continuation_prompt)
1367 1382 else:
1368 1383 self._continuation_prompt = \
1369 1384 self._insert_html_fetching_plain_text(
1370 1385 cursor, self._continuation_prompt_html)
1371 1386 cursor.insertText(line)
1372 1387 cursor.endEditBlock()
1373 1388
1374 1389 def _in_buffer(self, position=None):
1375 1390 """ Returns whether the current cursor (or, if specified, a position) is
1376 1391 inside the editing region.
1377 1392 """
1378 1393 cursor = self._control.textCursor()
1379 1394 if position is None:
1380 1395 position = cursor.position()
1381 1396 else:
1382 1397 cursor.setPosition(position)
1383 1398 line = cursor.blockNumber()
1384 1399 prompt_line = self._get_prompt_cursor().blockNumber()
1385 1400 if line == prompt_line:
1386 1401 return position >= self._prompt_pos
1387 1402 elif line > prompt_line:
1388 1403 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1389 1404 prompt_pos = cursor.position() + len(self._continuation_prompt)
1390 1405 return position >= prompt_pos
1391 1406 return False
1392 1407
1393 1408 def _keep_cursor_in_buffer(self):
1394 1409 """ Ensures that the cursor is inside the editing region. Returns
1395 1410 whether the cursor was moved.
1396 1411 """
1397 1412 moved = not self._in_buffer()
1398 1413 if moved:
1399 1414 cursor = self._control.textCursor()
1400 1415 cursor.movePosition(QtGui.QTextCursor.End)
1401 1416 self._control.setTextCursor(cursor)
1402 1417 return moved
1403 1418
1404 1419 def _keyboard_quit(self):
1405 1420 """ Cancels the current editing task ala Ctrl-G in Emacs.
1406 1421 """
1407 1422 if self._text_completing_pos:
1408 1423 self._cancel_text_completion()
1409 1424 else:
1410 1425 self.input_buffer = ''
1411 1426
1412 1427 def _page(self, text, html=False):
1413 1428 """ Displays text using the pager if it exceeds the height of the
1414 1429 viewport.
1415 1430
1416 1431 Parameters:
1417 1432 -----------
1418 1433 html : bool, optional (default False)
1419 1434 If set, the text will be interpreted as HTML instead of plain text.
1420 1435 """
1421 1436 line_height = QtGui.QFontMetrics(self.font).height()
1422 1437 minlines = self._control.viewport().height() / line_height
1423 1438 if self.paging != 'none' and \
1424 1439 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1425 1440 if self.paging == 'custom':
1426 1441 self.custom_page_requested.emit(text)
1427 1442 else:
1428 1443 self._page_control.clear()
1429 1444 cursor = self._page_control.textCursor()
1430 1445 if html:
1431 1446 self._insert_html(cursor, text)
1432 1447 else:
1433 1448 self._insert_plain_text(cursor, text)
1434 1449 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1435 1450
1436 1451 self._page_control.viewport().resize(self._control.size())
1437 1452 if self._splitter:
1438 1453 self._page_control.show()
1439 1454 self._page_control.setFocus()
1440 1455 else:
1441 1456 self.layout().setCurrentWidget(self._page_control)
1442 1457 elif html:
1443 1458 self._append_plain_html(text)
1444 1459 else:
1445 1460 self._append_plain_text(text)
1446 1461
1447 1462 def _prompt_finished(self):
1448 1463 """ Called immediately after a prompt is finished, i.e. when some input
1449 1464 will be processed and a new prompt displayed.
1450 1465 """
1451 1466 # Flush all state from the input splitter so the next round of
1452 1467 # reading input starts with a clean buffer.
1453 1468 self._input_splitter.reset()
1454 1469
1455 1470 self._control.setReadOnly(True)
1456 1471 self._prompt_finished_hook()
1457 1472
1458 1473 def _prompt_started(self):
1459 1474 """ Called immediately after a new prompt is displayed.
1460 1475 """
1461 1476 # Temporarily disable the maximum block count to permit undo/redo and
1462 1477 # to ensure that the prompt position does not change due to truncation.
1463 1478 self._control.document().setMaximumBlockCount(0)
1464 1479 self._control.setUndoRedoEnabled(True)
1465 1480
1466 1481 self._control.setReadOnly(False)
1467 1482 self._control.moveCursor(QtGui.QTextCursor.End)
1468 1483 self._executing = False
1469 1484 self._prompt_started_hook()
1470 1485
1471 1486 def _readline(self, prompt='', callback=None):
1472 1487 """ Reads one line of input from the user.
1473 1488
1474 1489 Parameters
1475 1490 ----------
1476 1491 prompt : str, optional
1477 1492 The prompt to print before reading the line.
1478 1493
1479 1494 callback : callable, optional
1480 1495 A callback to execute with the read line. If not specified, input is
1481 1496 read *synchronously* and this method does not return until it has
1482 1497 been read.
1483 1498
1484 1499 Returns
1485 1500 -------
1486 1501 If a callback is specified, returns nothing. Otherwise, returns the
1487 1502 input string with the trailing newline stripped.
1488 1503 """
1489 1504 if self._reading:
1490 1505 raise RuntimeError('Cannot read a line. Widget is already reading.')
1491 1506
1492 1507 if not callback and not self.isVisible():
1493 1508 # If the user cannot see the widget, this function cannot return.
1494 1509 raise RuntimeError('Cannot synchronously read a line if the widget '
1495 1510 'is not visible!')
1496 1511
1497 1512 self._reading = True
1498 1513 self._show_prompt(prompt, newline=False)
1499 1514
1500 1515 if callback is None:
1501 1516 self._reading_callback = None
1502 1517 while self._reading:
1503 1518 QtCore.QCoreApplication.processEvents()
1504 1519 return self.input_buffer.rstrip('\n')
1505 1520
1506 1521 else:
1507 1522 self._reading_callback = lambda: \
1508 1523 callback(self.input_buffer.rstrip('\n'))
1509 1524
1510 1525 def _set_continuation_prompt(self, prompt, html=False):
1511 1526 """ Sets the continuation prompt.
1512 1527
1513 1528 Parameters
1514 1529 ----------
1515 1530 prompt : str
1516 1531 The prompt to show when more input is needed.
1517 1532
1518 1533 html : bool, optional (default False)
1519 1534 If set, the prompt will be inserted as formatted HTML. Otherwise,
1520 1535 the prompt will be treated as plain text, though ANSI color codes
1521 1536 will be handled.
1522 1537 """
1523 1538 if html:
1524 1539 self._continuation_prompt_html = prompt
1525 1540 else:
1526 1541 self._continuation_prompt = prompt
1527 1542 self._continuation_prompt_html = None
1528 1543
1529 1544 def _set_cursor(self, cursor):
1530 1545 """ Convenience method to set the current cursor.
1531 1546 """
1532 1547 self._control.setTextCursor(cursor)
1533 1548
1534 1549 def _set_top_cursor(self, cursor):
1535 1550 """ Scrolls the viewport so that the specified cursor is at the top.
1536 1551 """
1537 1552 scrollbar = self._control.verticalScrollBar()
1538 1553 scrollbar.setValue(scrollbar.maximum())
1539 1554 original_cursor = self._control.textCursor()
1540 1555 self._control.setTextCursor(cursor)
1541 1556 self._control.ensureCursorVisible()
1542 1557 self._control.setTextCursor(original_cursor)
1543 1558
1544 1559 def _show_prompt(self, prompt=None, html=False, newline=True):
1545 1560 """ Writes a new prompt at the end of the buffer.
1546 1561
1547 1562 Parameters
1548 1563 ----------
1549 1564 prompt : str, optional
1550 1565 The prompt to show. If not specified, the previous prompt is used.
1551 1566
1552 1567 html : bool, optional (default False)
1553 1568 Only relevant when a prompt is specified. If set, the prompt will
1554 1569 be inserted as formatted HTML. Otherwise, the prompt will be treated
1555 1570 as plain text, though ANSI color codes will be handled.
1556 1571
1557 1572 newline : bool, optional (default True)
1558 1573 If set, a new line will be written before showing the prompt if
1559 1574 there is not already a newline at the end of the buffer.
1560 1575 """
1561 1576 # Insert a preliminary newline, if necessary.
1562 1577 if newline:
1563 1578 cursor = self._get_end_cursor()
1564 1579 if cursor.position() > 0:
1565 1580 cursor.movePosition(QtGui.QTextCursor.Left,
1566 1581 QtGui.QTextCursor.KeepAnchor)
1567 1582 if unicode(cursor.selection().toPlainText()) != '\n':
1568 1583 self._append_plain_text('\n')
1569 1584
1570 1585 # Write the prompt.
1571 1586 self._append_plain_text(self._prompt_sep)
1572 1587 if prompt is None:
1573 1588 if self._prompt_html is None:
1574 1589 self._append_plain_text(self._prompt)
1575 1590 else:
1576 1591 self._append_html(self._prompt_html)
1577 1592 else:
1578 1593 if html:
1579 1594 self._prompt = self._append_html_fetching_plain_text(prompt)
1580 1595 self._prompt_html = prompt
1581 1596 else:
1582 1597 self._append_plain_text(prompt)
1583 1598 self._prompt = prompt
1584 1599 self._prompt_html = None
1585 1600
1586 1601 self._prompt_pos = self._get_end_cursor().position()
1587 1602 self._prompt_started()
1588 1603
1589 1604 #------ Signal handlers ----------------------------------------------------
1590 1605
1591 1606 def _adjust_scrollbars(self):
1592 1607 """ Expands the vertical scrollbar beyond the range set by Qt.
1593 1608 """
1594 1609 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1595 1610 # and qtextedit.cpp.
1596 1611 document = self._control.document()
1597 1612 scrollbar = self._control.verticalScrollBar()
1598 1613 viewport_height = self._control.viewport().height()
1599 1614 if isinstance(self._control, QtGui.QPlainTextEdit):
1600 1615 maximum = max(0, document.lineCount() - 1)
1601 1616 step = viewport_height / self._control.fontMetrics().lineSpacing()
1602 1617 else:
1603 1618 # QTextEdit does not do line-based layout and blocks will not in
1604 1619 # general have the same height. Therefore it does not make sense to
1605 1620 # attempt to scroll in line height increments.
1606 1621 maximum = document.size().height()
1607 1622 step = viewport_height
1608 1623 diff = maximum - scrollbar.maximum()
1609 1624 scrollbar.setRange(0, maximum)
1610 1625 scrollbar.setPageStep(step)
1611 1626 # Compensate for undesirable scrolling that occurs automatically due to
1612 1627 # maximumBlockCount() text truncation.
1613 1628 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1614 1629 scrollbar.setValue(scrollbar.value() + diff)
1615 1630
1616 1631 def _cursor_position_changed(self):
1617 1632 """ Clears the temporary buffer based on the cursor position.
1618 1633 """
1619 1634 if self._text_completing_pos:
1620 1635 document = self._control.document()
1621 1636 if self._text_completing_pos < document.characterCount():
1622 1637 cursor = self._control.textCursor()
1623 1638 pos = cursor.position()
1624 1639 text_cursor = self._control.textCursor()
1625 1640 text_cursor.setPosition(self._text_completing_pos)
1626 1641 if pos < self._text_completing_pos or \
1627 1642 cursor.blockNumber() > text_cursor.blockNumber():
1628 1643 self._clear_temporary_buffer()
1629 1644 self._text_completing_pos = 0
1630 1645 else:
1631 1646 self._clear_temporary_buffer()
1632 1647 self._text_completing_pos = 0
1633 1648
1634 1649 def _custom_context_menu_requested(self, pos):
1635 1650 """ Shows a context menu at the given QPoint (in widget coordinates).
1636 1651 """
1637 1652 menu = self._context_menu_make(pos)
1638 1653 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now