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