##// END OF EJS Templates
Use 'is' to compare with None...
Boris Egorov -
Show More
@@ -1,533 +1,533 b''
1 """Various display related classes.
1 """Various display related classes.
2
2
3 Authors : MinRK, gregcaporaso, dannystaple
3 Authors : MinRK, gregcaporaso, dannystaple
4 """
4 """
5 from os.path import exists, isfile, splitext, abspath, join, isdir
5 from os.path import exists, isfile, splitext, abspath, join, isdir
6 from os import walk, sep
6 from os import walk, sep
7
7
8 from IPython.core.display import DisplayObject
8 from IPython.core.display import DisplayObject
9
9
10 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
10 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
11 'FileLink', 'FileLinks']
11 'FileLink', 'FileLinks']
12
12
13
13
14 class Audio(DisplayObject):
14 class Audio(DisplayObject):
15 """Create an audio object.
15 """Create an audio object.
16
16
17 When this object is returned by an input cell or passed to the
17 When this object is returned by an input cell or passed to the
18 display function, it will result in Audio controls being displayed
18 display function, it will result in Audio controls being displayed
19 in the frontend (only works in the notebook).
19 in the frontend (only works in the notebook).
20
20
21 Parameters
21 Parameters
22 ----------
22 ----------
23 data : numpy array, list, unicode, str or bytes
23 data : numpy array, list, unicode, str or bytes
24 Can be one of
24 Can be one of
25
25
26 * Numpy 1d array containing the desired waveform (mono)
26 * Numpy 1d array containing the desired waveform (mono)
27 * Numpy 2d array containing waveforms for each channel.
27 * Numpy 2d array containing waveforms for each channel.
28 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
28 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
29 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
29 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
30 * List of float or integer representing the waveform (mono)
30 * List of float or integer representing the waveform (mono)
31 * String containing the filename
31 * String containing the filename
32 * Bytestring containing raw PCM data or
32 * Bytestring containing raw PCM data or
33 * URL pointing to a file on the web.
33 * URL pointing to a file on the web.
34
34
35 If the array option is used the waveform will be normalized.
35 If the array option is used the waveform will be normalized.
36
36
37 If a filename or url is used the format support will be browser
37 If a filename or url is used the format support will be browser
38 dependent.
38 dependent.
39 url : unicode
39 url : unicode
40 A URL to download the data from.
40 A URL to download the data from.
41 filename : unicode
41 filename : unicode
42 Path to a local file to load the data from.
42 Path to a local file to load the data from.
43 embed : boolean
43 embed : boolean
44 Should the image data be embedded using a data URI (True) or should
44 Should the image data be embedded using a data URI (True) or should
45 the original source be referenced. Set this to True if you want the
45 the original source be referenced. Set this to True if you want the
46 audio to playable later with no internet connection in the notebook.
46 audio to playable later with no internet connection in the notebook.
47
47
48 Default is `True`, unless the keyword argument `url` is set, then
48 Default is `True`, unless the keyword argument `url` is set, then
49 default value is `False`.
49 default value is `False`.
50 rate : integer
50 rate : integer
51 The sampling rate of the raw data.
51 The sampling rate of the raw data.
52 Only required when data parameter is being used as an array
52 Only required when data parameter is being used as an array
53 autoplay : bool
53 autoplay : bool
54 Set to True if the audio should immediately start playing.
54 Set to True if the audio should immediately start playing.
55 Default is `False`.
55 Default is `False`.
56
56
57 Examples
57 Examples
58 --------
58 --------
59 ::
59 ::
60
60
61 # Generate a sound
61 # Generate a sound
62 import numpy as np
62 import numpy as np
63 framerate = 44100
63 framerate = 44100
64 t = np.linspace(0,5,framerate*5)
64 t = np.linspace(0,5,framerate*5)
65 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t))
65 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t))
66 Audio(data,rate=framerate)
66 Audio(data,rate=framerate)
67
67
68 # Can also do stereo or more channels
68 # Can also do stereo or more channels
69 dataleft = np.sin(2*np.pi*220*t)
69 dataleft = np.sin(2*np.pi*220*t)
70 dataright = np.sin(2*np.pi*224*t)
70 dataright = np.sin(2*np.pi*224*t)
71 Audio([dataleft, dataright],rate=framerate)
71 Audio([dataleft, dataright],rate=framerate)
72
72
73 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
73 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
74 Audio(url="http://www.w3schools.com/html/horse.ogg")
74 Audio(url="http://www.w3schools.com/html/horse.ogg")
75
75
76 Audio('/path/to/sound.wav') # From file
76 Audio('/path/to/sound.wav') # From file
77 Audio(filename='/path/to/sound.ogg')
77 Audio(filename='/path/to/sound.ogg')
78
78
79 Audio(b'RAW_WAV_DATA..) # From bytes
79 Audio(b'RAW_WAV_DATA..) # From bytes
80 Audio(data=b'RAW_WAV_DATA..)
80 Audio(data=b'RAW_WAV_DATA..)
81
81
82 """
82 """
83 _read_flags = 'rb'
83 _read_flags = 'rb'
84
84
85 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False):
85 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False):
86 if filename is None and url is None and data is None:
86 if filename is None and url is None and data is None:
87 raise ValueError("No image data found. Expecting filename, url, or data.")
87 raise ValueError("No image data found. Expecting filename, url, or data.")
88 if embed is False and url is None:
88 if embed is False and url is None:
89 raise ValueError("No url found. Expecting url when embed=False")
89 raise ValueError("No url found. Expecting url when embed=False")
90
90
91 if url is not None and embed is not True:
91 if url is not None and embed is not True:
92 self.embed = False
92 self.embed = False
93 else:
93 else:
94 self.embed = True
94 self.embed = True
95 self.autoplay = autoplay
95 self.autoplay = autoplay
96 super(Audio, self).__init__(data=data, url=url, filename=filename)
96 super(Audio, self).__init__(data=data, url=url, filename=filename)
97
97
98 if self.data is not None and not isinstance(self.data, bytes):
98 if self.data is not None and not isinstance(self.data, bytes):
99 self.data = self._make_wav(data,rate)
99 self.data = self._make_wav(data,rate)
100
100
101 def reload(self):
101 def reload(self):
102 """Reload the raw data from file or URL."""
102 """Reload the raw data from file or URL."""
103 import mimetypes
103 import mimetypes
104 if self.embed:
104 if self.embed:
105 super(Audio, self).reload()
105 super(Audio, self).reload()
106
106
107 if self.filename is not None:
107 if self.filename is not None:
108 self.mimetype = mimetypes.guess_type(self.filename)[0]
108 self.mimetype = mimetypes.guess_type(self.filename)[0]
109 elif self.url is not None:
109 elif self.url is not None:
110 self.mimetype = mimetypes.guess_type(self.url)[0]
110 self.mimetype = mimetypes.guess_type(self.url)[0]
111 else:
111 else:
112 self.mimetype = "audio/wav"
112 self.mimetype = "audio/wav"
113
113
114 def _make_wav(self, data, rate):
114 def _make_wav(self, data, rate):
115 """ Transform a numpy array to a PCM bytestring """
115 """ Transform a numpy array to a PCM bytestring """
116 import struct
116 import struct
117 from io import BytesIO
117 from io import BytesIO
118 import wave
118 import wave
119
119
120 try:
120 try:
121 import numpy as np
121 import numpy as np
122
122
123 data = np.array(data, dtype=float)
123 data = np.array(data, dtype=float)
124 if len(data.shape) == 1:
124 if len(data.shape) == 1:
125 nchan = 1
125 nchan = 1
126 elif len(data.shape) == 2:
126 elif len(data.shape) == 2:
127 # In wave files,channels are interleaved. E.g.,
127 # In wave files,channels are interleaved. E.g.,
128 # "L1R1L2R2..." for stereo. See
128 # "L1R1L2R2..." for stereo. See
129 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
129 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
130 # for channel ordering
130 # for channel ordering
131 nchan = data.shape[0]
131 nchan = data.shape[0]
132 data = data.T.ravel()
132 data = data.T.ravel()
133 else:
133 else:
134 raise ValueError('Array audio input must be a 1D or 2D array')
134 raise ValueError('Array audio input must be a 1D or 2D array')
135 scaled = np.int16(data/np.max(np.abs(data))*32767).tolist()
135 scaled = np.int16(data/np.max(np.abs(data))*32767).tolist()
136 except ImportError:
136 except ImportError:
137 # check that it is a "1D" list
137 # check that it is a "1D" list
138 idata = iter(data) # fails if not an iterable
138 idata = iter(data) # fails if not an iterable
139 try:
139 try:
140 iter(idata.next())
140 iter(idata.next())
141 raise TypeError('Only lists of mono audio are '
141 raise TypeError('Only lists of mono audio are '
142 'supported if numpy is not installed')
142 'supported if numpy is not installed')
143 except TypeError:
143 except TypeError:
144 # this means it's not a nested list, which is what we want
144 # this means it's not a nested list, which is what we want
145 pass
145 pass
146 maxabsvalue = float(max([abs(x) for x in data]))
146 maxabsvalue = float(max([abs(x) for x in data]))
147 scaled = [int(x/maxabsvalue*32767) for x in data]
147 scaled = [int(x/maxabsvalue*32767) for x in data]
148 nchan = 1
148 nchan = 1
149
149
150 fp = BytesIO()
150 fp = BytesIO()
151 waveobj = wave.open(fp,mode='wb')
151 waveobj = wave.open(fp,mode='wb')
152 waveobj.setnchannels(nchan)
152 waveobj.setnchannels(nchan)
153 waveobj.setframerate(rate)
153 waveobj.setframerate(rate)
154 waveobj.setsampwidth(2)
154 waveobj.setsampwidth(2)
155 waveobj.setcomptype('NONE','NONE')
155 waveobj.setcomptype('NONE','NONE')
156 waveobj.writeframes(b''.join([struct.pack('<h',x) for x in scaled]))
156 waveobj.writeframes(b''.join([struct.pack('<h',x) for x in scaled]))
157 val = fp.getvalue()
157 val = fp.getvalue()
158 waveobj.close()
158 waveobj.close()
159
159
160 return val
160 return val
161
161
162 def _data_and_metadata(self):
162 def _data_and_metadata(self):
163 """shortcut for returning metadata with url information, if defined"""
163 """shortcut for returning metadata with url information, if defined"""
164 md = {}
164 md = {}
165 if self.url:
165 if self.url:
166 md['url'] = self.url
166 md['url'] = self.url
167 if md:
167 if md:
168 return self.data, md
168 return self.data, md
169 else:
169 else:
170 return self.data
170 return self.data
171
171
172 def _repr_html_(self):
172 def _repr_html_(self):
173 src = """
173 src = """
174 <audio controls="controls" {autoplay}>
174 <audio controls="controls" {autoplay}>
175 <source src="{src}" type="{type}" />
175 <source src="{src}" type="{type}" />
176 Your browser does not support the audio element.
176 Your browser does not support the audio element.
177 </audio>
177 </audio>
178 """
178 """
179 return src.format(src=self.src_attr(),type=self.mimetype, autoplay=self.autoplay_attr())
179 return src.format(src=self.src_attr(),type=self.mimetype, autoplay=self.autoplay_attr())
180
180
181 def src_attr(self):
181 def src_attr(self):
182 import base64
182 import base64
183 if self.embed and (self.data is not None):
183 if self.embed and (self.data is not None):
184 data = base64=base64.b64encode(self.data).decode('ascii')
184 data = base64=base64.b64encode(self.data).decode('ascii')
185 return """data:{type};base64,{base64}""".format(type=self.mimetype,
185 return """data:{type};base64,{base64}""".format(type=self.mimetype,
186 base64=data)
186 base64=data)
187 elif self.url is not None:
187 elif self.url is not None:
188 return self.url
188 return self.url
189 else:
189 else:
190 return ""
190 return ""
191
191
192 def autoplay_attr(self):
192 def autoplay_attr(self):
193 if(self.autoplay):
193 if(self.autoplay):
194 return 'autoplay="autoplay"'
194 return 'autoplay="autoplay"'
195 else:
195 else:
196 return ''
196 return ''
197
197
198 class IFrame(object):
198 class IFrame(object):
199 """
199 """
200 Generic class to embed an iframe in an IPython notebook
200 Generic class to embed an iframe in an IPython notebook
201 """
201 """
202
202
203 iframe = """
203 iframe = """
204 <iframe
204 <iframe
205 width="{width}"
205 width="{width}"
206 height="{height}"
206 height="{height}"
207 src="{src}{params}"
207 src="{src}{params}"
208 frameborder="0"
208 frameborder="0"
209 allowfullscreen
209 allowfullscreen
210 ></iframe>
210 ></iframe>
211 """
211 """
212
212
213 def __init__(self, src, width, height, **kwargs):
213 def __init__(self, src, width, height, **kwargs):
214 self.src = src
214 self.src = src
215 self.width = width
215 self.width = width
216 self.height = height
216 self.height = height
217 self.params = kwargs
217 self.params = kwargs
218
218
219 def _repr_html_(self):
219 def _repr_html_(self):
220 """return the embed iframe"""
220 """return the embed iframe"""
221 if self.params:
221 if self.params:
222 try:
222 try:
223 from urllib.parse import urlencode # Py 3
223 from urllib.parse import urlencode # Py 3
224 except ImportError:
224 except ImportError:
225 from urllib import urlencode
225 from urllib import urlencode
226 params = "?" + urlencode(self.params)
226 params = "?" + urlencode(self.params)
227 else:
227 else:
228 params = ""
228 params = ""
229 return self.iframe.format(src=self.src,
229 return self.iframe.format(src=self.src,
230 width=self.width,
230 width=self.width,
231 height=self.height,
231 height=self.height,
232 params=params)
232 params=params)
233
233
234 class YouTubeVideo(IFrame):
234 class YouTubeVideo(IFrame):
235 """Class for embedding a YouTube Video in an IPython session, based on its video id.
235 """Class for embedding a YouTube Video in an IPython session, based on its video id.
236
236
237 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
237 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
238 do::
238 do::
239
239
240 vid = YouTubeVideo("foo")
240 vid = YouTubeVideo("foo")
241 display(vid)
241 display(vid)
242
242
243 To start from 30 seconds::
243 To start from 30 seconds::
244
244
245 vid = YouTubeVideo("abc", start=30)
245 vid = YouTubeVideo("abc", start=30)
246 display(vid)
246 display(vid)
247
247
248 To calculate seconds from time as hours, minutes, seconds use
248 To calculate seconds from time as hours, minutes, seconds use
249 :class:`datetime.timedelta`::
249 :class:`datetime.timedelta`::
250
250
251 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
251 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
252
252
253 Other parameters can be provided as documented at
253 Other parameters can be provided as documented at
254 https://developers.google.com/youtube/player_parameters#parameter-subheader
254 https://developers.google.com/youtube/player_parameters#parameter-subheader
255 """
255 """
256
256
257 def __init__(self, id, width=400, height=300, **kwargs):
257 def __init__(self, id, width=400, height=300, **kwargs):
258 src = "https://www.youtube.com/embed/{0}".format(id)
258 src = "https://www.youtube.com/embed/{0}".format(id)
259 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
259 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
260
260
261 class VimeoVideo(IFrame):
261 class VimeoVideo(IFrame):
262 """
262 """
263 Class for embedding a Vimeo video in an IPython session, based on its video id.
263 Class for embedding a Vimeo video in an IPython session, based on its video id.
264 """
264 """
265
265
266 def __init__(self, id, width=400, height=300, **kwargs):
266 def __init__(self, id, width=400, height=300, **kwargs):
267 src="https://player.vimeo.com/video/{0}".format(id)
267 src="https://player.vimeo.com/video/{0}".format(id)
268 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
268 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
269
269
270 class ScribdDocument(IFrame):
270 class ScribdDocument(IFrame):
271 """
271 """
272 Class for embedding a Scribd document in an IPython session
272 Class for embedding a Scribd document in an IPython session
273
273
274 Use the start_page params to specify a starting point in the document
274 Use the start_page params to specify a starting point in the document
275 Use the view_mode params to specify display type one off scroll | slideshow | book
275 Use the view_mode params to specify display type one off scroll | slideshow | book
276
276
277 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
277 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
278
278
279 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
279 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
280 """
280 """
281
281
282 def __init__(self, id, width=400, height=300, **kwargs):
282 def __init__(self, id, width=400, height=300, **kwargs):
283 src="https://www.scribd.com/embeds/{0}/content".format(id)
283 src="https://www.scribd.com/embeds/{0}/content".format(id)
284 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
284 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
285
285
286 class FileLink(object):
286 class FileLink(object):
287 """Class for embedding a local file link in an IPython session, based on path
287 """Class for embedding a local file link in an IPython session, based on path
288
288
289 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
289 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
290
290
291 you would do::
291 you would do::
292
292
293 local_file = FileLink("my/data.txt")
293 local_file = FileLink("my/data.txt")
294 display(local_file)
294 display(local_file)
295
295
296 or in the HTML notebook, just::
296 or in the HTML notebook, just::
297
297
298 FileLink("my/data.txt")
298 FileLink("my/data.txt")
299 """
299 """
300
300
301 html_link_str = "<a href='%s' target='_blank'>%s</a>"
301 html_link_str = "<a href='%s' target='_blank'>%s</a>"
302
302
303 def __init__(self,
303 def __init__(self,
304 path,
304 path,
305 url_prefix='',
305 url_prefix='',
306 result_html_prefix='',
306 result_html_prefix='',
307 result_html_suffix='<br>'):
307 result_html_suffix='<br>'):
308 """
308 """
309 Parameters
309 Parameters
310 ----------
310 ----------
311 path : str
311 path : str
312 path to the file or directory that should be formatted
312 path to the file or directory that should be formatted
313 directory_prefix : str
313 directory_prefix : str
314 prefix to be prepended to all files to form a working link [default:
314 prefix to be prepended to all files to form a working link [default:
315 'files']
315 'files']
316 result_html_prefix : str
316 result_html_prefix : str
317 text to append to beginning to link [default: none]
317 text to append to beginning to link [default: none]
318 result_html_suffix : str
318 result_html_suffix : str
319 text to append at the end of link [default: '<br>']
319 text to append at the end of link [default: '<br>']
320 """
320 """
321 if isdir(path):
321 if isdir(path):
322 raise ValueError("Cannot display a directory using FileLink. "
322 raise ValueError("Cannot display a directory using FileLink. "
323 "Use FileLinks to display '%s'." % path)
323 "Use FileLinks to display '%s'." % path)
324 self.path = path
324 self.path = path
325 self.url_prefix = url_prefix
325 self.url_prefix = url_prefix
326 self.result_html_prefix = result_html_prefix
326 self.result_html_prefix = result_html_prefix
327 self.result_html_suffix = result_html_suffix
327 self.result_html_suffix = result_html_suffix
328
328
329 def _format_path(self):
329 def _format_path(self):
330 fp = ''.join([self.url_prefix,self.path])
330 fp = ''.join([self.url_prefix,self.path])
331 return ''.join([self.result_html_prefix,
331 return ''.join([self.result_html_prefix,
332 self.html_link_str % (fp, self.path),
332 self.html_link_str % (fp, self.path),
333 self.result_html_suffix])
333 self.result_html_suffix])
334
334
335 def _repr_html_(self):
335 def _repr_html_(self):
336 """return html link to file
336 """return html link to file
337 """
337 """
338 if not exists(self.path):
338 if not exists(self.path):
339 return ("Path (<tt>%s</tt>) doesn't exist. "
339 return ("Path (<tt>%s</tt>) doesn't exist. "
340 "It may still be in the process of "
340 "It may still be in the process of "
341 "being generated, or you may have the "
341 "being generated, or you may have the "
342 "incorrect path." % self.path)
342 "incorrect path." % self.path)
343
343
344 return self._format_path()
344 return self._format_path()
345
345
346 def __repr__(self):
346 def __repr__(self):
347 """return absolute path to file
347 """return absolute path to file
348 """
348 """
349 return abspath(self.path)
349 return abspath(self.path)
350
350
351 class FileLinks(FileLink):
351 class FileLinks(FileLink):
352 """Class for embedding local file links in an IPython session, based on path
352 """Class for embedding local file links in an IPython session, based on path
353
353
354 e.g. to embed links to files that were generated in the IPython notebook
354 e.g. to embed links to files that were generated in the IPython notebook
355 under ``my/data``, you would do::
355 under ``my/data``, you would do::
356
356
357 local_files = FileLinks("my/data")
357 local_files = FileLinks("my/data")
358 display(local_files)
358 display(local_files)
359
359
360 or in the HTML notebook, just::
360 or in the HTML notebook, just::
361
361
362 FileLinks("my/data")
362 FileLinks("my/data")
363 """
363 """
364 def __init__(self,
364 def __init__(self,
365 path,
365 path,
366 url_prefix='',
366 url_prefix='',
367 included_suffixes=None,
367 included_suffixes=None,
368 result_html_prefix='',
368 result_html_prefix='',
369 result_html_suffix='<br>',
369 result_html_suffix='<br>',
370 notebook_display_formatter=None,
370 notebook_display_formatter=None,
371 terminal_display_formatter=None):
371 terminal_display_formatter=None):
372 """
372 """
373 See :class:`FileLink` for the ``path``, ``url_prefix``,
373 See :class:`FileLink` for the ``path``, ``url_prefix``,
374 ``result_html_prefix`` and ``result_html_suffix`` parameters.
374 ``result_html_prefix`` and ``result_html_suffix`` parameters.
375
375
376 included_suffixes : list
376 included_suffixes : list
377 Filename suffixes to include when formatting output [default: include
377 Filename suffixes to include when formatting output [default: include
378 all files]
378 all files]
379
379
380 notebook_display_formatter : function
380 notebook_display_formatter : function
381 Used to format links for display in the notebook. See discussion of
381 Used to format links for display in the notebook. See discussion of
382 formatter functions below.
382 formatter functions below.
383
383
384 terminal_display_formatter : function
384 terminal_display_formatter : function
385 Used to format links for display in the terminal. See discussion of
385 Used to format links for display in the terminal. See discussion of
386 formatter functions below.
386 formatter functions below.
387
387
388 Formatter functions must be of the form::
388 Formatter functions must be of the form::
389
389
390 f(dirname, fnames, included_suffixes)
390 f(dirname, fnames, included_suffixes)
391
391
392 dirname : str
392 dirname : str
393 The name of a directory
393 The name of a directory
394 fnames : list
394 fnames : list
395 The files in that directory
395 The files in that directory
396 included_suffixes : list
396 included_suffixes : list
397 The file suffixes that should be included in the output (passing None
397 The file suffixes that should be included in the output (passing None
398 meansto include all suffixes in the output in the built-in formatters)
398 meansto include all suffixes in the output in the built-in formatters)
399
399
400 The function should return a list of lines that will be printed in the
400 The function should return a list of lines that will be printed in the
401 notebook (if passing notebook_display_formatter) or the terminal (if
401 notebook (if passing notebook_display_formatter) or the terminal (if
402 passing terminal_display_formatter). This function is iterated over for
402 passing terminal_display_formatter). This function is iterated over for
403 each directory in self.path. Default formatters are in place, can be
403 each directory in self.path. Default formatters are in place, can be
404 passed here to support alternative formatting.
404 passed here to support alternative formatting.
405
405
406 """
406 """
407 if isfile(path):
407 if isfile(path):
408 raise ValueError("Cannot display a file using FileLinks. "
408 raise ValueError("Cannot display a file using FileLinks. "
409 "Use FileLink to display '%s'." % path)
409 "Use FileLink to display '%s'." % path)
410 self.included_suffixes = included_suffixes
410 self.included_suffixes = included_suffixes
411 # remove trailing slashs for more consistent output formatting
411 # remove trailing slashs for more consistent output formatting
412 path = path.rstrip('/')
412 path = path.rstrip('/')
413
413
414 self.path = path
414 self.path = path
415 self.url_prefix = url_prefix
415 self.url_prefix = url_prefix
416 self.result_html_prefix = result_html_prefix
416 self.result_html_prefix = result_html_prefix
417 self.result_html_suffix = result_html_suffix
417 self.result_html_suffix = result_html_suffix
418
418
419 self.notebook_display_formatter = \
419 self.notebook_display_formatter = \
420 notebook_display_formatter or self._get_notebook_display_formatter()
420 notebook_display_formatter or self._get_notebook_display_formatter()
421 self.terminal_display_formatter = \
421 self.terminal_display_formatter = \
422 terminal_display_formatter or self._get_terminal_display_formatter()
422 terminal_display_formatter or self._get_terminal_display_formatter()
423
423
424 def _get_display_formatter(self,
424 def _get_display_formatter(self,
425 dirname_output_format,
425 dirname_output_format,
426 fname_output_format,
426 fname_output_format,
427 fp_format,
427 fp_format,
428 fp_cleaner=None):
428 fp_cleaner=None):
429 """ generate built-in formatter function
429 """ generate built-in formatter function
430
430
431 this is used to define both the notebook and terminal built-in
431 this is used to define both the notebook and terminal built-in
432 formatters as they only differ by some wrapper text for each entry
432 formatters as they only differ by some wrapper text for each entry
433
433
434 dirname_output_format: string to use for formatting directory
434 dirname_output_format: string to use for formatting directory
435 names, dirname will be substituted for a single "%s" which
435 names, dirname will be substituted for a single "%s" which
436 must appear in this string
436 must appear in this string
437 fname_output_format: string to use for formatting file names,
437 fname_output_format: string to use for formatting file names,
438 if a single "%s" appears in the string, fname will be substituted
438 if a single "%s" appears in the string, fname will be substituted
439 if two "%s" appear in the string, the path to fname will be
439 if two "%s" appear in the string, the path to fname will be
440 substituted for the first and fname will be substituted for the
440 substituted for the first and fname will be substituted for the
441 second
441 second
442 fp_format: string to use for formatting filepaths, must contain
442 fp_format: string to use for formatting filepaths, must contain
443 exactly two "%s" and the dirname will be subsituted for the first
443 exactly two "%s" and the dirname will be subsituted for the first
444 and fname will be substituted for the second
444 and fname will be substituted for the second
445 """
445 """
446 def f(dirname, fnames, included_suffixes=None):
446 def f(dirname, fnames, included_suffixes=None):
447 result = []
447 result = []
448 # begin by figuring out which filenames, if any,
448 # begin by figuring out which filenames, if any,
449 # are going to be displayed
449 # are going to be displayed
450 display_fnames = []
450 display_fnames = []
451 for fname in fnames:
451 for fname in fnames:
452 if (isfile(join(dirname,fname)) and
452 if (isfile(join(dirname,fname)) and
453 (included_suffixes == None or
453 (included_suffixes is None or
454 splitext(fname)[1] in included_suffixes)):
454 splitext(fname)[1] in included_suffixes)):
455 display_fnames.append(fname)
455 display_fnames.append(fname)
456
456
457 if len(display_fnames) == 0:
457 if len(display_fnames) == 0:
458 # if there are no filenames to display, don't print anything
458 # if there are no filenames to display, don't print anything
459 # (not even the directory name)
459 # (not even the directory name)
460 pass
460 pass
461 else:
461 else:
462 # otherwise print the formatted directory name followed by
462 # otherwise print the formatted directory name followed by
463 # the formatted filenames
463 # the formatted filenames
464 dirname_output_line = dirname_output_format % dirname
464 dirname_output_line = dirname_output_format % dirname
465 result.append(dirname_output_line)
465 result.append(dirname_output_line)
466 for fname in display_fnames:
466 for fname in display_fnames:
467 fp = fp_format % (dirname,fname)
467 fp = fp_format % (dirname,fname)
468 if fp_cleaner is not None:
468 if fp_cleaner is not None:
469 fp = fp_cleaner(fp)
469 fp = fp_cleaner(fp)
470 try:
470 try:
471 # output can include both a filepath and a filename...
471 # output can include both a filepath and a filename...
472 fname_output_line = fname_output_format % (fp, fname)
472 fname_output_line = fname_output_format % (fp, fname)
473 except TypeError:
473 except TypeError:
474 # ... or just a single filepath
474 # ... or just a single filepath
475 fname_output_line = fname_output_format % fname
475 fname_output_line = fname_output_format % fname
476 result.append(fname_output_line)
476 result.append(fname_output_line)
477 return result
477 return result
478 return f
478 return f
479
479
480 def _get_notebook_display_formatter(self,
480 def _get_notebook_display_formatter(self,
481 spacer="&nbsp;&nbsp;"):
481 spacer="&nbsp;&nbsp;"):
482 """ generate function to use for notebook formatting
482 """ generate function to use for notebook formatting
483 """
483 """
484 dirname_output_format = \
484 dirname_output_format = \
485 self.result_html_prefix + "%s/" + self.result_html_suffix
485 self.result_html_prefix + "%s/" + self.result_html_suffix
486 fname_output_format = \
486 fname_output_format = \
487 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
487 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
488 fp_format = self.url_prefix + '%s/%s'
488 fp_format = self.url_prefix + '%s/%s'
489 if sep == "\\":
489 if sep == "\\":
490 # Working on a platform where the path separator is "\", so
490 # Working on a platform where the path separator is "\", so
491 # must convert these to "/" for generating a URI
491 # must convert these to "/" for generating a URI
492 def fp_cleaner(fp):
492 def fp_cleaner(fp):
493 # Replace all occurences of backslash ("\") with a forward
493 # Replace all occurences of backslash ("\") with a forward
494 # slash ("/") - this is necessary on windows when a path is
494 # slash ("/") - this is necessary on windows when a path is
495 # provided as input, but we must link to a URI
495 # provided as input, but we must link to a URI
496 return fp.replace('\\','/')
496 return fp.replace('\\','/')
497 else:
497 else:
498 fp_cleaner = None
498 fp_cleaner = None
499
499
500 return self._get_display_formatter(dirname_output_format,
500 return self._get_display_formatter(dirname_output_format,
501 fname_output_format,
501 fname_output_format,
502 fp_format,
502 fp_format,
503 fp_cleaner)
503 fp_cleaner)
504
504
505 def _get_terminal_display_formatter(self,
505 def _get_terminal_display_formatter(self,
506 spacer=" "):
506 spacer=" "):
507 """ generate function to use for terminal formatting
507 """ generate function to use for terminal formatting
508 """
508 """
509 dirname_output_format = "%s/"
509 dirname_output_format = "%s/"
510 fname_output_format = spacer + "%s"
510 fname_output_format = spacer + "%s"
511 fp_format = '%s/%s'
511 fp_format = '%s/%s'
512
512
513 return self._get_display_formatter(dirname_output_format,
513 return self._get_display_formatter(dirname_output_format,
514 fname_output_format,
514 fname_output_format,
515 fp_format)
515 fp_format)
516
516
517 def _format_path(self):
517 def _format_path(self):
518 result_lines = []
518 result_lines = []
519 walked_dir = list(walk(self.path))
519 walked_dir = list(walk(self.path))
520 walked_dir.sort()
520 walked_dir.sort()
521 for dirname, subdirs, fnames in walked_dir:
521 for dirname, subdirs, fnames in walked_dir:
522 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
522 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
523 return '\n'.join(result_lines)
523 return '\n'.join(result_lines)
524
524
525 def __repr__(self):
525 def __repr__(self):
526 """return newline-separated absolute paths
526 """return newline-separated absolute paths
527 """
527 """
528 result_lines = []
528 result_lines = []
529 walked_dir = list(walk(self.path))
529 walked_dir = list(walk(self.path))
530 walked_dir.sort()
530 walked_dir.sort()
531 for dirname, subdirs, fnames in walked_dir:
531 for dirname, subdirs, fnames in walked_dir:
532 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
532 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
533 return '\n'.join(result_lines)
533 return '\n'.join(result_lines)
@@ -1,213 +1,213 b''
1 """MagicHelper - dockable widget showing magic commands for the MainWindow
1 """MagicHelper - dockable widget showing magic commands for the MainWindow
2 """
2 """
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Imports
8 # Imports
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 # stdlib imports
11 # stdlib imports
12 import json
12 import json
13 import re
13 import re
14 import sys
14 import sys
15
15
16 # System library imports
16 # System library imports
17 from IPython.external.qt import QtGui,QtCore
17 from IPython.external.qt import QtGui,QtCore
18
18
19 from IPython.core.magic import magic_escapes
19 from IPython.core.magic import magic_escapes
20
20
21 class MagicHelper(QtGui.QDockWidget):
21 class MagicHelper(QtGui.QDockWidget):
22 """MagicHelper - dockable widget for convenient search and running of
22 """MagicHelper - dockable widget for convenient search and running of
23 magic command for IPython QtConsole.
23 magic command for IPython QtConsole.
24 """
24 """
25
25
26 #---------------------------------------------------------------------------
26 #---------------------------------------------------------------------------
27 # signals
27 # signals
28 #---------------------------------------------------------------------------
28 #---------------------------------------------------------------------------
29
29
30 pasteRequested = QtCore.Signal(str, name = 'pasteRequested')
30 pasteRequested = QtCore.Signal(str, name = 'pasteRequested')
31 """This signal is emitted when user wants to paste selected magic
31 """This signal is emitted when user wants to paste selected magic
32 command into the command line.
32 command into the command line.
33 """
33 """
34
34
35 runRequested = QtCore.Signal(str, name = 'runRequested')
35 runRequested = QtCore.Signal(str, name = 'runRequested')
36 """This signal is emitted when user wants to execute selected magic command
36 """This signal is emitted when user wants to execute selected magic command
37 """
37 """
38
38
39 readyForUpdate = QtCore.Signal(name = 'readyForUpdate')
39 readyForUpdate = QtCore.Signal(name = 'readyForUpdate')
40 """This signal is emitted when MagicHelper is ready to be populated.
40 """This signal is emitted when MagicHelper is ready to be populated.
41 Since kernel querying mechanisms are out of scope of this class,
41 Since kernel querying mechanisms are out of scope of this class,
42 it expects its owner to invoke MagicHelper.populate_magic_helper()
42 it expects its owner to invoke MagicHelper.populate_magic_helper()
43 as a reaction on this event.
43 as a reaction on this event.
44 """
44 """
45
45
46 #---------------------------------------------------------------------------
46 #---------------------------------------------------------------------------
47 # constructor
47 # constructor
48 #---------------------------------------------------------------------------
48 #---------------------------------------------------------------------------
49
49
50 def __init__(self, name, parent):
50 def __init__(self, name, parent):
51 super(MagicHelper, self).__init__(name, parent)
51 super(MagicHelper, self).__init__(name, parent)
52
52
53 self.data = None
53 self.data = None
54
54
55 class MinListWidget(QtGui.QListWidget):
55 class MinListWidget(QtGui.QListWidget):
56 """Temp class to overide the default QListWidget size hint
56 """Temp class to overide the default QListWidget size hint
57 in order to make MagicHelper narrow
57 in order to make MagicHelper narrow
58 """
58 """
59 def sizeHint(self):
59 def sizeHint(self):
60 s = QtCore.QSize()
60 s = QtCore.QSize()
61 s.setHeight(super(MinListWidget,self).sizeHint().height())
61 s.setHeight(super(MinListWidget,self).sizeHint().height())
62 s.setWidth(self.sizeHintForColumn(0))
62 s.setWidth(self.sizeHintForColumn(0))
63 return s
63 return s
64
64
65 # construct content
65 # construct content
66 self.frame = QtGui.QFrame()
66 self.frame = QtGui.QFrame()
67 self.search_label = QtGui.QLabel("Search:")
67 self.search_label = QtGui.QLabel("Search:")
68 self.search_line = QtGui.QLineEdit()
68 self.search_line = QtGui.QLineEdit()
69 self.search_class = QtGui.QComboBox()
69 self.search_class = QtGui.QComboBox()
70 self.search_list = MinListWidget()
70 self.search_list = MinListWidget()
71 self.paste_button = QtGui.QPushButton("Paste")
71 self.paste_button = QtGui.QPushButton("Paste")
72 self.run_button = QtGui.QPushButton("Run")
72 self.run_button = QtGui.QPushButton("Run")
73
73
74 # layout all the widgets
74 # layout all the widgets
75 main_layout = QtGui.QVBoxLayout()
75 main_layout = QtGui.QVBoxLayout()
76 search_layout = QtGui.QHBoxLayout()
76 search_layout = QtGui.QHBoxLayout()
77 search_layout.addWidget(self.search_label)
77 search_layout.addWidget(self.search_label)
78 search_layout.addWidget(self.search_line, 10)
78 search_layout.addWidget(self.search_line, 10)
79 main_layout.addLayout(search_layout)
79 main_layout.addLayout(search_layout)
80 main_layout.addWidget(self.search_class)
80 main_layout.addWidget(self.search_class)
81 main_layout.addWidget(self.search_list, 10)
81 main_layout.addWidget(self.search_list, 10)
82 action_layout = QtGui.QHBoxLayout()
82 action_layout = QtGui.QHBoxLayout()
83 action_layout.addWidget(self.paste_button)
83 action_layout.addWidget(self.paste_button)
84 action_layout.addWidget(self.run_button)
84 action_layout.addWidget(self.run_button)
85 main_layout.addLayout(action_layout)
85 main_layout.addLayout(action_layout)
86
86
87 self.frame.setLayout(main_layout)
87 self.frame.setLayout(main_layout)
88 self.setWidget(self.frame)
88 self.setWidget(self.frame)
89
89
90 # connect all the relevant signals to handlers
90 # connect all the relevant signals to handlers
91 self.visibilityChanged[bool].connect( self._update_magic_helper )
91 self.visibilityChanged[bool].connect( self._update_magic_helper )
92 self.search_class.activated[int].connect(
92 self.search_class.activated[int].connect(
93 self.class_selected
93 self.class_selected
94 )
94 )
95 self.search_line.textChanged[str].connect(
95 self.search_line.textChanged[str].connect(
96 self.search_changed
96 self.search_changed
97 )
97 )
98 self.search_list.itemDoubleClicked[QtGui.QListWidgetItem].connect(
98 self.search_list.itemDoubleClicked[QtGui.QListWidgetItem].connect(
99 self.paste_requested
99 self.paste_requested
100 )
100 )
101 self.paste_button.clicked[bool].connect(
101 self.paste_button.clicked[bool].connect(
102 self.paste_requested
102 self.paste_requested
103 )
103 )
104 self.run_button.clicked[bool].connect(
104 self.run_button.clicked[bool].connect(
105 self.run_requested
105 self.run_requested
106 )
106 )
107
107
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109 # implementation
109 # implementation
110 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
111
111
112 def _update_magic_helper(self, visible):
112 def _update_magic_helper(self, visible):
113 """Start update sequence.
113 """Start update sequence.
114 This method is called when MagicHelper becomes visible. It clears
114 This method is called when MagicHelper becomes visible. It clears
115 the content and emits readyForUpdate signal. The owner of the
115 the content and emits readyForUpdate signal. The owner of the
116 instance is expected to invoke populate_magic_helper() when magic
116 instance is expected to invoke populate_magic_helper() when magic
117 info is available.
117 info is available.
118 """
118 """
119 if not visible or self.data != None:
119 if not visible or self.data is not None:
120 return
120 return
121 self.data = {}
121 self.data = {}
122 self.search_class.clear()
122 self.search_class.clear()
123 self.search_class.addItem("Populating...")
123 self.search_class.addItem("Populating...")
124 self.search_list.clear()
124 self.search_list.clear()
125 self.readyForUpdate.emit()
125 self.readyForUpdate.emit()
126
126
127 def populate_magic_helper(self, data):
127 def populate_magic_helper(self, data):
128 """Expects data returned by lsmagics query from kernel.
128 """Expects data returned by lsmagics query from kernel.
129 Populates the search_class and search_list with relevant items.
129 Populates the search_class and search_list with relevant items.
130 """
130 """
131 self.search_class.clear()
131 self.search_class.clear()
132 self.search_list.clear()
132 self.search_list.clear()
133
133
134 self.data = json.loads(
134 self.data = json.loads(
135 data['data'].get('application/json', {})
135 data['data'].get('application/json', {})
136 )
136 )
137
137
138 self.search_class.addItem('All Magics', 'any')
138 self.search_class.addItem('All Magics', 'any')
139 classes = set()
139 classes = set()
140
140
141 for mtype in sorted(self.data):
141 for mtype in sorted(self.data):
142 subdict = self.data[mtype]
142 subdict = self.data[mtype]
143 for name in sorted(subdict):
143 for name in sorted(subdict):
144 classes.add(subdict[name])
144 classes.add(subdict[name])
145
145
146 for cls in sorted(classes):
146 for cls in sorted(classes):
147 label = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>", cls)
147 label = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>", cls)
148 self.search_class.addItem(label, cls)
148 self.search_class.addItem(label, cls)
149
149
150 self.filter_magic_helper('.', 'any')
150 self.filter_magic_helper('.', 'any')
151
151
152 def class_selected(self, index):
152 def class_selected(self, index):
153 """Handle search_class selection changes
153 """Handle search_class selection changes
154 """
154 """
155 item = self.search_class.itemData(index)
155 item = self.search_class.itemData(index)
156 regex = self.search_line.text()
156 regex = self.search_line.text()
157 self.filter_magic_helper(regex = regex, cls = item)
157 self.filter_magic_helper(regex = regex, cls = item)
158
158
159 def search_changed(self, search_string):
159 def search_changed(self, search_string):
160 """Handle search_line text changes.
160 """Handle search_line text changes.
161 The text is interpreted as a regular expression
161 The text is interpreted as a regular expression
162 """
162 """
163 item = self.search_class.itemData(
163 item = self.search_class.itemData(
164 self.search_class.currentIndex()
164 self.search_class.currentIndex()
165 )
165 )
166 self.filter_magic_helper(regex = search_string, cls = item)
166 self.filter_magic_helper(regex = search_string, cls = item)
167
167
168 def _get_current_search_item(self, item = None):
168 def _get_current_search_item(self, item = None):
169 """Retrieve magic command currently selected in the search_list
169 """Retrieve magic command currently selected in the search_list
170 """
170 """
171 text = None
171 text = None
172 if not isinstance(item, QtGui.QListWidgetItem):
172 if not isinstance(item, QtGui.QListWidgetItem):
173 item = self.search_list.currentItem()
173 item = self.search_list.currentItem()
174 text = item.text()
174 text = item.text()
175 return text
175 return text
176
176
177 def paste_requested(self, item = None):
177 def paste_requested(self, item = None):
178 """Emit pasteRequested signal with currently selected item text
178 """Emit pasteRequested signal with currently selected item text
179 """
179 """
180 text = self._get_current_search_item(item)
180 text = self._get_current_search_item(item)
181 if text != None:
181 if text is not None:
182 self.pasteRequested.emit(text)
182 self.pasteRequested.emit(text)
183
183
184 def run_requested(self, item = None):
184 def run_requested(self, item = None):
185 """Emit runRequested signal with currently selected item text
185 """Emit runRequested signal with currently selected item text
186 """
186 """
187 text = self._get_current_search_item(item)
187 text = self._get_current_search_item(item)
188 if text != None:
188 if text is not None:
189 self.runRequested.emit(text)
189 self.runRequested.emit(text)
190
190
191 def filter_magic_helper(self, regex, cls):
191 def filter_magic_helper(self, regex, cls):
192 """Update search_list with magic commands whose text match
192 """Update search_list with magic commands whose text match
193 regex and class match cls.
193 regex and class match cls.
194 If cls equals 'any' - any class matches.
194 If cls equals 'any' - any class matches.
195 """
195 """
196 if regex == "" or regex == None:
196 if regex == "" or regex is None:
197 regex = '.'
197 regex = '.'
198 if cls == None:
198 if cls is None:
199 cls = 'any'
199 cls = 'any'
200
200
201 self.search_list.clear()
201 self.search_list.clear()
202 for mtype in sorted(self.data):
202 for mtype in sorted(self.data):
203 subdict = self.data[mtype]
203 subdict = self.data[mtype]
204 prefix = magic_escapes[mtype]
204 prefix = magic_escapes[mtype]
205
205
206 for name in sorted(subdict):
206 for name in sorted(subdict):
207 mclass = subdict[name]
207 mclass = subdict[name]
208 pmagic = prefix + name
208 pmagic = prefix + name
209
209
210 if (re.match(regex, name) or re.match(regex, pmagic)) and \
210 if (re.match(regex, name) or re.match(regex, pmagic)) and \
211 (cls == 'any' or cls == mclass):
211 (cls == 'any' or cls == mclass):
212 self.search_list.addItem(pmagic)
212 self.search_list.addItem(pmagic)
213
213
@@ -1,928 +1,928 b''
1 """The Qt MainWindow for the QtConsole
1 """The Qt MainWindow for the QtConsole
2
2
3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
4 common actions.
4 common actions.
5 """
5 """
6
6
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 # stdlib imports
15 # stdlib imports
16 import json
16 import json
17 import re
17 import re
18 import sys
18 import sys
19 import webbrowser
19 import webbrowser
20 from threading import Thread
20 from threading import Thread
21
21
22 # System library imports
22 # System library imports
23 from IPython.external.qt import QtGui,QtCore
23 from IPython.external.qt import QtGui,QtCore
24
24
25 from IPython.core.magic import magic_escapes
25 from IPython.core.magic import magic_escapes
26
26
27 def background(f):
27 def background(f):
28 """call a function in a simple thread, to prevent blocking"""
28 """call a function in a simple thread, to prevent blocking"""
29 t = Thread(target=f)
29 t = Thread(target=f)
30 t.start()
30 t.start()
31 return t
31 return t
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Classes
34 # Classes
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class MainWindow(QtGui.QMainWindow):
37 class MainWindow(QtGui.QMainWindow):
38
38
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40 # 'object' interface
40 # 'object' interface
41 #---------------------------------------------------------------------------
41 #---------------------------------------------------------------------------
42
42
43 def __init__(self, app,
43 def __init__(self, app,
44 confirm_exit=True,
44 confirm_exit=True,
45 new_frontend_factory=None, slave_frontend_factory=None,
45 new_frontend_factory=None, slave_frontend_factory=None,
46 ):
46 ):
47 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
47 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
48
48
49 Parameters
49 Parameters
50 ----------
50 ----------
51
51
52 app : reference to QApplication parent
52 app : reference to QApplication parent
53 confirm_exit : bool, optional
53 confirm_exit : bool, optional
54 Whether we should prompt on close of tabs
54 Whether we should prompt on close of tabs
55 new_frontend_factory : callable
55 new_frontend_factory : callable
56 A callable that returns a new IPythonWidget instance, attached to
56 A callable that returns a new IPythonWidget instance, attached to
57 its own running kernel.
57 its own running kernel.
58 slave_frontend_factory : callable
58 slave_frontend_factory : callable
59 A callable that takes an existing IPythonWidget, and returns a new
59 A callable that takes an existing IPythonWidget, and returns a new
60 IPythonWidget instance, attached to the same kernel.
60 IPythonWidget instance, attached to the same kernel.
61 """
61 """
62
62
63 super(MainWindow, self).__init__()
63 super(MainWindow, self).__init__()
64 self._kernel_counter = 0
64 self._kernel_counter = 0
65 self._app = app
65 self._app = app
66 self.confirm_exit = confirm_exit
66 self.confirm_exit = confirm_exit
67 self.new_frontend_factory = new_frontend_factory
67 self.new_frontend_factory = new_frontend_factory
68 self.slave_frontend_factory = slave_frontend_factory
68 self.slave_frontend_factory = slave_frontend_factory
69
69
70 self.tab_widget = QtGui.QTabWidget(self)
70 self.tab_widget = QtGui.QTabWidget(self)
71 self.tab_widget.setDocumentMode(True)
71 self.tab_widget.setDocumentMode(True)
72 self.tab_widget.setTabsClosable(True)
72 self.tab_widget.setTabsClosable(True)
73 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
73 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
74
74
75 self.setCentralWidget(self.tab_widget)
75 self.setCentralWidget(self.tab_widget)
76 # hide tab bar at first, since we have no tabs:
76 # hide tab bar at first, since we have no tabs:
77 self.tab_widget.tabBar().setVisible(False)
77 self.tab_widget.tabBar().setVisible(False)
78 # prevent focus in tab bar
78 # prevent focus in tab bar
79 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
79 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
80
80
81 def update_tab_bar_visibility(self):
81 def update_tab_bar_visibility(self):
82 """ update visibility of the tabBar depending of the number of tab
82 """ update visibility of the tabBar depending of the number of tab
83
83
84 0 or 1 tab, tabBar hidden
84 0 or 1 tab, tabBar hidden
85 2+ tabs, tabBar visible
85 2+ tabs, tabBar visible
86
86
87 send a self.close if number of tab ==0
87 send a self.close if number of tab ==0
88
88
89 need to be called explicitly, or be connected to tabInserted/tabRemoved
89 need to be called explicitly, or be connected to tabInserted/tabRemoved
90 """
90 """
91 if self.tab_widget.count() <= 1:
91 if self.tab_widget.count() <= 1:
92 self.tab_widget.tabBar().setVisible(False)
92 self.tab_widget.tabBar().setVisible(False)
93 else:
93 else:
94 self.tab_widget.tabBar().setVisible(True)
94 self.tab_widget.tabBar().setVisible(True)
95 if self.tab_widget.count()==0 :
95 if self.tab_widget.count()==0 :
96 self.close()
96 self.close()
97
97
98 @property
98 @property
99 def next_kernel_id(self):
99 def next_kernel_id(self):
100 """constantly increasing counter for kernel IDs"""
100 """constantly increasing counter for kernel IDs"""
101 c = self._kernel_counter
101 c = self._kernel_counter
102 self._kernel_counter += 1
102 self._kernel_counter += 1
103 return c
103 return c
104
104
105 @property
105 @property
106 def active_frontend(self):
106 def active_frontend(self):
107 return self.tab_widget.currentWidget()
107 return self.tab_widget.currentWidget()
108
108
109 def create_tab_with_new_frontend(self):
109 def create_tab_with_new_frontend(self):
110 """create a new frontend and attach it to a new tab"""
110 """create a new frontend and attach it to a new tab"""
111 widget = self.new_frontend_factory()
111 widget = self.new_frontend_factory()
112 self.add_tab_with_frontend(widget)
112 self.add_tab_with_frontend(widget)
113
113
114 def create_tab_with_current_kernel(self):
114 def create_tab_with_current_kernel(self):
115 """create a new frontend attached to the same kernel as the current tab"""
115 """create a new frontend attached to the same kernel as the current tab"""
116 current_widget = self.tab_widget.currentWidget()
116 current_widget = self.tab_widget.currentWidget()
117 current_widget_index = self.tab_widget.indexOf(current_widget)
117 current_widget_index = self.tab_widget.indexOf(current_widget)
118 current_widget_name = self.tab_widget.tabText(current_widget_index)
118 current_widget_name = self.tab_widget.tabText(current_widget_index)
119 widget = self.slave_frontend_factory(current_widget)
119 widget = self.slave_frontend_factory(current_widget)
120 if 'slave' in current_widget_name:
120 if 'slave' in current_widget_name:
121 # don't keep stacking slaves
121 # don't keep stacking slaves
122 name = current_widget_name
122 name = current_widget_name
123 else:
123 else:
124 name = '(%s) slave' % current_widget_name
124 name = '(%s) slave' % current_widget_name
125 self.add_tab_with_frontend(widget,name=name)
125 self.add_tab_with_frontend(widget,name=name)
126
126
127 def close_tab(self,current_tab):
127 def close_tab(self,current_tab):
128 """ Called when you need to try to close a tab.
128 """ Called when you need to try to close a tab.
129
129
130 It takes the number of the tab to be closed as argument, or a reference
130 It takes the number of the tab to be closed as argument, or a reference
131 to the widget inside this tab
131 to the widget inside this tab
132 """
132 """
133
133
134 # let's be sure "tab" and "closing widget" are respectively the index
134 # let's be sure "tab" and "closing widget" are respectively the index
135 # of the tab to close and a reference to the frontend to close
135 # of the tab to close and a reference to the frontend to close
136 if type(current_tab) is not int :
136 if type(current_tab) is not int :
137 current_tab = self.tab_widget.indexOf(current_tab)
137 current_tab = self.tab_widget.indexOf(current_tab)
138 closing_widget=self.tab_widget.widget(current_tab)
138 closing_widget=self.tab_widget.widget(current_tab)
139
139
140
140
141 # when trying to be closed, widget might re-send a request to be
141 # when trying to be closed, widget might re-send a request to be
142 # closed again, but will be deleted when event will be processed. So
142 # closed again, but will be deleted when event will be processed. So
143 # need to check that widget still exists and skip if not. One example
143 # need to check that widget still exists and skip if not. One example
144 # of this is when 'exit' is sent in a slave tab. 'exit' will be
144 # of this is when 'exit' is sent in a slave tab. 'exit' will be
145 # re-sent by this function on the master widget, which ask all slave
145 # re-sent by this function on the master widget, which ask all slave
146 # widgets to exit
146 # widgets to exit
147 if closing_widget==None:
147 if closing_widget is None:
148 return
148 return
149
149
150 #get a list of all slave widgets on the same kernel.
150 #get a list of all slave widgets on the same kernel.
151 slave_tabs = self.find_slave_widgets(closing_widget)
151 slave_tabs = self.find_slave_widgets(closing_widget)
152
152
153 keepkernel = None #Use the prompt by default
153 keepkernel = None #Use the prompt by default
154 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
154 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
155 keepkernel = closing_widget._keep_kernel_on_exit
155 keepkernel = closing_widget._keep_kernel_on_exit
156 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
156 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
157 # we set local slave tabs._hidden to True to avoid prompting for kernel
157 # we set local slave tabs._hidden to True to avoid prompting for kernel
158 # restart when they get the signal. and then "forward" the 'exit'
158 # restart when they get the signal. and then "forward" the 'exit'
159 # to the main window
159 # to the main window
160 if keepkernel is not None:
160 if keepkernel is not None:
161 for tab in slave_tabs:
161 for tab in slave_tabs:
162 tab._hidden = True
162 tab._hidden = True
163 if closing_widget in slave_tabs:
163 if closing_widget in slave_tabs:
164 try :
164 try :
165 self.find_master_tab(closing_widget).execute('exit')
165 self.find_master_tab(closing_widget).execute('exit')
166 except AttributeError:
166 except AttributeError:
167 self.log.info("Master already closed or not local, closing only current tab")
167 self.log.info("Master already closed or not local, closing only current tab")
168 self.tab_widget.removeTab(current_tab)
168 self.tab_widget.removeTab(current_tab)
169 self.update_tab_bar_visibility()
169 self.update_tab_bar_visibility()
170 return
170 return
171
171
172 kernel_client = closing_widget.kernel_client
172 kernel_client = closing_widget.kernel_client
173 kernel_manager = closing_widget.kernel_manager
173 kernel_manager = closing_widget.kernel_manager
174
174
175 if keepkernel is None and not closing_widget._confirm_exit:
175 if keepkernel is None and not closing_widget._confirm_exit:
176 # don't prompt, just terminate the kernel if we own it
176 # don't prompt, just terminate the kernel if we own it
177 # or leave it alone if we don't
177 # or leave it alone if we don't
178 keepkernel = closing_widget._existing
178 keepkernel = closing_widget._existing
179 if keepkernel is None: #show prompt
179 if keepkernel is None: #show prompt
180 if kernel_client and kernel_client.channels_running:
180 if kernel_client and kernel_client.channels_running:
181 title = self.window().windowTitle()
181 title = self.window().windowTitle()
182 cancel = QtGui.QMessageBox.Cancel
182 cancel = QtGui.QMessageBox.Cancel
183 okay = QtGui.QMessageBox.Ok
183 okay = QtGui.QMessageBox.Ok
184 if closing_widget._may_close:
184 if closing_widget._may_close:
185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
187 justthis = QtGui.QPushButton("&No, just this Tab", self)
187 justthis = QtGui.QPushButton("&No, just this Tab", self)
188 justthis.setShortcut('N')
188 justthis.setShortcut('N')
189 closeall = QtGui.QPushButton("&Yes, close all", self)
189 closeall = QtGui.QPushButton("&Yes, close all", self)
190 closeall.setShortcut('Y')
190 closeall.setShortcut('Y')
191 # allow ctrl-d ctrl-d exit, like in terminal
191 # allow ctrl-d ctrl-d exit, like in terminal
192 closeall.setShortcut('Ctrl+D')
192 closeall.setShortcut('Ctrl+D')
193 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
193 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
194 title, msg)
194 title, msg)
195 box.setInformativeText(info)
195 box.setInformativeText(info)
196 box.addButton(cancel)
196 box.addButton(cancel)
197 box.addButton(justthis, QtGui.QMessageBox.NoRole)
197 box.addButton(justthis, QtGui.QMessageBox.NoRole)
198 box.addButton(closeall, QtGui.QMessageBox.YesRole)
198 box.addButton(closeall, QtGui.QMessageBox.YesRole)
199 box.setDefaultButton(closeall)
199 box.setDefaultButton(closeall)
200 box.setEscapeButton(cancel)
200 box.setEscapeButton(cancel)
201 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
201 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
202 box.setIconPixmap(pixmap)
202 box.setIconPixmap(pixmap)
203 reply = box.exec_()
203 reply = box.exec_()
204 if reply == 1: # close All
204 if reply == 1: # close All
205 for slave in slave_tabs:
205 for slave in slave_tabs:
206 background(slave.kernel_client.stop_channels)
206 background(slave.kernel_client.stop_channels)
207 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
207 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
208 closing_widget.execute("exit")
208 closing_widget.execute("exit")
209 self.tab_widget.removeTab(current_tab)
209 self.tab_widget.removeTab(current_tab)
210 background(kernel_client.stop_channels)
210 background(kernel_client.stop_channels)
211 elif reply == 0: # close Console
211 elif reply == 0: # close Console
212 if not closing_widget._existing:
212 if not closing_widget._existing:
213 # Have kernel: don't quit, just close the tab
213 # Have kernel: don't quit, just close the tab
214 closing_widget.execute("exit True")
214 closing_widget.execute("exit True")
215 self.tab_widget.removeTab(current_tab)
215 self.tab_widget.removeTab(current_tab)
216 background(kernel_client.stop_channels)
216 background(kernel_client.stop_channels)
217 else:
217 else:
218 reply = QtGui.QMessageBox.question(self, title,
218 reply = QtGui.QMessageBox.question(self, title,
219 "Are you sure you want to close this Console?"+
219 "Are you sure you want to close this Console?"+
220 "\nThe Kernel and other Consoles will remain active.",
220 "\nThe Kernel and other Consoles will remain active.",
221 okay|cancel,
221 okay|cancel,
222 defaultButton=okay
222 defaultButton=okay
223 )
223 )
224 if reply == okay:
224 if reply == okay:
225 self.tab_widget.removeTab(current_tab)
225 self.tab_widget.removeTab(current_tab)
226 elif keepkernel: #close console but leave kernel running (no prompt)
226 elif keepkernel: #close console but leave kernel running (no prompt)
227 self.tab_widget.removeTab(current_tab)
227 self.tab_widget.removeTab(current_tab)
228 background(kernel_client.stop_channels)
228 background(kernel_client.stop_channels)
229 else: #close console and kernel (no prompt)
229 else: #close console and kernel (no prompt)
230 self.tab_widget.removeTab(current_tab)
230 self.tab_widget.removeTab(current_tab)
231 if kernel_client and kernel_client.channels_running:
231 if kernel_client and kernel_client.channels_running:
232 for slave in slave_tabs:
232 for slave in slave_tabs:
233 background(slave.kernel_client.stop_channels)
233 background(slave.kernel_client.stop_channels)
234 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
234 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
235 if kernel_manager:
235 if kernel_manager:
236 kernel_manager.shutdown_kernel()
236 kernel_manager.shutdown_kernel()
237 background(kernel_client.stop_channels)
237 background(kernel_client.stop_channels)
238
238
239 self.update_tab_bar_visibility()
239 self.update_tab_bar_visibility()
240
240
241 def add_tab_with_frontend(self,frontend,name=None):
241 def add_tab_with_frontend(self,frontend,name=None):
242 """ insert a tab with a given frontend in the tab bar, and give it a name
242 """ insert a tab with a given frontend in the tab bar, and give it a name
243
243
244 """
244 """
245 if not name:
245 if not name:
246 name = 'kernel %i' % self.next_kernel_id
246 name = 'kernel %i' % self.next_kernel_id
247 self.tab_widget.addTab(frontend,name)
247 self.tab_widget.addTab(frontend,name)
248 self.update_tab_bar_visibility()
248 self.update_tab_bar_visibility()
249 self.make_frontend_visible(frontend)
249 self.make_frontend_visible(frontend)
250 frontend.exit_requested.connect(self.close_tab)
250 frontend.exit_requested.connect(self.close_tab)
251
251
252 def next_tab(self):
252 def next_tab(self):
253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
254
254
255 def prev_tab(self):
255 def prev_tab(self):
256 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
256 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
257
257
258 def make_frontend_visible(self,frontend):
258 def make_frontend_visible(self,frontend):
259 widget_index=self.tab_widget.indexOf(frontend)
259 widget_index=self.tab_widget.indexOf(frontend)
260 if widget_index > 0 :
260 if widget_index > 0 :
261 self.tab_widget.setCurrentIndex(widget_index)
261 self.tab_widget.setCurrentIndex(widget_index)
262
262
263 def find_master_tab(self,tab,as_list=False):
263 def find_master_tab(self,tab,as_list=False):
264 """
264 """
265 Try to return the frontend that owns the kernel attached to the given widget/tab.
265 Try to return the frontend that owns the kernel attached to the given widget/tab.
266
266
267 Only finds frontend owned by the current application. Selection
267 Only finds frontend owned by the current application. Selection
268 based on port of the kernel might be inaccurate if several kernel
268 based on port of the kernel might be inaccurate if several kernel
269 on different ip use same port number.
269 on different ip use same port number.
270
270
271 This function does the conversion tabNumber/widget if needed.
271 This function does the conversion tabNumber/widget if needed.
272 Might return None if no master widget (non local kernel)
272 Might return None if no master widget (non local kernel)
273 Will crash IPython if more than 1 masterWidget
273 Will crash IPython if more than 1 masterWidget
274
274
275 When asList set to True, always return a list of widget(s) owning
275 When asList set to True, always return a list of widget(s) owning
276 the kernel. The list might be empty or containing several Widget.
276 the kernel. The list might be empty or containing several Widget.
277 """
277 """
278
278
279 #convert from/to int/richIpythonWidget if needed
279 #convert from/to int/richIpythonWidget if needed
280 if isinstance(tab, int):
280 if isinstance(tab, int):
281 tab = self.tab_widget.widget(tab)
281 tab = self.tab_widget.widget(tab)
282 km=tab.kernel_client
282 km=tab.kernel_client
283
283
284 #build list of all widgets
284 #build list of all widgets
285 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
285 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
286
286
287 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
287 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
288 # And should have a _may_close attribute
288 # And should have a _may_close attribute
289 filtered_widget_list = [ widget for widget in widget_list if
289 filtered_widget_list = [ widget for widget in widget_list if
290 widget.kernel_client.connection_file == km.connection_file and
290 widget.kernel_client.connection_file == km.connection_file and
291 hasattr(widget,'_may_close') ]
291 hasattr(widget,'_may_close') ]
292 # the master widget is the one that may close the kernel
292 # the master widget is the one that may close the kernel
293 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
293 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
294 if as_list:
294 if as_list:
295 return master_widget
295 return master_widget
296 assert(len(master_widget)<=1 )
296 assert(len(master_widget)<=1 )
297 if len(master_widget)==0:
297 if len(master_widget)==0:
298 return None
298 return None
299
299
300 return master_widget[0]
300 return master_widget[0]
301
301
302 def find_slave_widgets(self,tab):
302 def find_slave_widgets(self,tab):
303 """return all the frontends that do not own the kernel attached to the given widget/tab.
303 """return all the frontends that do not own the kernel attached to the given widget/tab.
304
304
305 Only find frontends owned by the current application. Selection
305 Only find frontends owned by the current application. Selection
306 based on connection file of the kernel.
306 based on connection file of the kernel.
307
307
308 This function does the conversion tabNumber/widget if needed.
308 This function does the conversion tabNumber/widget if needed.
309 """
309 """
310 #convert from/to int/richIpythonWidget if needed
310 #convert from/to int/richIpythonWidget if needed
311 if isinstance(tab, int):
311 if isinstance(tab, int):
312 tab = self.tab_widget.widget(tab)
312 tab = self.tab_widget.widget(tab)
313 km=tab.kernel_client
313 km=tab.kernel_client
314
314
315 #build list of all widgets
315 #build list of all widgets
316 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
316 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
317
317
318 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
318 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
319 filtered_widget_list = ( widget for widget in widget_list if
319 filtered_widget_list = ( widget for widget in widget_list if
320 widget.kernel_client.connection_file == km.connection_file)
320 widget.kernel_client.connection_file == km.connection_file)
321 # Get a list of all widget owning the same kernel and removed it from
321 # Get a list of all widget owning the same kernel and removed it from
322 # the previous cadidate. (better using sets ?)
322 # the previous cadidate. (better using sets ?)
323 master_widget_list = self.find_master_tab(tab, as_list=True)
323 master_widget_list = self.find_master_tab(tab, as_list=True)
324 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
324 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
325
325
326 return slave_list
326 return slave_list
327
327
328 # Populate the menu bar with common actions and shortcuts
328 # Populate the menu bar with common actions and shortcuts
329 def add_menu_action(self, menu, action, defer_shortcut=False):
329 def add_menu_action(self, menu, action, defer_shortcut=False):
330 """Add action to menu as well as self
330 """Add action to menu as well as self
331
331
332 So that when the menu bar is invisible, its actions are still available.
332 So that when the menu bar is invisible, its actions are still available.
333
333
334 If defer_shortcut is True, set the shortcut context to widget-only,
334 If defer_shortcut is True, set the shortcut context to widget-only,
335 where it will avoid conflict with shortcuts already bound to the
335 where it will avoid conflict with shortcuts already bound to the
336 widgets themselves.
336 widgets themselves.
337 """
337 """
338 menu.addAction(action)
338 menu.addAction(action)
339 self.addAction(action)
339 self.addAction(action)
340
340
341 if defer_shortcut:
341 if defer_shortcut:
342 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
342 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
343
343
344 def init_menu_bar(self):
344 def init_menu_bar(self):
345 #create menu in the order they should appear in the menu bar
345 #create menu in the order they should appear in the menu bar
346 self.init_file_menu()
346 self.init_file_menu()
347 self.init_edit_menu()
347 self.init_edit_menu()
348 self.init_view_menu()
348 self.init_view_menu()
349 self.init_kernel_menu()
349 self.init_kernel_menu()
350 self.init_magic_menu()
350 self.init_magic_menu()
351 self.init_window_menu()
351 self.init_window_menu()
352 self.init_help_menu()
352 self.init_help_menu()
353
353
354 def init_file_menu(self):
354 def init_file_menu(self):
355 self.file_menu = self.menuBar().addMenu("&File")
355 self.file_menu = self.menuBar().addMenu("&File")
356
356
357 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
357 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
358 self,
358 self,
359 shortcut="Ctrl+T",
359 shortcut="Ctrl+T",
360 triggered=self.create_tab_with_new_frontend)
360 triggered=self.create_tab_with_new_frontend)
361 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
361 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
362
362
363 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
363 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
364 self,
364 self,
365 shortcut="Ctrl+Shift+T",
365 shortcut="Ctrl+Shift+T",
366 triggered=self.create_tab_with_current_kernel)
366 triggered=self.create_tab_with_current_kernel)
367 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
367 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
368
368
369 self.file_menu.addSeparator()
369 self.file_menu.addSeparator()
370
370
371 self.close_action=QtGui.QAction("&Close Tab",
371 self.close_action=QtGui.QAction("&Close Tab",
372 self,
372 self,
373 shortcut=QtGui.QKeySequence.Close,
373 shortcut=QtGui.QKeySequence.Close,
374 triggered=self.close_active_frontend
374 triggered=self.close_active_frontend
375 )
375 )
376 self.add_menu_action(self.file_menu, self.close_action)
376 self.add_menu_action(self.file_menu, self.close_action)
377
377
378 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
378 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
379 self,
379 self,
380 shortcut=QtGui.QKeySequence.Save,
380 shortcut=QtGui.QKeySequence.Save,
381 triggered=self.export_action_active_frontend
381 triggered=self.export_action_active_frontend
382 )
382 )
383 self.add_menu_action(self.file_menu, self.export_action, True)
383 self.add_menu_action(self.file_menu, self.export_action, True)
384
384
385 self.file_menu.addSeparator()
385 self.file_menu.addSeparator()
386
386
387 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
387 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
388 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
388 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
389 # Only override the default if there is a collision.
389 # Only override the default if there is a collision.
390 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
390 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
391 printkey = "Ctrl+Shift+P"
391 printkey = "Ctrl+Shift+P"
392 self.print_action = QtGui.QAction("&Print",
392 self.print_action = QtGui.QAction("&Print",
393 self,
393 self,
394 shortcut=printkey,
394 shortcut=printkey,
395 triggered=self.print_action_active_frontend)
395 triggered=self.print_action_active_frontend)
396 self.add_menu_action(self.file_menu, self.print_action, True)
396 self.add_menu_action(self.file_menu, self.print_action, True)
397
397
398 if sys.platform != 'darwin':
398 if sys.platform != 'darwin':
399 # OSX always has Quit in the Application menu, only add it
399 # OSX always has Quit in the Application menu, only add it
400 # to the File menu elsewhere.
400 # to the File menu elsewhere.
401
401
402 self.file_menu.addSeparator()
402 self.file_menu.addSeparator()
403
403
404 self.quit_action = QtGui.QAction("&Quit",
404 self.quit_action = QtGui.QAction("&Quit",
405 self,
405 self,
406 shortcut=QtGui.QKeySequence.Quit,
406 shortcut=QtGui.QKeySequence.Quit,
407 triggered=self.close,
407 triggered=self.close,
408 )
408 )
409 self.add_menu_action(self.file_menu, self.quit_action)
409 self.add_menu_action(self.file_menu, self.quit_action)
410
410
411
411
412 def init_edit_menu(self):
412 def init_edit_menu(self):
413 self.edit_menu = self.menuBar().addMenu("&Edit")
413 self.edit_menu = self.menuBar().addMenu("&Edit")
414
414
415 self.undo_action = QtGui.QAction("&Undo",
415 self.undo_action = QtGui.QAction("&Undo",
416 self,
416 self,
417 shortcut=QtGui.QKeySequence.Undo,
417 shortcut=QtGui.QKeySequence.Undo,
418 statusTip="Undo last action if possible",
418 statusTip="Undo last action if possible",
419 triggered=self.undo_active_frontend
419 triggered=self.undo_active_frontend
420 )
420 )
421 self.add_menu_action(self.edit_menu, self.undo_action)
421 self.add_menu_action(self.edit_menu, self.undo_action)
422
422
423 self.redo_action = QtGui.QAction("&Redo",
423 self.redo_action = QtGui.QAction("&Redo",
424 self,
424 self,
425 shortcut=QtGui.QKeySequence.Redo,
425 shortcut=QtGui.QKeySequence.Redo,
426 statusTip="Redo last action if possible",
426 statusTip="Redo last action if possible",
427 triggered=self.redo_active_frontend)
427 triggered=self.redo_active_frontend)
428 self.add_menu_action(self.edit_menu, self.redo_action)
428 self.add_menu_action(self.edit_menu, self.redo_action)
429
429
430 self.edit_menu.addSeparator()
430 self.edit_menu.addSeparator()
431
431
432 self.cut_action = QtGui.QAction("&Cut",
432 self.cut_action = QtGui.QAction("&Cut",
433 self,
433 self,
434 shortcut=QtGui.QKeySequence.Cut,
434 shortcut=QtGui.QKeySequence.Cut,
435 triggered=self.cut_active_frontend
435 triggered=self.cut_active_frontend
436 )
436 )
437 self.add_menu_action(self.edit_menu, self.cut_action, True)
437 self.add_menu_action(self.edit_menu, self.cut_action, True)
438
438
439 self.copy_action = QtGui.QAction("&Copy",
439 self.copy_action = QtGui.QAction("&Copy",
440 self,
440 self,
441 shortcut=QtGui.QKeySequence.Copy,
441 shortcut=QtGui.QKeySequence.Copy,
442 triggered=self.copy_active_frontend
442 triggered=self.copy_active_frontend
443 )
443 )
444 self.add_menu_action(self.edit_menu, self.copy_action, True)
444 self.add_menu_action(self.edit_menu, self.copy_action, True)
445
445
446 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
446 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
447 self,
447 self,
448 shortcut="Ctrl+Shift+C",
448 shortcut="Ctrl+Shift+C",
449 triggered=self.copy_raw_active_frontend
449 triggered=self.copy_raw_active_frontend
450 )
450 )
451 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
451 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
452
452
453 self.paste_action = QtGui.QAction("&Paste",
453 self.paste_action = QtGui.QAction("&Paste",
454 self,
454 self,
455 shortcut=QtGui.QKeySequence.Paste,
455 shortcut=QtGui.QKeySequence.Paste,
456 triggered=self.paste_active_frontend
456 triggered=self.paste_active_frontend
457 )
457 )
458 self.add_menu_action(self.edit_menu, self.paste_action, True)
458 self.add_menu_action(self.edit_menu, self.paste_action, True)
459
459
460 self.edit_menu.addSeparator()
460 self.edit_menu.addSeparator()
461
461
462 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
462 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
463 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
463 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
464 # Only override the default if there is a collision.
464 # Only override the default if there is a collision.
465 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
465 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
466 selectall = "Ctrl+Shift+A"
466 selectall = "Ctrl+Shift+A"
467 self.select_all_action = QtGui.QAction("Select &All",
467 self.select_all_action = QtGui.QAction("Select &All",
468 self,
468 self,
469 shortcut=selectall,
469 shortcut=selectall,
470 triggered=self.select_all_active_frontend
470 triggered=self.select_all_active_frontend
471 )
471 )
472 self.add_menu_action(self.edit_menu, self.select_all_action, True)
472 self.add_menu_action(self.edit_menu, self.select_all_action, True)
473
473
474
474
475 def init_view_menu(self):
475 def init_view_menu(self):
476 self.view_menu = self.menuBar().addMenu("&View")
476 self.view_menu = self.menuBar().addMenu("&View")
477
477
478 if sys.platform != 'darwin':
478 if sys.platform != 'darwin':
479 # disable on OSX, where there is always a menu bar
479 # disable on OSX, where there is always a menu bar
480 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
480 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
481 self,
481 self,
482 shortcut="Ctrl+Shift+M",
482 shortcut="Ctrl+Shift+M",
483 statusTip="Toggle visibility of menubar",
483 statusTip="Toggle visibility of menubar",
484 triggered=self.toggle_menu_bar)
484 triggered=self.toggle_menu_bar)
485 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
485 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
486
486
487 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
487 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
488 self.full_screen_act = QtGui.QAction("&Full Screen",
488 self.full_screen_act = QtGui.QAction("&Full Screen",
489 self,
489 self,
490 shortcut=fs_key,
490 shortcut=fs_key,
491 statusTip="Toggle between Fullscreen and Normal Size",
491 statusTip="Toggle between Fullscreen and Normal Size",
492 triggered=self.toggleFullScreen)
492 triggered=self.toggleFullScreen)
493 self.add_menu_action(self.view_menu, self.full_screen_act)
493 self.add_menu_action(self.view_menu, self.full_screen_act)
494
494
495 self.view_menu.addSeparator()
495 self.view_menu.addSeparator()
496
496
497 self.increase_font_size = QtGui.QAction("Zoom &In",
497 self.increase_font_size = QtGui.QAction("Zoom &In",
498 self,
498 self,
499 shortcut=QtGui.QKeySequence.ZoomIn,
499 shortcut=QtGui.QKeySequence.ZoomIn,
500 triggered=self.increase_font_size_active_frontend
500 triggered=self.increase_font_size_active_frontend
501 )
501 )
502 self.add_menu_action(self.view_menu, self.increase_font_size, True)
502 self.add_menu_action(self.view_menu, self.increase_font_size, True)
503
503
504 self.decrease_font_size = QtGui.QAction("Zoom &Out",
504 self.decrease_font_size = QtGui.QAction("Zoom &Out",
505 self,
505 self,
506 shortcut=QtGui.QKeySequence.ZoomOut,
506 shortcut=QtGui.QKeySequence.ZoomOut,
507 triggered=self.decrease_font_size_active_frontend
507 triggered=self.decrease_font_size_active_frontend
508 )
508 )
509 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
509 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
510
510
511 self.reset_font_size = QtGui.QAction("Zoom &Reset",
511 self.reset_font_size = QtGui.QAction("Zoom &Reset",
512 self,
512 self,
513 shortcut="Ctrl+0",
513 shortcut="Ctrl+0",
514 triggered=self.reset_font_size_active_frontend
514 triggered=self.reset_font_size_active_frontend
515 )
515 )
516 self.add_menu_action(self.view_menu, self.reset_font_size, True)
516 self.add_menu_action(self.view_menu, self.reset_font_size, True)
517
517
518 self.view_menu.addSeparator()
518 self.view_menu.addSeparator()
519
519
520 self.clear_action = QtGui.QAction("&Clear Screen",
520 self.clear_action = QtGui.QAction("&Clear Screen",
521 self,
521 self,
522 shortcut='Ctrl+L',
522 shortcut='Ctrl+L',
523 statusTip="Clear the console",
523 statusTip="Clear the console",
524 triggered=self.clear_magic_active_frontend)
524 triggered=self.clear_magic_active_frontend)
525 self.add_menu_action(self.view_menu, self.clear_action)
525 self.add_menu_action(self.view_menu, self.clear_action)
526
526
527 self.pager_menu = self.view_menu.addMenu("&Pager")
527 self.pager_menu = self.view_menu.addMenu("&Pager")
528
528
529 hsplit_action = QtGui.QAction(".. &Horizontal Split",
529 hsplit_action = QtGui.QAction(".. &Horizontal Split",
530 self,
530 self,
531 triggered=lambda: self.set_paging_active_frontend('hsplit'))
531 triggered=lambda: self.set_paging_active_frontend('hsplit'))
532
532
533 vsplit_action = QtGui.QAction(" : &Vertical Split",
533 vsplit_action = QtGui.QAction(" : &Vertical Split",
534 self,
534 self,
535 triggered=lambda: self.set_paging_active_frontend('vsplit'))
535 triggered=lambda: self.set_paging_active_frontend('vsplit'))
536
536
537 inside_action = QtGui.QAction(" &Inside Pager",
537 inside_action = QtGui.QAction(" &Inside Pager",
538 self,
538 self,
539 triggered=lambda: self.set_paging_active_frontend('inside'))
539 triggered=lambda: self.set_paging_active_frontend('inside'))
540
540
541 self.pager_menu.addAction(hsplit_action)
541 self.pager_menu.addAction(hsplit_action)
542 self.pager_menu.addAction(vsplit_action)
542 self.pager_menu.addAction(vsplit_action)
543 self.pager_menu.addAction(inside_action)
543 self.pager_menu.addAction(inside_action)
544
544
545 def init_kernel_menu(self):
545 def init_kernel_menu(self):
546 self.kernel_menu = self.menuBar().addMenu("&Kernel")
546 self.kernel_menu = self.menuBar().addMenu("&Kernel")
547 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
547 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
548 # keep the signal shortcuts to ctrl, rather than
548 # keep the signal shortcuts to ctrl, rather than
549 # platform-default like we do elsewhere.
549 # platform-default like we do elsewhere.
550
550
551 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
551 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
552
552
553 self.interrupt_kernel_action = QtGui.QAction("&Interrupt current Kernel",
553 self.interrupt_kernel_action = QtGui.QAction("&Interrupt current Kernel",
554 self,
554 self,
555 triggered=self.interrupt_kernel_active_frontend,
555 triggered=self.interrupt_kernel_active_frontend,
556 shortcut=ctrl+"+C",
556 shortcut=ctrl+"+C",
557 )
557 )
558 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
558 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
559
559
560 self.restart_kernel_action = QtGui.QAction("&Restart current Kernel",
560 self.restart_kernel_action = QtGui.QAction("&Restart current Kernel",
561 self,
561 self,
562 triggered=self.restart_kernel_active_frontend,
562 triggered=self.restart_kernel_active_frontend,
563 shortcut=ctrl+"+.",
563 shortcut=ctrl+"+.",
564 )
564 )
565 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
565 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
566
566
567 self.kernel_menu.addSeparator()
567 self.kernel_menu.addSeparator()
568
568
569 self.confirm_restart_kernel_action = QtGui.QAction("&Confirm kernel restart",
569 self.confirm_restart_kernel_action = QtGui.QAction("&Confirm kernel restart",
570 self,
570 self,
571 checkable=True,
571 checkable=True,
572 checked=self.active_frontend.confirm_restart,
572 checked=self.active_frontend.confirm_restart,
573 triggered=self.toggle_confirm_restart_active_frontend
573 triggered=self.toggle_confirm_restart_active_frontend
574 )
574 )
575
575
576 self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action)
576 self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action)
577 self.tab_widget.currentChanged.connect(self.update_restart_checkbox)
577 self.tab_widget.currentChanged.connect(self.update_restart_checkbox)
578
578
579 def init_magic_menu(self):
579 def init_magic_menu(self):
580 self.magic_menu = self.menuBar().addMenu("&Magic")
580 self.magic_menu = self.menuBar().addMenu("&Magic")
581
581
582 self.add_menu_action(self.magic_menu,
582 self.add_menu_action(self.magic_menu,
583 self.magic_helper.toggleViewAction())
583 self.magic_helper.toggleViewAction())
584
584
585 self.magic_menu_separator = self.magic_menu.addSeparator()
585 self.magic_menu_separator = self.magic_menu.addSeparator()
586
586
587 self.reset_action = QtGui.QAction("&Reset",
587 self.reset_action = QtGui.QAction("&Reset",
588 self,
588 self,
589 statusTip="Clear all variables from workspace",
589 statusTip="Clear all variables from workspace",
590 triggered=self.reset_magic_active_frontend)
590 triggered=self.reset_magic_active_frontend)
591 self.add_menu_action(self.magic_menu, self.reset_action)
591 self.add_menu_action(self.magic_menu, self.reset_action)
592
592
593 self.history_action = QtGui.QAction("&History",
593 self.history_action = QtGui.QAction("&History",
594 self,
594 self,
595 statusTip="show command history",
595 statusTip="show command history",
596 triggered=self.history_magic_active_frontend)
596 triggered=self.history_magic_active_frontend)
597 self.add_menu_action(self.magic_menu, self.history_action)
597 self.add_menu_action(self.magic_menu, self.history_action)
598
598
599 self.save_action = QtGui.QAction("E&xport History ",
599 self.save_action = QtGui.QAction("E&xport History ",
600 self,
600 self,
601 statusTip="Export History as Python File",
601 statusTip="Export History as Python File",
602 triggered=self.save_magic_active_frontend)
602 triggered=self.save_magic_active_frontend)
603 self.add_menu_action(self.magic_menu, self.save_action)
603 self.add_menu_action(self.magic_menu, self.save_action)
604
604
605 self.who_action = QtGui.QAction("&Who",
605 self.who_action = QtGui.QAction("&Who",
606 self,
606 self,
607 statusTip="List interactive variables",
607 statusTip="List interactive variables",
608 triggered=self.who_magic_active_frontend)
608 triggered=self.who_magic_active_frontend)
609 self.add_menu_action(self.magic_menu, self.who_action)
609 self.add_menu_action(self.magic_menu, self.who_action)
610
610
611 self.who_ls_action = QtGui.QAction("Wh&o ls",
611 self.who_ls_action = QtGui.QAction("Wh&o ls",
612 self,
612 self,
613 statusTip="Return a list of interactive variables",
613 statusTip="Return a list of interactive variables",
614 triggered=self.who_ls_magic_active_frontend)
614 triggered=self.who_ls_magic_active_frontend)
615 self.add_menu_action(self.magic_menu, self.who_ls_action)
615 self.add_menu_action(self.magic_menu, self.who_ls_action)
616
616
617 self.whos_action = QtGui.QAction("Who&s",
617 self.whos_action = QtGui.QAction("Who&s",
618 self,
618 self,
619 statusTip="List interactive variables with details",
619 statusTip="List interactive variables with details",
620 triggered=self.whos_magic_active_frontend)
620 triggered=self.whos_magic_active_frontend)
621 self.add_menu_action(self.magic_menu, self.whos_action)
621 self.add_menu_action(self.magic_menu, self.whos_action)
622
622
623 def init_window_menu(self):
623 def init_window_menu(self):
624 self.window_menu = self.menuBar().addMenu("&Window")
624 self.window_menu = self.menuBar().addMenu("&Window")
625 if sys.platform == 'darwin':
625 if sys.platform == 'darwin':
626 # add min/maximize actions to OSX, which lacks default bindings.
626 # add min/maximize actions to OSX, which lacks default bindings.
627 self.minimizeAct = QtGui.QAction("Mini&mize",
627 self.minimizeAct = QtGui.QAction("Mini&mize",
628 self,
628 self,
629 shortcut="Ctrl+m",
629 shortcut="Ctrl+m",
630 statusTip="Minimize the window/Restore Normal Size",
630 statusTip="Minimize the window/Restore Normal Size",
631 triggered=self.toggleMinimized)
631 triggered=self.toggleMinimized)
632 # maximize is called 'Zoom' on OSX for some reason
632 # maximize is called 'Zoom' on OSX for some reason
633 self.maximizeAct = QtGui.QAction("&Zoom",
633 self.maximizeAct = QtGui.QAction("&Zoom",
634 self,
634 self,
635 shortcut="Ctrl+Shift+M",
635 shortcut="Ctrl+Shift+M",
636 statusTip="Maximize the window/Restore Normal Size",
636 statusTip="Maximize the window/Restore Normal Size",
637 triggered=self.toggleMaximized)
637 triggered=self.toggleMaximized)
638
638
639 self.add_menu_action(self.window_menu, self.minimizeAct)
639 self.add_menu_action(self.window_menu, self.minimizeAct)
640 self.add_menu_action(self.window_menu, self.maximizeAct)
640 self.add_menu_action(self.window_menu, self.maximizeAct)
641 self.window_menu.addSeparator()
641 self.window_menu.addSeparator()
642
642
643 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
643 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
644 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
644 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
645 self,
645 self,
646 shortcut=prev_key,
646 shortcut=prev_key,
647 statusTip="Select previous tab",
647 statusTip="Select previous tab",
648 triggered=self.prev_tab)
648 triggered=self.prev_tab)
649 self.add_menu_action(self.window_menu, self.prev_tab_act)
649 self.add_menu_action(self.window_menu, self.prev_tab_act)
650
650
651 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
651 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
652 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
652 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
653 self,
653 self,
654 shortcut=next_key,
654 shortcut=next_key,
655 statusTip="Select next tab",
655 statusTip="Select next tab",
656 triggered=self.next_tab)
656 triggered=self.next_tab)
657 self.add_menu_action(self.window_menu, self.next_tab_act)
657 self.add_menu_action(self.window_menu, self.next_tab_act)
658
658
659 def init_help_menu(self):
659 def init_help_menu(self):
660 # please keep the Help menu in Mac Os even if empty. It will
660 # please keep the Help menu in Mac Os even if empty. It will
661 # automatically contain a search field to search inside menus and
661 # automatically contain a search field to search inside menus and
662 # please keep it spelled in English, as long as Qt Doesn't support
662 # please keep it spelled in English, as long as Qt Doesn't support
663 # a QAction.MenuRole like HelpMenuRole otherwise it will lose
663 # a QAction.MenuRole like HelpMenuRole otherwise it will lose
664 # this search field functionality
664 # this search field functionality
665
665
666 self.help_menu = self.menuBar().addMenu("&Help")
666 self.help_menu = self.menuBar().addMenu("&Help")
667
667
668
668
669 # Help Menu
669 # Help Menu
670
670
671 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
671 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
672 self,
672 self,
673 triggered=self.intro_active_frontend
673 triggered=self.intro_active_frontend
674 )
674 )
675 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
675 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
676
676
677 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
677 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
678 self,
678 self,
679 triggered=self.quickref_active_frontend
679 triggered=self.quickref_active_frontend
680 )
680 )
681 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
681 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
682
682
683 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
683 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
684 self,
684 self,
685 triggered=self.guiref_active_frontend
685 triggered=self.guiref_active_frontend
686 )
686 )
687 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
687 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
688
688
689 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
689 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
690 self,
690 self,
691 triggered=self._open_online_help)
691 triggered=self._open_online_help)
692 self.add_menu_action(self.help_menu, self.onlineHelpAct)
692 self.add_menu_action(self.help_menu, self.onlineHelpAct)
693
693
694 def init_magic_helper(self):
694 def init_magic_helper(self):
695 from .magic_helper import MagicHelper
695 from .magic_helper import MagicHelper
696
696
697 self.magic_helper = MagicHelper("Show Magics", self)
697 self.magic_helper = MagicHelper("Show Magics", self)
698
698
699 self.magic_helper.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
699 self.magic_helper.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
700 QtCore.Qt.RightDockWidgetArea)
700 QtCore.Qt.RightDockWidgetArea)
701 self.magic_helper.setVisible(False)
701 self.magic_helper.setVisible(False)
702
702
703 self.magic_helper.pasteRequested[str].connect(
703 self.magic_helper.pasteRequested[str].connect(
704 self.magic_helper_paste_requested
704 self.magic_helper_paste_requested
705 )
705 )
706 self.magic_helper.runRequested[str].connect(
706 self.magic_helper.runRequested[str].connect(
707 self.magic_helper_run_requested
707 self.magic_helper_run_requested
708 )
708 )
709 self.magic_helper.readyForUpdate.connect(
709 self.magic_helper.readyForUpdate.connect(
710 self.magic_helper_update_requested
710 self.magic_helper_update_requested
711 )
711 )
712
712
713 self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.magic_helper)
713 self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.magic_helper)
714
714
715 def _set_active_frontend_focus(self):
715 def _set_active_frontend_focus(self):
716 # this is a hack, self.active_frontend._control seems to be
716 # this is a hack, self.active_frontend._control seems to be
717 # a private member. Unfortunately this is the only method
717 # a private member. Unfortunately this is the only method
718 # to set focus reliably
718 # to set focus reliably
719 QtCore.QTimer.singleShot(200, self.active_frontend._control.setFocus)
719 QtCore.QTimer.singleShot(200, self.active_frontend._control.setFocus)
720
720
721 def magic_helper_paste_requested(self, text = None):
721 def magic_helper_paste_requested(self, text = None):
722 if text != None:
722 if text is not None:
723 self.active_frontend.input_buffer = text
723 self.active_frontend.input_buffer = text
724 self._set_active_frontend_focus()
724 self._set_active_frontend_focus()
725
725
726 def magic_helper_run_requested(self, text = None):
726 def magic_helper_run_requested(self, text = None):
727 if text != None:
727 if text is not None:
728 self.active_frontend.execute(text)
728 self.active_frontend.execute(text)
729 self._set_active_frontend_focus()
729 self._set_active_frontend_focus()
730
730
731 def magic_helper_update_requested(self):
731 def magic_helper_update_requested(self):
732 def _handle_data(data):
732 def _handle_data(data):
733 if not data:
733 if not data:
734 return
734 return
735
735
736 if data['status'] != 'ok':
736 if data['status'] != 'ok':
737 self.log.warn(
737 self.log.warn(
738 "%%lsmagic user-expression failed: {}".format(data)
738 "%%lsmagic user-expression failed: {}".format(data)
739 )
739 )
740 return
740 return
741 self.magic_helper.populate_magic_helper(data)
741 self.magic_helper.populate_magic_helper(data)
742
742
743 self.active_frontend._silent_exec_callback(
743 self.active_frontend._silent_exec_callback(
744 'get_ipython().magic("lsmagic")',
744 'get_ipython().magic("lsmagic")',
745 _handle_data
745 _handle_data
746 )
746 )
747
747
748 # minimize/maximize/fullscreen actions:
748 # minimize/maximize/fullscreen actions:
749
749
750 def toggle_menu_bar(self):
750 def toggle_menu_bar(self):
751 menu_bar = self.menuBar()
751 menu_bar = self.menuBar()
752 if menu_bar.isVisible():
752 if menu_bar.isVisible():
753 menu_bar.setVisible(False)
753 menu_bar.setVisible(False)
754 else:
754 else:
755 menu_bar.setVisible(True)
755 menu_bar.setVisible(True)
756
756
757 def toggleMinimized(self):
757 def toggleMinimized(self):
758 if not self.isMinimized():
758 if not self.isMinimized():
759 self.showMinimized()
759 self.showMinimized()
760 else:
760 else:
761 self.showNormal()
761 self.showNormal()
762
762
763 def _open_online_help(self):
763 def _open_online_help(self):
764 filename="http://ipython.org/ipython-doc/stable/index.html"
764 filename="http://ipython.org/ipython-doc/stable/index.html"
765 webbrowser.open(filename, new=1, autoraise=True)
765 webbrowser.open(filename, new=1, autoraise=True)
766
766
767 def toggleMaximized(self):
767 def toggleMaximized(self):
768 if not self.isMaximized():
768 if not self.isMaximized():
769 self.showMaximized()
769 self.showMaximized()
770 else:
770 else:
771 self.showNormal()
771 self.showNormal()
772
772
773 # Min/Max imizing while in full screen give a bug
773 # Min/Max imizing while in full screen give a bug
774 # when going out of full screen, at least on OSX
774 # when going out of full screen, at least on OSX
775 def toggleFullScreen(self):
775 def toggleFullScreen(self):
776 if not self.isFullScreen():
776 if not self.isFullScreen():
777 self.showFullScreen()
777 self.showFullScreen()
778 if sys.platform == 'darwin':
778 if sys.platform == 'darwin':
779 self.maximizeAct.setEnabled(False)
779 self.maximizeAct.setEnabled(False)
780 self.minimizeAct.setEnabled(False)
780 self.minimizeAct.setEnabled(False)
781 else:
781 else:
782 self.showNormal()
782 self.showNormal()
783 if sys.platform == 'darwin':
783 if sys.platform == 'darwin':
784 self.maximizeAct.setEnabled(True)
784 self.maximizeAct.setEnabled(True)
785 self.minimizeAct.setEnabled(True)
785 self.minimizeAct.setEnabled(True)
786
786
787 def set_paging_active_frontend(self, paging):
787 def set_paging_active_frontend(self, paging):
788 self.active_frontend._set_paging(paging)
788 self.active_frontend._set_paging(paging)
789
789
790 def close_active_frontend(self):
790 def close_active_frontend(self):
791 self.close_tab(self.active_frontend)
791 self.close_tab(self.active_frontend)
792
792
793 def restart_kernel_active_frontend(self):
793 def restart_kernel_active_frontend(self):
794 self.active_frontend.request_restart_kernel()
794 self.active_frontend.request_restart_kernel()
795
795
796 def interrupt_kernel_active_frontend(self):
796 def interrupt_kernel_active_frontend(self):
797 self.active_frontend.request_interrupt_kernel()
797 self.active_frontend.request_interrupt_kernel()
798
798
799 def toggle_confirm_restart_active_frontend(self):
799 def toggle_confirm_restart_active_frontend(self):
800 widget = self.active_frontend
800 widget = self.active_frontend
801 widget.confirm_restart = not widget.confirm_restart
801 widget.confirm_restart = not widget.confirm_restart
802 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
802 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
803
803
804 def update_restart_checkbox(self):
804 def update_restart_checkbox(self):
805 if self.active_frontend is None:
805 if self.active_frontend is None:
806 return
806 return
807 widget = self.active_frontend
807 widget = self.active_frontend
808 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
808 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
809
809
810 def cut_active_frontend(self):
810 def cut_active_frontend(self):
811 widget = self.active_frontend
811 widget = self.active_frontend
812 if widget.can_cut():
812 if widget.can_cut():
813 widget.cut()
813 widget.cut()
814
814
815 def copy_active_frontend(self):
815 def copy_active_frontend(self):
816 widget = self.active_frontend
816 widget = self.active_frontend
817 widget.copy()
817 widget.copy()
818
818
819 def copy_raw_active_frontend(self):
819 def copy_raw_active_frontend(self):
820 self.active_frontend._copy_raw_action.trigger()
820 self.active_frontend._copy_raw_action.trigger()
821
821
822 def paste_active_frontend(self):
822 def paste_active_frontend(self):
823 widget = self.active_frontend
823 widget = self.active_frontend
824 if widget.can_paste():
824 if widget.can_paste():
825 widget.paste()
825 widget.paste()
826
826
827 def undo_active_frontend(self):
827 def undo_active_frontend(self):
828 self.active_frontend.undo()
828 self.active_frontend.undo()
829
829
830 def redo_active_frontend(self):
830 def redo_active_frontend(self):
831 self.active_frontend.redo()
831 self.active_frontend.redo()
832
832
833 def reset_magic_active_frontend(self):
833 def reset_magic_active_frontend(self):
834 self.active_frontend.execute("%reset")
834 self.active_frontend.execute("%reset")
835
835
836 def history_magic_active_frontend(self):
836 def history_magic_active_frontend(self):
837 self.active_frontend.execute("%history")
837 self.active_frontend.execute("%history")
838
838
839 def save_magic_active_frontend(self):
839 def save_magic_active_frontend(self):
840 self.active_frontend.save_magic()
840 self.active_frontend.save_magic()
841
841
842 def clear_magic_active_frontend(self):
842 def clear_magic_active_frontend(self):
843 self.active_frontend.execute("%clear")
843 self.active_frontend.execute("%clear")
844
844
845 def who_magic_active_frontend(self):
845 def who_magic_active_frontend(self):
846 self.active_frontend.execute("%who")
846 self.active_frontend.execute("%who")
847
847
848 def who_ls_magic_active_frontend(self):
848 def who_ls_magic_active_frontend(self):
849 self.active_frontend.execute("%who_ls")
849 self.active_frontend.execute("%who_ls")
850
850
851 def whos_magic_active_frontend(self):
851 def whos_magic_active_frontend(self):
852 self.active_frontend.execute("%whos")
852 self.active_frontend.execute("%whos")
853
853
854 def print_action_active_frontend(self):
854 def print_action_active_frontend(self):
855 self.active_frontend.print_action.trigger()
855 self.active_frontend.print_action.trigger()
856
856
857 def export_action_active_frontend(self):
857 def export_action_active_frontend(self):
858 self.active_frontend.export_action.trigger()
858 self.active_frontend.export_action.trigger()
859
859
860 def select_all_active_frontend(self):
860 def select_all_active_frontend(self):
861 self.active_frontend.select_all_action.trigger()
861 self.active_frontend.select_all_action.trigger()
862
862
863 def increase_font_size_active_frontend(self):
863 def increase_font_size_active_frontend(self):
864 self.active_frontend.increase_font_size.trigger()
864 self.active_frontend.increase_font_size.trigger()
865
865
866 def decrease_font_size_active_frontend(self):
866 def decrease_font_size_active_frontend(self):
867 self.active_frontend.decrease_font_size.trigger()
867 self.active_frontend.decrease_font_size.trigger()
868
868
869 def reset_font_size_active_frontend(self):
869 def reset_font_size_active_frontend(self):
870 self.active_frontend.reset_font_size.trigger()
870 self.active_frontend.reset_font_size.trigger()
871
871
872 def guiref_active_frontend(self):
872 def guiref_active_frontend(self):
873 self.active_frontend.execute("%guiref")
873 self.active_frontend.execute("%guiref")
874
874
875 def intro_active_frontend(self):
875 def intro_active_frontend(self):
876 self.active_frontend.execute("?")
876 self.active_frontend.execute("?")
877
877
878 def quickref_active_frontend(self):
878 def quickref_active_frontend(self):
879 self.active_frontend.execute("%quickref")
879 self.active_frontend.execute("%quickref")
880 #---------------------------------------------------------------------------
880 #---------------------------------------------------------------------------
881 # QWidget interface
881 # QWidget interface
882 #---------------------------------------------------------------------------
882 #---------------------------------------------------------------------------
883
883
884 def closeEvent(self, event):
884 def closeEvent(self, event):
885 """ Forward the close event to every tabs contained by the windows
885 """ Forward the close event to every tabs contained by the windows
886 """
886 """
887 if self.tab_widget.count() == 0:
887 if self.tab_widget.count() == 0:
888 # no tabs, just close
888 # no tabs, just close
889 event.accept()
889 event.accept()
890 return
890 return
891 # Do Not loop on the widget count as it change while closing
891 # Do Not loop on the widget count as it change while closing
892 title = self.window().windowTitle()
892 title = self.window().windowTitle()
893 cancel = QtGui.QMessageBox.Cancel
893 cancel = QtGui.QMessageBox.Cancel
894 okay = QtGui.QMessageBox.Ok
894 okay = QtGui.QMessageBox.Ok
895 accept_role = QtGui.QMessageBox.AcceptRole
895 accept_role = QtGui.QMessageBox.AcceptRole
896
896
897 if self.confirm_exit:
897 if self.confirm_exit:
898 if self.tab_widget.count() > 1:
898 if self.tab_widget.count() > 1:
899 msg = "Close all tabs, stop all kernels, and Quit?"
899 msg = "Close all tabs, stop all kernels, and Quit?"
900 else:
900 else:
901 msg = "Close console, stop kernel, and Quit?"
901 msg = "Close console, stop kernel, and Quit?"
902 info = "Kernels not started here (e.g. notebooks) will be left alone."
902 info = "Kernels not started here (e.g. notebooks) will be left alone."
903 closeall = QtGui.QPushButton("&Quit", self)
903 closeall = QtGui.QPushButton("&Quit", self)
904 closeall.setShortcut('Q')
904 closeall.setShortcut('Q')
905 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
905 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
906 title, msg)
906 title, msg)
907 box.setInformativeText(info)
907 box.setInformativeText(info)
908 box.addButton(cancel)
908 box.addButton(cancel)
909 box.addButton(closeall, QtGui.QMessageBox.YesRole)
909 box.addButton(closeall, QtGui.QMessageBox.YesRole)
910 box.setDefaultButton(closeall)
910 box.setDefaultButton(closeall)
911 box.setEscapeButton(cancel)
911 box.setEscapeButton(cancel)
912 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
912 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
913 box.setIconPixmap(pixmap)
913 box.setIconPixmap(pixmap)
914 reply = box.exec_()
914 reply = box.exec_()
915 else:
915 else:
916 reply = okay
916 reply = okay
917
917
918 if reply == cancel:
918 if reply == cancel:
919 event.ignore()
919 event.ignore()
920 return
920 return
921 if reply == okay or reply == accept_role:
921 if reply == okay or reply == accept_role:
922 while self.tab_widget.count() >= 1:
922 while self.tab_widget.count() >= 1:
923 # prevent further confirmations:
923 # prevent further confirmations:
924 widget = self.active_frontend
924 widget = self.active_frontend
925 widget._confirm_exit = False
925 widget._confirm_exit = False
926 self.close_tab(widget)
926 self.close_tab(widget)
927 event.accept()
927 event.accept()
928
928
@@ -1,577 +1,577 b''
1 """Windows-specific implementation of process utilities with direct WinAPI.
1 """Windows-specific implementation of process utilities with direct WinAPI.
2
2
3 This file is meant to be used by process.py
3 This file is meant to be used by process.py
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team
7 # Copyright (C) 2010-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 from __future__ import print_function
13 from __future__ import print_function
14
14
15 # stdlib
15 # stdlib
16 import os, sys, threading
16 import os, sys, threading
17 import ctypes, msvcrt
17 import ctypes, msvcrt
18
18
19 # local imports
19 # local imports
20 from . import py3compat
20 from . import py3compat
21
21
22 # Win32 API types needed for the API calls
22 # Win32 API types needed for the API calls
23 from ctypes import POINTER
23 from ctypes import POINTER
24 from ctypes.wintypes import HANDLE, HLOCAL, LPVOID, WORD, DWORD, BOOL, \
24 from ctypes.wintypes import HANDLE, HLOCAL, LPVOID, WORD, DWORD, BOOL, \
25 ULONG, LPCWSTR
25 ULONG, LPCWSTR
26 LPDWORD = POINTER(DWORD)
26 LPDWORD = POINTER(DWORD)
27 LPHANDLE = POINTER(HANDLE)
27 LPHANDLE = POINTER(HANDLE)
28 ULONG_PTR = POINTER(ULONG)
28 ULONG_PTR = POINTER(ULONG)
29 class SECURITY_ATTRIBUTES(ctypes.Structure):
29 class SECURITY_ATTRIBUTES(ctypes.Structure):
30 _fields_ = [("nLength", DWORD),
30 _fields_ = [("nLength", DWORD),
31 ("lpSecurityDescriptor", LPVOID),
31 ("lpSecurityDescriptor", LPVOID),
32 ("bInheritHandle", BOOL)]
32 ("bInheritHandle", BOOL)]
33 LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES)
33 LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES)
34 class STARTUPINFO(ctypes.Structure):
34 class STARTUPINFO(ctypes.Structure):
35 _fields_ = [("cb", DWORD),
35 _fields_ = [("cb", DWORD),
36 ("lpReserved", LPCWSTR),
36 ("lpReserved", LPCWSTR),
37 ("lpDesktop", LPCWSTR),
37 ("lpDesktop", LPCWSTR),
38 ("lpTitle", LPCWSTR),
38 ("lpTitle", LPCWSTR),
39 ("dwX", DWORD),
39 ("dwX", DWORD),
40 ("dwY", DWORD),
40 ("dwY", DWORD),
41 ("dwXSize", DWORD),
41 ("dwXSize", DWORD),
42 ("dwYSize", DWORD),
42 ("dwYSize", DWORD),
43 ("dwXCountChars", DWORD),
43 ("dwXCountChars", DWORD),
44 ("dwYCountChars", DWORD),
44 ("dwYCountChars", DWORD),
45 ("dwFillAttribute", DWORD),
45 ("dwFillAttribute", DWORD),
46 ("dwFlags", DWORD),
46 ("dwFlags", DWORD),
47 ("wShowWindow", WORD),
47 ("wShowWindow", WORD),
48 ("cbReserved2", WORD),
48 ("cbReserved2", WORD),
49 ("lpReserved2", LPVOID),
49 ("lpReserved2", LPVOID),
50 ("hStdInput", HANDLE),
50 ("hStdInput", HANDLE),
51 ("hStdOutput", HANDLE),
51 ("hStdOutput", HANDLE),
52 ("hStdError", HANDLE)]
52 ("hStdError", HANDLE)]
53 LPSTARTUPINFO = POINTER(STARTUPINFO)
53 LPSTARTUPINFO = POINTER(STARTUPINFO)
54 class PROCESS_INFORMATION(ctypes.Structure):
54 class PROCESS_INFORMATION(ctypes.Structure):
55 _fields_ = [("hProcess", HANDLE),
55 _fields_ = [("hProcess", HANDLE),
56 ("hThread", HANDLE),
56 ("hThread", HANDLE),
57 ("dwProcessId", DWORD),
57 ("dwProcessId", DWORD),
58 ("dwThreadId", DWORD)]
58 ("dwThreadId", DWORD)]
59 LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
59 LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
60
60
61 # Win32 API constants needed
61 # Win32 API constants needed
62 ERROR_HANDLE_EOF = 38
62 ERROR_HANDLE_EOF = 38
63 ERROR_BROKEN_PIPE = 109
63 ERROR_BROKEN_PIPE = 109
64 ERROR_NO_DATA = 232
64 ERROR_NO_DATA = 232
65 HANDLE_FLAG_INHERIT = 0x0001
65 HANDLE_FLAG_INHERIT = 0x0001
66 STARTF_USESTDHANDLES = 0x0100
66 STARTF_USESTDHANDLES = 0x0100
67 CREATE_SUSPENDED = 0x0004
67 CREATE_SUSPENDED = 0x0004
68 CREATE_NEW_CONSOLE = 0x0010
68 CREATE_NEW_CONSOLE = 0x0010
69 CREATE_NO_WINDOW = 0x08000000
69 CREATE_NO_WINDOW = 0x08000000
70 STILL_ACTIVE = 259
70 STILL_ACTIVE = 259
71 WAIT_TIMEOUT = 0x0102
71 WAIT_TIMEOUT = 0x0102
72 WAIT_FAILED = 0xFFFFFFFF
72 WAIT_FAILED = 0xFFFFFFFF
73 INFINITE = 0xFFFFFFFF
73 INFINITE = 0xFFFFFFFF
74 DUPLICATE_SAME_ACCESS = 0x00000002
74 DUPLICATE_SAME_ACCESS = 0x00000002
75 ENABLE_ECHO_INPUT = 0x0004
75 ENABLE_ECHO_INPUT = 0x0004
76 ENABLE_LINE_INPUT = 0x0002
76 ENABLE_LINE_INPUT = 0x0002
77 ENABLE_PROCESSED_INPUT = 0x0001
77 ENABLE_PROCESSED_INPUT = 0x0001
78
78
79 # Win32 API functions needed
79 # Win32 API functions needed
80 GetLastError = ctypes.windll.kernel32.GetLastError
80 GetLastError = ctypes.windll.kernel32.GetLastError
81 GetLastError.argtypes = []
81 GetLastError.argtypes = []
82 GetLastError.restype = DWORD
82 GetLastError.restype = DWORD
83
83
84 CreateFile = ctypes.windll.kernel32.CreateFileW
84 CreateFile = ctypes.windll.kernel32.CreateFileW
85 CreateFile.argtypes = [LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE]
85 CreateFile.argtypes = [LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE]
86 CreateFile.restype = HANDLE
86 CreateFile.restype = HANDLE
87
87
88 CreatePipe = ctypes.windll.kernel32.CreatePipe
88 CreatePipe = ctypes.windll.kernel32.CreatePipe
89 CreatePipe.argtypes = [POINTER(HANDLE), POINTER(HANDLE),
89 CreatePipe.argtypes = [POINTER(HANDLE), POINTER(HANDLE),
90 LPSECURITY_ATTRIBUTES, DWORD]
90 LPSECURITY_ATTRIBUTES, DWORD]
91 CreatePipe.restype = BOOL
91 CreatePipe.restype = BOOL
92
92
93 CreateProcess = ctypes.windll.kernel32.CreateProcessW
93 CreateProcess = ctypes.windll.kernel32.CreateProcessW
94 CreateProcess.argtypes = [LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES,
94 CreateProcess.argtypes = [LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES,
95 LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFO,
95 LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFO,
96 LPPROCESS_INFORMATION]
96 LPPROCESS_INFORMATION]
97 CreateProcess.restype = BOOL
97 CreateProcess.restype = BOOL
98
98
99 GetExitCodeProcess = ctypes.windll.kernel32.GetExitCodeProcess
99 GetExitCodeProcess = ctypes.windll.kernel32.GetExitCodeProcess
100 GetExitCodeProcess.argtypes = [HANDLE, LPDWORD]
100 GetExitCodeProcess.argtypes = [HANDLE, LPDWORD]
101 GetExitCodeProcess.restype = BOOL
101 GetExitCodeProcess.restype = BOOL
102
102
103 GetCurrentProcess = ctypes.windll.kernel32.GetCurrentProcess
103 GetCurrentProcess = ctypes.windll.kernel32.GetCurrentProcess
104 GetCurrentProcess.argtypes = []
104 GetCurrentProcess.argtypes = []
105 GetCurrentProcess.restype = HANDLE
105 GetCurrentProcess.restype = HANDLE
106
106
107 ResumeThread = ctypes.windll.kernel32.ResumeThread
107 ResumeThread = ctypes.windll.kernel32.ResumeThread
108 ResumeThread.argtypes = [HANDLE]
108 ResumeThread.argtypes = [HANDLE]
109 ResumeThread.restype = DWORD
109 ResumeThread.restype = DWORD
110
110
111 ReadFile = ctypes.windll.kernel32.ReadFile
111 ReadFile = ctypes.windll.kernel32.ReadFile
112 ReadFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
112 ReadFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
113 ReadFile.restype = BOOL
113 ReadFile.restype = BOOL
114
114
115 WriteFile = ctypes.windll.kernel32.WriteFile
115 WriteFile = ctypes.windll.kernel32.WriteFile
116 WriteFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
116 WriteFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
117 WriteFile.restype = BOOL
117 WriteFile.restype = BOOL
118
118
119 GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode
119 GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode
120 GetConsoleMode.argtypes = [HANDLE, LPDWORD]
120 GetConsoleMode.argtypes = [HANDLE, LPDWORD]
121 GetConsoleMode.restype = BOOL
121 GetConsoleMode.restype = BOOL
122
122
123 SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode
123 SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode
124 SetConsoleMode.argtypes = [HANDLE, DWORD]
124 SetConsoleMode.argtypes = [HANDLE, DWORD]
125 SetConsoleMode.restype = BOOL
125 SetConsoleMode.restype = BOOL
126
126
127 FlushConsoleInputBuffer = ctypes.windll.kernel32.FlushConsoleInputBuffer
127 FlushConsoleInputBuffer = ctypes.windll.kernel32.FlushConsoleInputBuffer
128 FlushConsoleInputBuffer.argtypes = [HANDLE]
128 FlushConsoleInputBuffer.argtypes = [HANDLE]
129 FlushConsoleInputBuffer.restype = BOOL
129 FlushConsoleInputBuffer.restype = BOOL
130
130
131 WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
131 WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
132 WaitForSingleObject.argtypes = [HANDLE, DWORD]
132 WaitForSingleObject.argtypes = [HANDLE, DWORD]
133 WaitForSingleObject.restype = DWORD
133 WaitForSingleObject.restype = DWORD
134
134
135 DuplicateHandle = ctypes.windll.kernel32.DuplicateHandle
135 DuplicateHandle = ctypes.windll.kernel32.DuplicateHandle
136 DuplicateHandle.argtypes = [HANDLE, HANDLE, HANDLE, LPHANDLE,
136 DuplicateHandle.argtypes = [HANDLE, HANDLE, HANDLE, LPHANDLE,
137 DWORD, BOOL, DWORD]
137 DWORD, BOOL, DWORD]
138 DuplicateHandle.restype = BOOL
138 DuplicateHandle.restype = BOOL
139
139
140 SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation
140 SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation
141 SetHandleInformation.argtypes = [HANDLE, DWORD, DWORD]
141 SetHandleInformation.argtypes = [HANDLE, DWORD, DWORD]
142 SetHandleInformation.restype = BOOL
142 SetHandleInformation.restype = BOOL
143
143
144 CloseHandle = ctypes.windll.kernel32.CloseHandle
144 CloseHandle = ctypes.windll.kernel32.CloseHandle
145 CloseHandle.argtypes = [HANDLE]
145 CloseHandle.argtypes = [HANDLE]
146 CloseHandle.restype = BOOL
146 CloseHandle.restype = BOOL
147
147
148 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
148 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
149 CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(ctypes.c_int)]
149 CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(ctypes.c_int)]
150 CommandLineToArgvW.restype = POINTER(LPCWSTR)
150 CommandLineToArgvW.restype = POINTER(LPCWSTR)
151
151
152 LocalFree = ctypes.windll.kernel32.LocalFree
152 LocalFree = ctypes.windll.kernel32.LocalFree
153 LocalFree.argtypes = [HLOCAL]
153 LocalFree.argtypes = [HLOCAL]
154 LocalFree.restype = HLOCAL
154 LocalFree.restype = HLOCAL
155
155
156 class AvoidUNCPath(object):
156 class AvoidUNCPath(object):
157 """A context manager to protect command execution from UNC paths.
157 """A context manager to protect command execution from UNC paths.
158
158
159 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
159 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
160 This context manager temporarily changes directory to the 'C:' drive on
160 This context manager temporarily changes directory to the 'C:' drive on
161 entering, and restores the original working directory on exit.
161 entering, and restores the original working directory on exit.
162
162
163 The context manager returns the starting working directory *if* it made a
163 The context manager returns the starting working directory *if* it made a
164 change and None otherwise, so that users can apply the necessary adjustment
164 change and None otherwise, so that users can apply the necessary adjustment
165 to their system calls in the event of a change.
165 to their system calls in the event of a change.
166
166
167 Examples
167 Examples
168 --------
168 --------
169 ::
169 ::
170 cmd = 'dir'
170 cmd = 'dir'
171 with AvoidUNCPath() as path:
171 with AvoidUNCPath() as path:
172 if path is not None:
172 if path is not None:
173 cmd = '"pushd %s &&"%s' % (path, cmd)
173 cmd = '"pushd %s &&"%s' % (path, cmd)
174 os.system(cmd)
174 os.system(cmd)
175 """
175 """
176 def __enter__(self):
176 def __enter__(self):
177 self.path = py3compat.getcwd()
177 self.path = py3compat.getcwd()
178 self.is_unc_path = self.path.startswith(r"\\")
178 self.is_unc_path = self.path.startswith(r"\\")
179 if self.is_unc_path:
179 if self.is_unc_path:
180 # change to c drive (as cmd.exe cannot handle UNC addresses)
180 # change to c drive (as cmd.exe cannot handle UNC addresses)
181 os.chdir("C:")
181 os.chdir("C:")
182 return self.path
182 return self.path
183 else:
183 else:
184 # We return None to signal that there was no change in the working
184 # We return None to signal that there was no change in the working
185 # directory
185 # directory
186 return None
186 return None
187
187
188 def __exit__(self, exc_type, exc_value, traceback):
188 def __exit__(self, exc_type, exc_value, traceback):
189 if self.is_unc_path:
189 if self.is_unc_path:
190 os.chdir(self.path)
190 os.chdir(self.path)
191
191
192
192
193 class Win32ShellCommandController(object):
193 class Win32ShellCommandController(object):
194 """Runs a shell command in a 'with' context.
194 """Runs a shell command in a 'with' context.
195
195
196 This implementation is Win32-specific.
196 This implementation is Win32-specific.
197
197
198 Example:
198 Example:
199 # Runs the command interactively with default console stdin/stdout
199 # Runs the command interactively with default console stdin/stdout
200 with ShellCommandController('python -i') as scc:
200 with ShellCommandController('python -i') as scc:
201 scc.run()
201 scc.run()
202
202
203 # Runs the command using the provided functions for stdin/stdout
203 # Runs the command using the provided functions for stdin/stdout
204 def my_stdout_func(s):
204 def my_stdout_func(s):
205 # print or save the string 's'
205 # print or save the string 's'
206 write_to_stdout(s)
206 write_to_stdout(s)
207 def my_stdin_func():
207 def my_stdin_func():
208 # If input is available, return it as a string.
208 # If input is available, return it as a string.
209 if input_available():
209 if input_available():
210 return get_input()
210 return get_input()
211 # If no input available, return None after a short delay to
211 # If no input available, return None after a short delay to
212 # keep from blocking.
212 # keep from blocking.
213 else:
213 else:
214 time.sleep(0.01)
214 time.sleep(0.01)
215 return None
215 return None
216
216
217 with ShellCommandController('python -i') as scc:
217 with ShellCommandController('python -i') as scc:
218 scc.run(my_stdout_func, my_stdin_func)
218 scc.run(my_stdout_func, my_stdin_func)
219 """
219 """
220
220
221 def __init__(self, cmd, mergeout = True):
221 def __init__(self, cmd, mergeout = True):
222 """Initializes the shell command controller.
222 """Initializes the shell command controller.
223
223
224 The cmd is the program to execute, and mergeout is
224 The cmd is the program to execute, and mergeout is
225 whether to blend stdout and stderr into one output
225 whether to blend stdout and stderr into one output
226 in stdout. Merging them together in this fashion more
226 in stdout. Merging them together in this fashion more
227 reliably keeps stdout and stderr in the correct order
227 reliably keeps stdout and stderr in the correct order
228 especially for interactive shell usage.
228 especially for interactive shell usage.
229 """
229 """
230 self.cmd = cmd
230 self.cmd = cmd
231 self.mergeout = mergeout
231 self.mergeout = mergeout
232
232
233 def __enter__(self):
233 def __enter__(self):
234 cmd = self.cmd
234 cmd = self.cmd
235 mergeout = self.mergeout
235 mergeout = self.mergeout
236
236
237 self.hstdout, self.hstdin, self.hstderr = None, None, None
237 self.hstdout, self.hstdin, self.hstderr = None, None, None
238 self.piProcInfo = None
238 self.piProcInfo = None
239 try:
239 try:
240 p_hstdout, c_hstdout, p_hstderr, \
240 p_hstdout, c_hstdout, p_hstderr, \
241 c_hstderr, p_hstdin, c_hstdin = [None]*6
241 c_hstderr, p_hstdin, c_hstdin = [None]*6
242
242
243 # SECURITY_ATTRIBUTES with inherit handle set to True
243 # SECURITY_ATTRIBUTES with inherit handle set to True
244 saAttr = SECURITY_ATTRIBUTES()
244 saAttr = SECURITY_ATTRIBUTES()
245 saAttr.nLength = ctypes.sizeof(saAttr)
245 saAttr.nLength = ctypes.sizeof(saAttr)
246 saAttr.bInheritHandle = True
246 saAttr.bInheritHandle = True
247 saAttr.lpSecurityDescriptor = None
247 saAttr.lpSecurityDescriptor = None
248
248
249 def create_pipe(uninherit):
249 def create_pipe(uninherit):
250 """Creates a Windows pipe, which consists of two handles.
250 """Creates a Windows pipe, which consists of two handles.
251
251
252 The 'uninherit' parameter controls which handle is not
252 The 'uninherit' parameter controls which handle is not
253 inherited by the child process.
253 inherited by the child process.
254 """
254 """
255 handles = HANDLE(), HANDLE()
255 handles = HANDLE(), HANDLE()
256 if not CreatePipe(ctypes.byref(handles[0]),
256 if not CreatePipe(ctypes.byref(handles[0]),
257 ctypes.byref(handles[1]), ctypes.byref(saAttr), 0):
257 ctypes.byref(handles[1]), ctypes.byref(saAttr), 0):
258 raise ctypes.WinError()
258 raise ctypes.WinError()
259 if not SetHandleInformation(handles[uninherit],
259 if not SetHandleInformation(handles[uninherit],
260 HANDLE_FLAG_INHERIT, 0):
260 HANDLE_FLAG_INHERIT, 0):
261 raise ctypes.WinError()
261 raise ctypes.WinError()
262 return handles[0].value, handles[1].value
262 return handles[0].value, handles[1].value
263
263
264 p_hstdout, c_hstdout = create_pipe(uninherit=0)
264 p_hstdout, c_hstdout = create_pipe(uninherit=0)
265 # 'mergeout' signals that stdout and stderr should be merged.
265 # 'mergeout' signals that stdout and stderr should be merged.
266 # We do that by using one pipe for both of them.
266 # We do that by using one pipe for both of them.
267 if mergeout:
267 if mergeout:
268 c_hstderr = HANDLE()
268 c_hstderr = HANDLE()
269 if not DuplicateHandle(GetCurrentProcess(), c_hstdout,
269 if not DuplicateHandle(GetCurrentProcess(), c_hstdout,
270 GetCurrentProcess(), ctypes.byref(c_hstderr),
270 GetCurrentProcess(), ctypes.byref(c_hstderr),
271 0, True, DUPLICATE_SAME_ACCESS):
271 0, True, DUPLICATE_SAME_ACCESS):
272 raise ctypes.WinError()
272 raise ctypes.WinError()
273 else:
273 else:
274 p_hstderr, c_hstderr = create_pipe(uninherit=0)
274 p_hstderr, c_hstderr = create_pipe(uninherit=0)
275 c_hstdin, p_hstdin = create_pipe(uninherit=1)
275 c_hstdin, p_hstdin = create_pipe(uninherit=1)
276
276
277 # Create the process object
277 # Create the process object
278 piProcInfo = PROCESS_INFORMATION()
278 piProcInfo = PROCESS_INFORMATION()
279 siStartInfo = STARTUPINFO()
279 siStartInfo = STARTUPINFO()
280 siStartInfo.cb = ctypes.sizeof(siStartInfo)
280 siStartInfo.cb = ctypes.sizeof(siStartInfo)
281 siStartInfo.hStdInput = c_hstdin
281 siStartInfo.hStdInput = c_hstdin
282 siStartInfo.hStdOutput = c_hstdout
282 siStartInfo.hStdOutput = c_hstdout
283 siStartInfo.hStdError = c_hstderr
283 siStartInfo.hStdError = c_hstderr
284 siStartInfo.dwFlags = STARTF_USESTDHANDLES
284 siStartInfo.dwFlags = STARTF_USESTDHANDLES
285 dwCreationFlags = CREATE_SUSPENDED | CREATE_NO_WINDOW # | CREATE_NEW_CONSOLE
285 dwCreationFlags = CREATE_SUSPENDED | CREATE_NO_WINDOW # | CREATE_NEW_CONSOLE
286
286
287 if not CreateProcess(None,
287 if not CreateProcess(None,
288 u"cmd.exe /c " + cmd,
288 u"cmd.exe /c " + cmd,
289 None, None, True, dwCreationFlags,
289 None, None, True, dwCreationFlags,
290 None, None, ctypes.byref(siStartInfo),
290 None, None, ctypes.byref(siStartInfo),
291 ctypes.byref(piProcInfo)):
291 ctypes.byref(piProcInfo)):
292 raise ctypes.WinError()
292 raise ctypes.WinError()
293
293
294 # Close this process's versions of the child handles
294 # Close this process's versions of the child handles
295 CloseHandle(c_hstdin)
295 CloseHandle(c_hstdin)
296 c_hstdin = None
296 c_hstdin = None
297 CloseHandle(c_hstdout)
297 CloseHandle(c_hstdout)
298 c_hstdout = None
298 c_hstdout = None
299 if c_hstderr != None:
299 if c_hstderr is not None:
300 CloseHandle(c_hstderr)
300 CloseHandle(c_hstderr)
301 c_hstderr = None
301 c_hstderr = None
302
302
303 # Transfer ownership of the parent handles to the object
303 # Transfer ownership of the parent handles to the object
304 self.hstdin = p_hstdin
304 self.hstdin = p_hstdin
305 p_hstdin = None
305 p_hstdin = None
306 self.hstdout = p_hstdout
306 self.hstdout = p_hstdout
307 p_hstdout = None
307 p_hstdout = None
308 if not mergeout:
308 if not mergeout:
309 self.hstderr = p_hstderr
309 self.hstderr = p_hstderr
310 p_hstderr = None
310 p_hstderr = None
311 self.piProcInfo = piProcInfo
311 self.piProcInfo = piProcInfo
312
312
313 finally:
313 finally:
314 if p_hstdin:
314 if p_hstdin:
315 CloseHandle(p_hstdin)
315 CloseHandle(p_hstdin)
316 if c_hstdin:
316 if c_hstdin:
317 CloseHandle(c_hstdin)
317 CloseHandle(c_hstdin)
318 if p_hstdout:
318 if p_hstdout:
319 CloseHandle(p_hstdout)
319 CloseHandle(p_hstdout)
320 if c_hstdout:
320 if c_hstdout:
321 CloseHandle(c_hstdout)
321 CloseHandle(c_hstdout)
322 if p_hstderr:
322 if p_hstderr:
323 CloseHandle(p_hstderr)
323 CloseHandle(p_hstderr)
324 if c_hstderr:
324 if c_hstderr:
325 CloseHandle(c_hstderr)
325 CloseHandle(c_hstderr)
326
326
327 return self
327 return self
328
328
329 def _stdin_thread(self, handle, hprocess, func, stdout_func):
329 def _stdin_thread(self, handle, hprocess, func, stdout_func):
330 exitCode = DWORD()
330 exitCode = DWORD()
331 bytesWritten = DWORD(0)
331 bytesWritten = DWORD(0)
332 while True:
332 while True:
333 #print("stdin thread loop start")
333 #print("stdin thread loop start")
334 # Get the input string (may be bytes or unicode)
334 # Get the input string (may be bytes or unicode)
335 data = func()
335 data = func()
336
336
337 # None signals to poll whether the process has exited
337 # None signals to poll whether the process has exited
338 if data is None:
338 if data is None:
339 #print("checking for process completion")
339 #print("checking for process completion")
340 if not GetExitCodeProcess(hprocess, ctypes.byref(exitCode)):
340 if not GetExitCodeProcess(hprocess, ctypes.byref(exitCode)):
341 raise ctypes.WinError()
341 raise ctypes.WinError()
342 if exitCode.value != STILL_ACTIVE:
342 if exitCode.value != STILL_ACTIVE:
343 return
343 return
344 # TESTING: Does zero-sized writefile help?
344 # TESTING: Does zero-sized writefile help?
345 if not WriteFile(handle, "", 0,
345 if not WriteFile(handle, "", 0,
346 ctypes.byref(bytesWritten), None):
346 ctypes.byref(bytesWritten), None):
347 raise ctypes.WinError()
347 raise ctypes.WinError()
348 continue
348 continue
349 #print("\nGot str %s\n" % repr(data), file=sys.stderr)
349 #print("\nGot str %s\n" % repr(data), file=sys.stderr)
350
350
351 # Encode the string to the console encoding
351 # Encode the string to the console encoding
352 if isinstance(data, unicode): #FIXME: Python3
352 if isinstance(data, unicode): #FIXME: Python3
353 data = data.encode('utf_8')
353 data = data.encode('utf_8')
354
354
355 # What we have now must be a string of bytes
355 # What we have now must be a string of bytes
356 if not isinstance(data, str): #FIXME: Python3
356 if not isinstance(data, str): #FIXME: Python3
357 raise RuntimeError("internal stdin function string error")
357 raise RuntimeError("internal stdin function string error")
358
358
359 # An empty string signals EOF
359 # An empty string signals EOF
360 if len(data) == 0:
360 if len(data) == 0:
361 return
361 return
362
362
363 # In a windows console, sometimes the input is echoed,
363 # In a windows console, sometimes the input is echoed,
364 # but sometimes not. How do we determine when to do this?
364 # but sometimes not. How do we determine when to do this?
365 stdout_func(data)
365 stdout_func(data)
366 # WriteFile may not accept all the data at once.
366 # WriteFile may not accept all the data at once.
367 # Loop until everything is processed
367 # Loop until everything is processed
368 while len(data) != 0:
368 while len(data) != 0:
369 #print("Calling writefile")
369 #print("Calling writefile")
370 if not WriteFile(handle, data, len(data),
370 if not WriteFile(handle, data, len(data),
371 ctypes.byref(bytesWritten), None):
371 ctypes.byref(bytesWritten), None):
372 # This occurs at exit
372 # This occurs at exit
373 if GetLastError() == ERROR_NO_DATA:
373 if GetLastError() == ERROR_NO_DATA:
374 return
374 return
375 raise ctypes.WinError()
375 raise ctypes.WinError()
376 #print("Called writefile")
376 #print("Called writefile")
377 data = data[bytesWritten.value:]
377 data = data[bytesWritten.value:]
378
378
379 def _stdout_thread(self, handle, func):
379 def _stdout_thread(self, handle, func):
380 # Allocate the output buffer
380 # Allocate the output buffer
381 data = ctypes.create_string_buffer(4096)
381 data = ctypes.create_string_buffer(4096)
382 while True:
382 while True:
383 bytesRead = DWORD(0)
383 bytesRead = DWORD(0)
384 if not ReadFile(handle, data, 4096,
384 if not ReadFile(handle, data, 4096,
385 ctypes.byref(bytesRead), None):
385 ctypes.byref(bytesRead), None):
386 le = GetLastError()
386 le = GetLastError()
387 if le == ERROR_BROKEN_PIPE:
387 if le == ERROR_BROKEN_PIPE:
388 return
388 return
389 else:
389 else:
390 raise ctypes.WinError()
390 raise ctypes.WinError()
391 # FIXME: Python3
391 # FIXME: Python3
392 s = data.value[0:bytesRead.value]
392 s = data.value[0:bytesRead.value]
393 #print("\nv: %s" % repr(s), file=sys.stderr)
393 #print("\nv: %s" % repr(s), file=sys.stderr)
394 func(s.decode('utf_8', 'replace'))
394 func(s.decode('utf_8', 'replace'))
395
395
396 def run(self, stdout_func = None, stdin_func = None, stderr_func = None):
396 def run(self, stdout_func = None, stdin_func = None, stderr_func = None):
397 """Runs the process, using the provided functions for I/O.
397 """Runs the process, using the provided functions for I/O.
398
398
399 The function stdin_func should return strings whenever a
399 The function stdin_func should return strings whenever a
400 character or characters become available.
400 character or characters become available.
401 The functions stdout_func and stderr_func are called whenever
401 The functions stdout_func and stderr_func are called whenever
402 something is printed to stdout or stderr, respectively.
402 something is printed to stdout or stderr, respectively.
403 These functions are called from different threads (but not
403 These functions are called from different threads (but not
404 concurrently, because of the GIL).
404 concurrently, because of the GIL).
405 """
405 """
406 if stdout_func == None and stdin_func == None and stderr_func == None:
406 if stdout_func is None and stdin_func is None and stderr_func is None:
407 return self._run_stdio()
407 return self._run_stdio()
408
408
409 if stderr_func != None and self.mergeout:
409 if stderr_func is not None and self.mergeout:
410 raise RuntimeError("Shell command was initiated with "
410 raise RuntimeError("Shell command was initiated with "
411 "merged stdin/stdout, but a separate stderr_func "
411 "merged stdin/stdout, but a separate stderr_func "
412 "was provided to the run() method")
412 "was provided to the run() method")
413
413
414 # Create a thread for each input/output handle
414 # Create a thread for each input/output handle
415 stdin_thread = None
415 stdin_thread = None
416 threads = []
416 threads = []
417 if stdin_func:
417 if stdin_func:
418 stdin_thread = threading.Thread(target=self._stdin_thread,
418 stdin_thread = threading.Thread(target=self._stdin_thread,
419 args=(self.hstdin, self.piProcInfo.hProcess,
419 args=(self.hstdin, self.piProcInfo.hProcess,
420 stdin_func, stdout_func))
420 stdin_func, stdout_func))
421 threads.append(threading.Thread(target=self._stdout_thread,
421 threads.append(threading.Thread(target=self._stdout_thread,
422 args=(self.hstdout, stdout_func)))
422 args=(self.hstdout, stdout_func)))
423 if not self.mergeout:
423 if not self.mergeout:
424 if stderr_func == None:
424 if stderr_func is None:
425 stderr_func = stdout_func
425 stderr_func = stdout_func
426 threads.append(threading.Thread(target=self._stdout_thread,
426 threads.append(threading.Thread(target=self._stdout_thread,
427 args=(self.hstderr, stderr_func)))
427 args=(self.hstderr, stderr_func)))
428 # Start the I/O threads and the process
428 # Start the I/O threads and the process
429 if ResumeThread(self.piProcInfo.hThread) == 0xFFFFFFFF:
429 if ResumeThread(self.piProcInfo.hThread) == 0xFFFFFFFF:
430 raise ctypes.WinError()
430 raise ctypes.WinError()
431 if stdin_thread is not None:
431 if stdin_thread is not None:
432 stdin_thread.start()
432 stdin_thread.start()
433 for thread in threads:
433 for thread in threads:
434 thread.start()
434 thread.start()
435 # Wait for the process to complete
435 # Wait for the process to complete
436 if WaitForSingleObject(self.piProcInfo.hProcess, INFINITE) == \
436 if WaitForSingleObject(self.piProcInfo.hProcess, INFINITE) == \
437 WAIT_FAILED:
437 WAIT_FAILED:
438 raise ctypes.WinError()
438 raise ctypes.WinError()
439 # Wait for the I/O threads to complete
439 # Wait for the I/O threads to complete
440 for thread in threads:
440 for thread in threads:
441 thread.join()
441 thread.join()
442
442
443 # Wait for the stdin thread to complete
443 # Wait for the stdin thread to complete
444 if stdin_thread is not None:
444 if stdin_thread is not None:
445 stdin_thread.join()
445 stdin_thread.join()
446
446
447 def _stdin_raw_nonblock(self):
447 def _stdin_raw_nonblock(self):
448 """Use the raw Win32 handle of sys.stdin to do non-blocking reads"""
448 """Use the raw Win32 handle of sys.stdin to do non-blocking reads"""
449 # WARNING: This is experimental, and produces inconsistent results.
449 # WARNING: This is experimental, and produces inconsistent results.
450 # It's possible for the handle not to be appropriate for use
450 # It's possible for the handle not to be appropriate for use
451 # with WaitForSingleObject, among other things.
451 # with WaitForSingleObject, among other things.
452 handle = msvcrt.get_osfhandle(sys.stdin.fileno())
452 handle = msvcrt.get_osfhandle(sys.stdin.fileno())
453 result = WaitForSingleObject(handle, 100)
453 result = WaitForSingleObject(handle, 100)
454 if result == WAIT_FAILED:
454 if result == WAIT_FAILED:
455 raise ctypes.WinError()
455 raise ctypes.WinError()
456 elif result == WAIT_TIMEOUT:
456 elif result == WAIT_TIMEOUT:
457 print(".", end='')
457 print(".", end='')
458 return None
458 return None
459 else:
459 else:
460 data = ctypes.create_string_buffer(256)
460 data = ctypes.create_string_buffer(256)
461 bytesRead = DWORD(0)
461 bytesRead = DWORD(0)
462 print('?', end='')
462 print('?', end='')
463
463
464 if not ReadFile(handle, data, 256,
464 if not ReadFile(handle, data, 256,
465 ctypes.byref(bytesRead), None):
465 ctypes.byref(bytesRead), None):
466 raise ctypes.WinError()
466 raise ctypes.WinError()
467 # This ensures the non-blocking works with an actual console
467 # This ensures the non-blocking works with an actual console
468 # Not checking the error, so the processing will still work with
468 # Not checking the error, so the processing will still work with
469 # other handle types
469 # other handle types
470 FlushConsoleInputBuffer(handle)
470 FlushConsoleInputBuffer(handle)
471
471
472 data = data.value
472 data = data.value
473 data = data.replace('\r\n', '\n')
473 data = data.replace('\r\n', '\n')
474 data = data.replace('\r', '\n')
474 data = data.replace('\r', '\n')
475 print(repr(data) + " ", end='')
475 print(repr(data) + " ", end='')
476 return data
476 return data
477
477
478 def _stdin_raw_block(self):
478 def _stdin_raw_block(self):
479 """Use a blocking stdin read"""
479 """Use a blocking stdin read"""
480 # The big problem with the blocking read is that it doesn't
480 # The big problem with the blocking read is that it doesn't
481 # exit when it's supposed to in all contexts. An extra
481 # exit when it's supposed to in all contexts. An extra
482 # key-press may be required to trigger the exit.
482 # key-press may be required to trigger the exit.
483 try:
483 try:
484 data = sys.stdin.read(1)
484 data = sys.stdin.read(1)
485 data = data.replace('\r', '\n')
485 data = data.replace('\r', '\n')
486 return data
486 return data
487 except WindowsError as we:
487 except WindowsError as we:
488 if we.winerror == ERROR_NO_DATA:
488 if we.winerror == ERROR_NO_DATA:
489 # This error occurs when the pipe is closed
489 # This error occurs when the pipe is closed
490 return None
490 return None
491 else:
491 else:
492 # Otherwise let the error propagate
492 # Otherwise let the error propagate
493 raise we
493 raise we
494
494
495 def _stdout_raw(self, s):
495 def _stdout_raw(self, s):
496 """Writes the string to stdout"""
496 """Writes the string to stdout"""
497 print(s, end='', file=sys.stdout)
497 print(s, end='', file=sys.stdout)
498 sys.stdout.flush()
498 sys.stdout.flush()
499
499
500 def _stderr_raw(self, s):
500 def _stderr_raw(self, s):
501 """Writes the string to stdout"""
501 """Writes the string to stdout"""
502 print(s, end='', file=sys.stderr)
502 print(s, end='', file=sys.stderr)
503 sys.stderr.flush()
503 sys.stderr.flush()
504
504
505 def _run_stdio(self):
505 def _run_stdio(self):
506 """Runs the process using the system standard I/O.
506 """Runs the process using the system standard I/O.
507
507
508 IMPORTANT: stdin needs to be asynchronous, so the Python
508 IMPORTANT: stdin needs to be asynchronous, so the Python
509 sys.stdin object is not used. Instead,
509 sys.stdin object is not used. Instead,
510 msvcrt.kbhit/getwch are used asynchronously.
510 msvcrt.kbhit/getwch are used asynchronously.
511 """
511 """
512 # Disable Line and Echo mode
512 # Disable Line and Echo mode
513 #lpMode = DWORD()
513 #lpMode = DWORD()
514 #handle = msvcrt.get_osfhandle(sys.stdin.fileno())
514 #handle = msvcrt.get_osfhandle(sys.stdin.fileno())
515 #if GetConsoleMode(handle, ctypes.byref(lpMode)):
515 #if GetConsoleMode(handle, ctypes.byref(lpMode)):
516 # set_console_mode = True
516 # set_console_mode = True
517 # if not SetConsoleMode(handle, lpMode.value &
517 # if not SetConsoleMode(handle, lpMode.value &
518 # ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT)):
518 # ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT)):
519 # raise ctypes.WinError()
519 # raise ctypes.WinError()
520
520
521 if self.mergeout:
521 if self.mergeout:
522 return self.run(stdout_func = self._stdout_raw,
522 return self.run(stdout_func = self._stdout_raw,
523 stdin_func = self._stdin_raw_block)
523 stdin_func = self._stdin_raw_block)
524 else:
524 else:
525 return self.run(stdout_func = self._stdout_raw,
525 return self.run(stdout_func = self._stdout_raw,
526 stdin_func = self._stdin_raw_block,
526 stdin_func = self._stdin_raw_block,
527 stderr_func = self._stderr_raw)
527 stderr_func = self._stderr_raw)
528
528
529 # Restore the previous console mode
529 # Restore the previous console mode
530 #if set_console_mode:
530 #if set_console_mode:
531 # if not SetConsoleMode(handle, lpMode.value):
531 # if not SetConsoleMode(handle, lpMode.value):
532 # raise ctypes.WinError()
532 # raise ctypes.WinError()
533
533
534 def __exit__(self, exc_type, exc_value, traceback):
534 def __exit__(self, exc_type, exc_value, traceback):
535 if self.hstdin:
535 if self.hstdin:
536 CloseHandle(self.hstdin)
536 CloseHandle(self.hstdin)
537 self.hstdin = None
537 self.hstdin = None
538 if self.hstdout:
538 if self.hstdout:
539 CloseHandle(self.hstdout)
539 CloseHandle(self.hstdout)
540 self.hstdout = None
540 self.hstdout = None
541 if self.hstderr:
541 if self.hstderr:
542 CloseHandle(self.hstderr)
542 CloseHandle(self.hstderr)
543 self.hstderr = None
543 self.hstderr = None
544 if self.piProcInfo != None:
544 if self.piProcInfo is not None:
545 CloseHandle(self.piProcInfo.hProcess)
545 CloseHandle(self.piProcInfo.hProcess)
546 CloseHandle(self.piProcInfo.hThread)
546 CloseHandle(self.piProcInfo.hThread)
547 self.piProcInfo = None
547 self.piProcInfo = None
548
548
549
549
550 def system(cmd):
550 def system(cmd):
551 """Win32 version of os.system() that works with network shares.
551 """Win32 version of os.system() that works with network shares.
552
552
553 Note that this implementation returns None, as meant for use in IPython.
553 Note that this implementation returns None, as meant for use in IPython.
554
554
555 Parameters
555 Parameters
556 ----------
556 ----------
557 cmd : str
557 cmd : str
558 A command to be executed in the system shell.
558 A command to be executed in the system shell.
559
559
560 Returns
560 Returns
561 -------
561 -------
562 None : we explicitly do NOT return the subprocess status code, as this
562 None : we explicitly do NOT return the subprocess status code, as this
563 utility is meant to be used extensively in IPython, where any return value
563 utility is meant to be used extensively in IPython, where any return value
564 would trigger :func:`sys.displayhook` calls.
564 would trigger :func:`sys.displayhook` calls.
565 """
565 """
566 with AvoidUNCPath() as path:
566 with AvoidUNCPath() as path:
567 if path is not None:
567 if path is not None:
568 cmd = '"pushd %s &&"%s' % (path, cmd)
568 cmd = '"pushd %s &&"%s' % (path, cmd)
569 with Win32ShellCommandController(cmd) as scc:
569 with Win32ShellCommandController(cmd) as scc:
570 scc.run()
570 scc.run()
571
571
572
572
573 if __name__ == "__main__":
573 if __name__ == "__main__":
574 print("Test starting!")
574 print("Test starting!")
575 #system("cmd")
575 #system("cmd")
576 system("python -i")
576 system("python -i")
577 print("Test finished!")
577 print("Test finished!")
General Comments 0
You need to be logged in to leave comments. Login now