##// END OF EJS Templates
move %history and %save action into mainwindow
Matthias BUSSONNIER -
Show More
@@ -1,300 +1,286 b''
1 1 # System library imports
2 2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from IPython.utils.traitlets import Bool
6 6 from console_widget import ConsoleWidget
7 7
8 8
9 9 class HistoryConsoleWidget(ConsoleWidget):
10 10 """ A ConsoleWidget that keeps a history of the commands that have been
11 11 executed and provides a readline-esque interface to this history.
12 12 """
13 13
14 14 #------ Configuration ------------------------------------------------------
15 15
16 16 # If enabled, the input buffer will become "locked" to history movement when
17 17 # an edit is made to a multi-line input buffer. To override the lock, use
18 18 # Shift in conjunction with the standard history cycling keys.
19 19 history_lock = Bool(False, config=True)
20 20
21 21 #---------------------------------------------------------------------------
22 22 # 'object' interface
23 23 #---------------------------------------------------------------------------
24 24
25 25 def __init__(self, *args, **kw):
26 26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27 27
28 28 # HistoryConsoleWidget protected variables.
29 29 self._history = []
30 30 self._history_edits = {}
31 31 self._history_index = 0
32 32 self._history_prefix = ''
33 33
34 self.history_action = QtGui.QAction("History",
35 self,
36 statusTip="show command history",
37 triggered=self.history_magic)
38 self.history_action.setDisabled(True)
39 self.addAction(self.history_action)
40
41 self.save_action = QtGui.QAction("Export History ",
42 self,
43 statusTip="Export History as Python File",
44 triggered=self.save_magic)
45 self.save_action.setDisabled(True)
46 self.addAction(self.save_action)
47
48 34 #---------------------------------------------------------------------------
49 35 # 'ConsoleWidget' public interface
50 36 #---------------------------------------------------------------------------
51 37
52 38 def execute(self, source=None, hidden=False, interactive=False):
53 39 """ Reimplemented to the store history.
54 40 """
55 41 if not hidden:
56 42 history = self.input_buffer if source is None else source
57 43
58 44 executed = super(HistoryConsoleWidget, self).execute(
59 45 source, hidden, interactive)
60 46
61 47 if executed and not hidden:
62 48 # Save the command unless it was an empty string or was identical
63 49 # to the previous command.
64 50 history = history.rstrip()
65 51 if history and (not self._history or self._history[-1] != history):
66 52 self._history.append(history)
67 53
68 54 # Emulate readline: reset all history edits.
69 55 self._history_edits = {}
70 56
71 57 # Move the history index to the most recent item.
72 58 self._history_index = len(self._history)
73 59
74 60 return executed
75 61
76 62 #---------------------------------------------------------------------------
77 63 # 'ConsoleWidget' abstract interface
78 64 #---------------------------------------------------------------------------
79 65
80 66 def _up_pressed(self, shift_modifier):
81 67 """ Called when the up key is pressed. Returns whether to continue
82 68 processing the event.
83 69 """
84 70 prompt_cursor = self._get_prompt_cursor()
85 71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
86 72 # Bail out if we're locked.
87 73 if self._history_locked() and not shift_modifier:
88 74 return False
89 75
90 76 # Set a search prefix based on the cursor position.
91 77 col = self._get_input_buffer_cursor_column()
92 78 input_buffer = self.input_buffer
93 79 if self._history_index == len(self._history) or \
94 80 (self._history_prefix and col != len(self._history_prefix)):
95 81 self._history_index = len(self._history)
96 82 self._history_prefix = input_buffer[:col]
97 83
98 84 # Perform the search.
99 85 self.history_previous(self._history_prefix,
100 86 as_prefix=not shift_modifier)
101 87
102 88 # Go to the first line of the prompt for seemless history scrolling.
103 89 # Emulate readline: keep the cursor position fixed for a prefix
104 90 # search.
105 91 cursor = self._get_prompt_cursor()
106 92 if self._history_prefix:
107 93 cursor.movePosition(QtGui.QTextCursor.Right,
108 94 n=len(self._history_prefix))
109 95 else:
110 96 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
111 97 self._set_cursor(cursor)
112 98
113 99 return False
114 100
115 101 return True
116 102
117 103 def _down_pressed(self, shift_modifier):
118 104 """ Called when the down key is pressed. Returns whether to continue
119 105 processing the event.
120 106 """
121 107 end_cursor = self._get_end_cursor()
122 108 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
123 109 # Bail out if we're locked.
124 110 if self._history_locked() and not shift_modifier:
125 111 return False
126 112
127 113 # Perform the search.
128 114 replaced = self.history_next(self._history_prefix,
129 115 as_prefix=not shift_modifier)
130 116
131 117 # Emulate readline: keep the cursor position fixed for a prefix
132 118 # search. (We don't need to move the cursor to the end of the buffer
133 119 # in the other case because this happens automatically when the
134 120 # input buffer is set.)
135 121 if self._history_prefix and replaced:
136 122 cursor = self._get_prompt_cursor()
137 123 cursor.movePosition(QtGui.QTextCursor.Right,
138 124 n=len(self._history_prefix))
139 125 self._set_cursor(cursor)
140 126
141 127 return False
142 128
143 129 return True
144 130
145 131 #---------------------------------------------------------------------------
146 132 # 'HistoryConsoleWidget' public interface
147 133 #---------------------------------------------------------------------------
148 134
149 135 def history_previous(self, substring='', as_prefix=True):
150 136 """ If possible, set the input buffer to a previous history item.
151 137
152 138 Parameters:
153 139 -----------
154 140 substring : str, optional
155 141 If specified, search for an item with this substring.
156 142 as_prefix : bool, optional
157 143 If True, the substring must match at the beginning (default).
158 144
159 145 Returns:
160 146 --------
161 147 Whether the input buffer was changed.
162 148 """
163 149 index = self._history_index
164 150 replace = False
165 151 while index > 0:
166 152 index -= 1
167 153 history = self._get_edited_history(index)
168 154 if (as_prefix and history.startswith(substring)) \
169 155 or (not as_prefix and substring in history):
170 156 replace = True
171 157 break
172 158
173 159 if replace:
174 160 self._store_edits()
175 161 self._history_index = index
176 162 self.input_buffer = history
177 163
178 164 return replace
179 165
180 166 def history_next(self, substring='', as_prefix=True):
181 167 """ If possible, set the input buffer to a subsequent history item.
182 168
183 169 Parameters:
184 170 -----------
185 171 substring : str, optional
186 172 If specified, search for an item with this substring.
187 173 as_prefix : bool, optional
188 174 If True, the substring must match at the beginning (default).
189 175
190 176 Returns:
191 177 --------
192 178 Whether the input buffer was changed.
193 179 """
194 180 index = self._history_index
195 181 replace = False
196 182 while self._history_index < len(self._history):
197 183 index += 1
198 184 history = self._get_edited_history(index)
199 185 if (as_prefix and history.startswith(substring)) \
200 186 or (not as_prefix and substring in history):
201 187 replace = True
202 188 break
203 189
204 190 if replace:
205 191 self._store_edits()
206 192 self._history_index = index
207 193 self.input_buffer = history
208 194
209 195 return replace
210 196
211 197 def history_tail(self, n=10):
212 198 """ Get the local history list.
213 199
214 200 Parameters:
215 201 -----------
216 202 n : int
217 203 The (maximum) number of history items to get.
218 204 """
219 205 return self._history[-n:]
220 206
221 207 def history_magic(self):
222 208 self.pasteMagic("%history")
223 209
224 210 def _request_update_session_history_length(self):
225 211 msg_id = self.kernel_manager.shell_channel.execute('',
226 212 silent=True,
227 213 user_expressions={
228 214 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
229 215 }
230 216 )
231 217 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'save_magic')
232 218
233 219 def _handle_execute_reply(self, msg):
234 220 """ Handles replies for code execution, here only session history length
235 221 """
236 222 info = self._request_info.get('execute')
237 223 if info and info.id == msg['parent_header']['msg_id'] and \
238 224 info.kind == 'save_magic' and not self._hidden:
239 225 content = msg['content']
240 226 status = content['status']
241 227 if status == 'ok':
242 228 self._max_session_history=(int(content['user_expressions']['hlen']))
243 229
244 230 def save_magic(self):
245 231 # update the session history length
246 232 self._request_update_session_history_length()
247 233
248 234 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
249 235 "Enter A filename",
250 236 filter='Python File (*.py);; All files (*.*)'
251 237 )
252 238
253 239 # let's the user search/type for a file name, while the history length
254 240 # is fetched
255 241
256 242 if file_name:
257 243 hist_range, ok = QtGui.QInputDialog.getText(self,
258 244 'Please enter an interval of command to save',
259 245 'Saving commands:',
260 246 text=str('1-'+str(self._max_session_history))
261 247 )
262 248 if ok:
263 249 self.pasteMagic("%save"+" "+file_name+" "+str(hist_range))
264 250
265 251 #---------------------------------------------------------------------------
266 252 # 'HistoryConsoleWidget' protected interface
267 253 #---------------------------------------------------------------------------
268 254
269 255 def _history_locked(self):
270 256 """ Returns whether history movement is locked.
271 257 """
272 258 return (self.history_lock and
273 259 (self._get_edited_history(self._history_index) !=
274 260 self.input_buffer) and
275 261 (self._get_prompt_cursor().blockNumber() !=
276 262 self._get_end_cursor().blockNumber()))
277 263
278 264 def _get_edited_history(self, index):
279 265 """ Retrieves a history item, possibly with temporary edits.
280 266 """
281 267 if index in self._history_edits:
282 268 return self._history_edits[index]
283 269 elif index == len(self._history):
284 270 return unicode()
285 271 return self._history[index]
286 272
287 273 def _set_history(self, history):
288 274 """ Replace the current history with a sequence of history items.
289 275 """
290 276 self._history = list(history)
291 277 self._history_edits = {}
292 278 self._history_index = len(self._history)
293 279
294 280 def _store_edits(self):
295 281 """ If there are edits to the current input buffer, store them.
296 282 """
297 283 current = self.input_buffer
298 284 if self._history_index == len(self._history) or \
299 285 self._history[self._history_index] != current:
300 286 self._history_edits[self._history_index] = current
@@ -1,798 +1,803 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12
13 13 """
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib imports
20 20 import json
21 21 import os
22 22 import signal
23 23 import sys
24 24
25 25 # System library imports
26 26 from IPython.external.qt import QtGui,QtCore
27 27 from pygments.styles import get_all_styles
28 28
29 29 # Local imports
30 30 from IPython.config.application import boolean_flag
31 31 from IPython.core.application import BaseIPythonApplication
32 32 from IPython.core.profiledir import ProfileDir
33 33 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
34 34 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
35 35 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
36 36 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
37 37 from IPython.frontend.qt.console import styles
38 38 from IPython.frontend.qt.kernelmanager import QtKernelManager
39 39 from IPython.utils.path import filefind
40 40 from IPython.utils.py3compat import str_to_bytes
41 41 from IPython.utils.traitlets import (
42 42 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
43 43 )
44 44 from IPython.zmq.ipkernel import (
45 45 flags as ipkernel_flags,
46 46 aliases as ipkernel_aliases,
47 47 IPKernelApp
48 48 )
49 49 from IPython.zmq.session import Session, default_secure
50 50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 51
52 52 import application_rc
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # Network Constants
56 56 #-----------------------------------------------------------------------------
57 57
58 58 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
59 59
60 60 #-----------------------------------------------------------------------------
61 61 # Globals
62 62 #-----------------------------------------------------------------------------
63 63
64 64 _examples = """
65 65 ipython qtconsole # start the qtconsole
66 66 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
67 67 """
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # Classes
71 71 #-----------------------------------------------------------------------------
72 72
73 73 class MainWindow(QtGui.QMainWindow):
74 74
75 75 #---------------------------------------------------------------------------
76 76 # 'object' interface
77 77 #---------------------------------------------------------------------------
78 78
79 79 def __init__(self, app, frontend, existing=False, may_close=True,
80 80 confirm_exit=True):
81 81 """ Create a MainWindow for the specified FrontendWidget.
82 82
83 83 The app is passed as an argument to allow for different
84 84 closing behavior depending on whether we are the Kernel's parent.
85 85
86 86 If existing is True, then this Console does not own the Kernel.
87 87
88 88 If may_close is True, then this Console is permitted to close the kernel
89 89 """
90 90 super(MainWindow, self).__init__()
91 91 self._app = app
92 92 self._frontend = frontend
93 93 self._existing = existing
94 94 if existing:
95 95 self._may_close = may_close
96 96 else:
97 97 self._may_close = True
98 98 self._frontend.exit_requested.connect(self.close)
99 99 self._confirm_exit = confirm_exit
100 100 self.setCentralWidget(frontend)
101 101
102 102 # MenuBar is always present on Mac Os, so let's populate
103 103 # it with possible action, don't do it on other platform
104 104 # as some user might not want the menu bar, or give them
105 105 # an option to remove it
106 106
107 107 def initMenuBar(self):
108 108 #create menu in the order they should appear in the menu bar
109 109 self.fileMenu = self.menuBar().addMenu("File")
110 110 self.editMenu = self.menuBar().addMenu("Edit")
111 111 self.fontMenu = self.menuBar().addMenu("Font")
112 112 self.windowMenu = self.menuBar().addMenu("Window")
113 113 self.magicMenu = self.menuBar().addMenu("Magic")
114 114
115 115 # please keep the Help menu in Mac Os even if empty. It will
116 116 # automatically contain a search field to search inside menus and
117 117 # please keep it spelled in English, as long as Qt Doesn't support
118 118 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
119 119 # this search field fonctionnality
120 120
121 121 self.helpMenu = self.menuBar().addMenu("Help")
122 122
123 123 # sould wrap every line of the following block into a try/except,
124 124 # as we are not sure of instanciating a _frontend which support all
125 125 # theses actions, but there might be a better way
126 126 try:
127 127 self.fileMenu.addAction(self._frontend.print_action)
128 128 except AttributeError:
129 129 print "trying to add unexisting action, skipping"
130 130
131 131 try:
132 132 self.fileMenu.addAction(self._frontend.export_action)
133 133 except AttributeError:
134 134 print "trying to add unexisting action, skipping"
135 135
136 136 try:
137 137 self.fileMenu.addAction(self._frontend.select_all_action)
138 138 except AttributeError:
139 139 print "trying to add unexisting action, skipping"
140 140
141 141 try:
142 142 self.undo_action = QtGui.QAction("Undo",
143 143 self,
144 144 shortcut="Ctrl+Z",
145 145 statusTip="Undo last action if possible",
146 146 triggered=self._frontend.undo)
147 147
148 148 self.editMenu.addAction(self.undo_action)
149 149 except AttributeError:
150 150 print "trying to add unexisting action, skipping"
151 151
152 152 try:
153 153 self.redo_action = QtGui.QAction("Redo",
154 154 self,
155 155 shortcut="Ctrl+Shift+Z",
156 156 statusTip="Redo last action if possible",
157 157 triggered=self._frontend.redo)
158 158 self.editMenu.addAction(self.redo_action)
159 159 except AttributeError:
160 160 print "trying to add unexisting action, skipping"
161 161
162 162 try:
163 163 self.fontMenu.addAction(self._frontend.increase_font_size)
164 164 except AttributeError:
165 165 print "trying to add unexisting action, skipping"
166 166
167 167 try:
168 168 self.fontMenu.addAction(self._frontend.decrease_font_size)
169 169 except AttributeError:
170 170 print "trying to add unexisting action, skipping"
171 171
172 172 try:
173 173 self.fontMenu.addAction(self._frontend.reset_font_size)
174 174 except AttributeError:
175 175 print "trying to add unexisting action, skipping"
176 176
177 177 try:
178 178 self.reset_action = QtGui.QAction("Reset",
179 179 self,
180 180 statusTip="Clear all varible from workspace",
181 181 triggered=self._frontend.reset_magic)
182 182 self.magicMenu.addAction(self.reset_action)
183 183 except AttributeError:
184 184 print "trying to add unexisting action (reset), skipping"
185 185
186 186 try:
187 self.magicMenu.addAction(self._frontend.history_action)
188 self._frontend.history_action.setEnabled(True)
187 self.history_action = QtGui.QAction("History",
188 self,
189 statusTip="show command history",
190 triggered=self._frontend.history_magic)
191 self.magicMenu.addAction(self.history_action)
189 192 except AttributeError:
190 193 print "trying to add unexisting action (history), skipping"
191 194
192 195 try:
193 self.magicMenu.addAction(self._frontend.save_action)
194 self._frontend.save_action.setEnabled(True)
196 self.save_action = QtGui.QAction("Export History ",
197 self,
198 statusTip="Export History as Python File",
199 triggered=self._frontend.save_magic)
200 self.magicMenu.addAction(self.save_action)
195 201 except AttributeError:
196 202 print "trying to add unexisting action (save), skipping"
197 self._frontend.reset_action.setEnabled(True)
198 203
199 204 try:
200 205 self.clear_action = QtGui.QAction("Clear",
201 206 self,
202 207 statusTip="Clear the console",
203 208 triggered=self._frontend.clear_magic)
204 209 self.magicMenu.addAction(self.clear_action)
205 210 except AttributeError:
206 211 print "trying to add unexisting action, skipping"
207 212
208 213 try:
209 214 self.who_action = QtGui.QAction("Who",
210 215 self,
211 216 statusTip="List interactive variable",
212 217 triggered=self._frontend.who_magic)
213 218 self.magicMenu.addAction(self.who_action)
214 219 except AttributeError:
215 220 print "trying to add unexisting action (who), skipping"
216 221
217 222 try:
218 223 self.who_ls_action = QtGui.QAction("Who ls",
219 224 self,
220 225 statusTip="Return a list of interactive variable",
221 226 triggered=self._frontend.who_ls_magic)
222 227 self.magicMenu.addAction(self.who_ls_action)
223 228 except AttributeError:
224 229 print "trying to add unexisting action (who_ls), skipping"
225 230
226 231 try:
227 232 self.whos_action = QtGui.QAction("Whos",
228 233 self,
229 234 statusTip="List interactive variable with detail",
230 235 triggered=self._frontend.whos_magic)
231 236 self.magicMenu.addAction(self.whos_action)
232 237 except AttributeError:
233 238 print "trying to add unexisting action (whos), skipping"
234 239
235 240
236 241 #---------------------------------------------------------------------------
237 242 # QWidget interface
238 243 #---------------------------------------------------------------------------
239 244
240 245 def closeEvent(self, event):
241 246 """ Close the window and the kernel (if necessary).
242 247
243 248 This will prompt the user if they are finished with the kernel, and if
244 249 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
245 250 it closes without prompt.
246 251 """
247 252 keepkernel = None #Use the prompt by default
248 253 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
249 254 keepkernel = self._frontend._keep_kernel_on_exit
250 255
251 256 kernel_manager = self._frontend.kernel_manager
252 257
253 258 if keepkernel is None and not self._confirm_exit:
254 259 # don't prompt, just terminate the kernel if we own it
255 260 # or leave it alone if we don't
256 261 keepkernel = not self._existing
257 262
258 263 if keepkernel is None: #show prompt
259 264 if kernel_manager and kernel_manager.channels_running:
260 265 title = self.window().windowTitle()
261 266 cancel = QtGui.QMessageBox.Cancel
262 267 okay = QtGui.QMessageBox.Ok
263 268 if self._may_close:
264 269 msg = "You are closing this Console window."
265 270 info = "Would you like to quit the Kernel and all attached Consoles as well?"
266 271 justthis = QtGui.QPushButton("&No, just this Console", self)
267 272 justthis.setShortcut('N')
268 273 closeall = QtGui.QPushButton("&Yes, quit everything", self)
269 274 closeall.setShortcut('Y')
270 275 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
271 276 title, msg)
272 277 box.setInformativeText(info)
273 278 box.addButton(cancel)
274 279 box.addButton(justthis, QtGui.QMessageBox.NoRole)
275 280 box.addButton(closeall, QtGui.QMessageBox.YesRole)
276 281 box.setDefaultButton(closeall)
277 282 box.setEscapeButton(cancel)
278 283 pixmap = QtGui.QPixmap(':/icon/IPythonConsole.png')
279 284 scaledpixmap = pixmap.scaledToWidth(64,mode=QtCore.Qt.SmoothTransformation)
280 285 box.setIconPixmap(scaledpixmap)
281 286 reply = box.exec_()
282 287 if reply == 1: # close All
283 288 kernel_manager.shutdown_kernel()
284 289 #kernel_manager.stop_channels()
285 290 event.accept()
286 291 elif reply == 0: # close Console
287 292 if not self._existing:
288 293 # Have kernel: don't quit, just close the window
289 294 self._app.setQuitOnLastWindowClosed(False)
290 295 self.deleteLater()
291 296 event.accept()
292 297 else:
293 298 event.ignore()
294 299 else:
295 300 reply = QtGui.QMessageBox.question(self, title,
296 301 "Are you sure you want to close this Console?"+
297 302 "\nThe Kernel and other Consoles will remain active.",
298 303 okay|cancel,
299 304 defaultButton=okay
300 305 )
301 306 if reply == okay:
302 307 event.accept()
303 308 else:
304 309 event.ignore()
305 310 elif keepkernel: #close console but leave kernel running (no prompt)
306 311 if kernel_manager and kernel_manager.channels_running:
307 312 if not self._existing:
308 313 # I have the kernel: don't quit, just close the window
309 314 self._app.setQuitOnLastWindowClosed(False)
310 315 event.accept()
311 316 else: #close console and kernel (no prompt)
312 317 if kernel_manager and kernel_manager.channels_running:
313 318 kernel_manager.shutdown_kernel()
314 319 event.accept()
315 320
316 321 #-----------------------------------------------------------------------------
317 322 # Aliases and Flags
318 323 #-----------------------------------------------------------------------------
319 324
320 325 flags = dict(ipkernel_flags)
321 326 qt_flags = {
322 327 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
323 328 "Connect to an existing kernel. If no argument specified, guess most recent"),
324 329 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
325 330 "Use a pure Python kernel instead of an IPython kernel."),
326 331 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
327 332 "Disable rich text support."),
328 333 }
329 334 qt_flags.update(boolean_flag(
330 335 'gui-completion', 'ConsoleWidget.gui_completion',
331 336 "use a GUI widget for tab completion",
332 337 "use plaintext output for completion"
333 338 ))
334 339 qt_flags.update(boolean_flag(
335 340 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
336 341 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
337 342 to force a direct exit without any confirmation.
338 343 """,
339 344 """Don't prompt the user when exiting. This will terminate the kernel
340 345 if it is owned by the frontend, and leave it alive if it is external.
341 346 """
342 347 ))
343 348 flags.update(qt_flags)
344 349
345 350 aliases = dict(ipkernel_aliases)
346 351
347 352 qt_aliases = dict(
348 353 hb = 'IPythonQtConsoleApp.hb_port',
349 354 shell = 'IPythonQtConsoleApp.shell_port',
350 355 iopub = 'IPythonQtConsoleApp.iopub_port',
351 356 stdin = 'IPythonQtConsoleApp.stdin_port',
352 357 ip = 'IPythonQtConsoleApp.ip',
353 358 existing = 'IPythonQtConsoleApp.existing',
354 359 f = 'IPythonQtConsoleApp.connection_file',
355 360
356 361 style = 'IPythonWidget.syntax_style',
357 362 stylesheet = 'IPythonQtConsoleApp.stylesheet',
358 363 colors = 'ZMQInteractiveShell.colors',
359 364
360 365 editor = 'IPythonWidget.editor',
361 366 paging = 'ConsoleWidget.paging',
362 367 ssh = 'IPythonQtConsoleApp.sshserver',
363 368 )
364 369 aliases.update(qt_aliases)
365 370
366 371
367 372 #-----------------------------------------------------------------------------
368 373 # IPythonQtConsole
369 374 #-----------------------------------------------------------------------------
370 375
371 376
372 377 class IPythonQtConsoleApp(BaseIPythonApplication):
373 378 name = 'ipython-qtconsole'
374 379 default_config_file_name='ipython_config.py'
375 380
376 381 description = """
377 382 The IPython QtConsole.
378 383
379 384 This launches a Console-style application using Qt. It is not a full
380 385 console, in that launched terminal subprocesses will not be able to accept
381 386 input.
382 387
383 388 The QtConsole supports various extra features beyond the Terminal IPython
384 389 shell, such as inline plotting with matplotlib, via:
385 390
386 391 ipython qtconsole --pylab=inline
387 392
388 393 as well as saving your session as HTML, and printing the output.
389 394
390 395 """
391 396 examples = _examples
392 397
393 398 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
394 399 flags = Dict(flags)
395 400 aliases = Dict(aliases)
396 401
397 402 kernel_argv = List(Unicode)
398 403
399 404 # create requested profiles by default, if they don't exist:
400 405 auto_create = CBool(True)
401 406 # connection info:
402 407 ip = Unicode(LOCALHOST, config=True,
403 408 help="""Set the kernel\'s IP address [default localhost].
404 409 If the IP address is something other than localhost, then
405 410 Consoles on other machines will be able to connect
406 411 to the Kernel, so be careful!"""
407 412 )
408 413
409 414 sshserver = Unicode('', config=True,
410 415 help="""The SSH server to use to connect to the kernel.""")
411 416 sshkey = Unicode('', config=True,
412 417 help="""Path to the ssh key to use for logging in to the ssh server.""")
413 418
414 419 hb_port = Int(0, config=True,
415 420 help="set the heartbeat port [default: random]")
416 421 shell_port = Int(0, config=True,
417 422 help="set the shell (XREP) port [default: random]")
418 423 iopub_port = Int(0, config=True,
419 424 help="set the iopub (PUB) port [default: random]")
420 425 stdin_port = Int(0, config=True,
421 426 help="set the stdin (XREQ) port [default: random]")
422 427 connection_file = Unicode('', config=True,
423 428 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
424 429
425 430 This file will contain the IP, ports, and authentication key needed to connect
426 431 clients to this kernel. By default, this file will be created in the security-dir
427 432 of the current profile, but can be specified by absolute path.
428 433 """)
429 434 def _connection_file_default(self):
430 435 return 'kernel-%i.json' % os.getpid()
431 436
432 437 existing = Unicode('', config=True,
433 438 help="""Connect to an already running kernel""")
434 439
435 440 stylesheet = Unicode('', config=True,
436 441 help="path to a custom CSS stylesheet")
437 442
438 443 pure = CBool(False, config=True,
439 444 help="Use a pure Python kernel instead of an IPython kernel.")
440 445 plain = CBool(False, config=True,
441 446 help="Use a plaintext widget instead of rich text (plain can't print/save).")
442 447
443 448 def _pure_changed(self, name, old, new):
444 449 kind = 'plain' if self.plain else 'rich'
445 450 self.config.ConsoleWidget.kind = kind
446 451 if self.pure:
447 452 self.widget_factory = FrontendWidget
448 453 elif self.plain:
449 454 self.widget_factory = IPythonWidget
450 455 else:
451 456 self.widget_factory = RichIPythonWidget
452 457
453 458 _plain_changed = _pure_changed
454 459
455 460 confirm_exit = CBool(True, config=True,
456 461 help="""
457 462 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
458 463 to force a direct exit without any confirmation.""",
459 464 )
460 465
461 466 # the factory for creating a widget
462 467 widget_factory = Any(RichIPythonWidget)
463 468
464 469 def parse_command_line(self, argv=None):
465 470 super(IPythonQtConsoleApp, self).parse_command_line(argv)
466 471 if argv is None:
467 472 argv = sys.argv[1:]
468 473
469 474 self.kernel_argv = list(argv) # copy
470 475 # kernel should inherit default config file from frontend
471 476 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
472 477 # Scrub frontend-specific flags
473 478 for a in argv:
474 479 if a.startswith('-') and a.lstrip('-') in qt_flags:
475 480 self.kernel_argv.remove(a)
476 481 swallow_next = False
477 482 for a in argv:
478 483 if swallow_next:
479 484 self.kernel_argv.remove(a)
480 485 swallow_next = False
481 486 continue
482 487 if a.startswith('-'):
483 488 split = a.lstrip('-').split('=')
484 489 alias = split[0]
485 490 if alias in qt_aliases:
486 491 self.kernel_argv.remove(a)
487 492 if len(split) == 1:
488 493 # alias passed with arg via space
489 494 swallow_next = True
490 495
491 496 def init_connection_file(self):
492 497 """find the connection file, and load the info if found.
493 498
494 499 The current working directory and the current profile's security
495 500 directory will be searched for the file if it is not given by
496 501 absolute path.
497 502
498 503 When attempting to connect to an existing kernel and the `--existing`
499 504 argument does not match an existing file, it will be interpreted as a
500 505 fileglob, and the matching file in the current profile's security dir
501 506 with the latest access time will be used.
502 507 """
503 508 if self.existing:
504 509 try:
505 510 cf = find_connection_file(self.existing)
506 511 except Exception:
507 512 self.log.critical("Could not find existing kernel connection file %s", self.existing)
508 513 self.exit(1)
509 514 self.log.info("Connecting to existing kernel: %s" % cf)
510 515 self.connection_file = cf
511 516 # should load_connection_file only be used for existing?
512 517 # as it is now, this allows reusing ports if an existing
513 518 # file is requested
514 519 try:
515 520 self.load_connection_file()
516 521 except Exception:
517 522 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
518 523 self.exit(1)
519 524
520 525 def load_connection_file(self):
521 526 """load ip/port/hmac config from JSON connection file"""
522 527 # this is identical to KernelApp.load_connection_file
523 528 # perhaps it can be centralized somewhere?
524 529 try:
525 530 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
526 531 except IOError:
527 532 self.log.debug("Connection File not found: %s", self.connection_file)
528 533 return
529 534 self.log.debug(u"Loading connection file %s", fname)
530 535 with open(fname) as f:
531 536 s = f.read()
532 537 cfg = json.loads(s)
533 538 if self.ip == LOCALHOST and 'ip' in cfg:
534 539 # not overridden by config or cl_args
535 540 self.ip = cfg['ip']
536 541 for channel in ('hb', 'shell', 'iopub', 'stdin'):
537 542 name = channel + '_port'
538 543 if getattr(self, name) == 0 and name in cfg:
539 544 # not overridden by config or cl_args
540 545 setattr(self, name, cfg[name])
541 546 if 'key' in cfg:
542 547 self.config.Session.key = str_to_bytes(cfg['key'])
543 548
544 549 def init_ssh(self):
545 550 """set up ssh tunnels, if needed."""
546 551 if not self.sshserver and not self.sshkey:
547 552 return
548 553
549 554 if self.sshkey and not self.sshserver:
550 555 # specifying just the key implies that we are connecting directly
551 556 self.sshserver = self.ip
552 557 self.ip = LOCALHOST
553 558
554 559 # build connection dict for tunnels:
555 560 info = dict(ip=self.ip,
556 561 shell_port=self.shell_port,
557 562 iopub_port=self.iopub_port,
558 563 stdin_port=self.stdin_port,
559 564 hb_port=self.hb_port
560 565 )
561 566
562 567 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
563 568
564 569 # tunnels return a new set of ports, which will be on localhost:
565 570 self.ip = LOCALHOST
566 571 try:
567 572 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
568 573 except:
569 574 # even catch KeyboardInterrupt
570 575 self.log.error("Could not setup tunnels", exc_info=True)
571 576 self.exit(1)
572 577
573 578 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
574 579
575 580 cf = self.connection_file
576 581 base,ext = os.path.splitext(cf)
577 582 base = os.path.basename(base)
578 583 self.connection_file = os.path.basename(base)+'-ssh'+ext
579 584 self.log.critical("To connect another client via this tunnel, use:")
580 585 self.log.critical("--existing %s" % self.connection_file)
581 586
582 587 def init_kernel_manager(self):
583 588 # Don't let Qt or ZMQ swallow KeyboardInterupts.
584 589 signal.signal(signal.SIGINT, signal.SIG_DFL)
585 590 sec = self.profile_dir.security_dir
586 591 try:
587 592 cf = filefind(self.connection_file, ['.', sec])
588 593 except IOError:
589 594 # file might not exist
590 595 if self.connection_file == os.path.basename(self.connection_file):
591 596 # just shortname, put it in security dir
592 597 cf = os.path.join(sec, self.connection_file)
593 598 else:
594 599 cf = self.connection_file
595 600
596 601 # Create a KernelManager and start a kernel.
597 602 self.kernel_manager = QtKernelManager(
598 603 ip=self.ip,
599 604 shell_port=self.shell_port,
600 605 iopub_port=self.iopub_port,
601 606 stdin_port=self.stdin_port,
602 607 hb_port=self.hb_port,
603 608 connection_file=cf,
604 609 config=self.config,
605 610 )
606 611 # start the kernel
607 612 if not self.existing:
608 613 kwargs = dict(ipython=not self.pure)
609 614 kwargs['extra_arguments'] = self.kernel_argv
610 615 self.kernel_manager.start_kernel(**kwargs)
611 616 elif self.sshserver:
612 617 # ssh, write new connection file
613 618 self.kernel_manager.write_connection_file()
614 619 self.kernel_manager.start_channels()
615 620
616 621
617 622 def init_qt_elements(self):
618 623 # Create the widget.
619 624 self.app = QtGui.QApplication([])
620 625 pixmap=QtGui.QPixmap(':/icon/IPythonConsole.png')
621 626 icon=QtGui.QIcon(pixmap)
622 627 QtGui.QApplication.setWindowIcon(icon)
623 628
624 629 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
625 630 self.widget = self.widget_factory(config=self.config,
626 631 local_kernel=local_kernel)
627 632 self.widget.kernel_manager = self.kernel_manager
628 633 self.window = MainWindow(self.app, self.widget, self.existing,
629 634 may_close=local_kernel,
630 635 confirm_exit=self.confirm_exit)
631 636 self.window.initMenuBar()
632 637 self.window.setWindowTitle('Python' if self.pure else 'IPython')
633 638
634 639 def init_colors(self):
635 640 """Configure the coloring of the widget"""
636 641 # Note: This will be dramatically simplified when colors
637 642 # are removed from the backend.
638 643
639 644 if self.pure:
640 645 # only IPythonWidget supports styling
641 646 return
642 647
643 648 # parse the colors arg down to current known labels
644 649 try:
645 650 colors = self.config.ZMQInteractiveShell.colors
646 651 except AttributeError:
647 652 colors = None
648 653 try:
649 654 style = self.config.IPythonWidget.colors
650 655 except AttributeError:
651 656 style = None
652 657
653 658 # find the value for colors:
654 659 if colors:
655 660 colors=colors.lower()
656 661 if colors in ('lightbg', 'light'):
657 662 colors='lightbg'
658 663 elif colors in ('dark', 'linux'):
659 664 colors='linux'
660 665 else:
661 666 colors='nocolor'
662 667 elif style:
663 668 if style=='bw':
664 669 colors='nocolor'
665 670 elif styles.dark_style(style):
666 671 colors='linux'
667 672 else:
668 673 colors='lightbg'
669 674 else:
670 675 colors=None
671 676
672 677 # Configure the style.
673 678 widget = self.widget
674 679 if style:
675 680 widget.style_sheet = styles.sheet_from_template(style, colors)
676 681 widget.syntax_style = style
677 682 widget._syntax_style_changed()
678 683 widget._style_sheet_changed()
679 684 elif colors:
680 685 # use a default style
681 686 widget.set_default_style(colors=colors)
682 687 else:
683 688 # this is redundant for now, but allows the widget's
684 689 # defaults to change
685 690 widget.set_default_style()
686 691
687 692 if self.stylesheet:
688 693 # we got an expicit stylesheet
689 694 if os.path.isfile(self.stylesheet):
690 695 with open(self.stylesheet) as f:
691 696 sheet = f.read()
692 697 widget.style_sheet = sheet
693 698 widget._style_sheet_changed()
694 699 else:
695 700 raise IOError("Stylesheet %r not found."%self.stylesheet)
696 701
697 702 def initialize(self, argv=None):
698 703 super(IPythonQtConsoleApp, self).initialize(argv)
699 704 self.init_connection_file()
700 705 default_secure(self.config)
701 706 self.init_ssh()
702 707 self.init_kernel_manager()
703 708 self.init_qt_elements()
704 709 self.init_colors()
705 710 self.init_window_shortcut()
706 711
707 712 def init_window_shortcut(self):
708 713
709 714 self.fullScreenAct = QtGui.QAction("Full Screen",
710 715 self.window,
711 716 shortcut="Ctrl+Meta+Space",
712 717 statusTip="Toggle between Fullscreen and Normal Size",
713 718 triggered=self.toggleFullScreen)
714 719
715 720
716 721 # creating shortcut in menubar only for Mac OS as I don't
717 722 # know the shortcut or if the windows manager assign it in
718 723 # other platform.
719 724 if sys.platform == 'darwin':
720 725 self.minimizeAct = QtGui.QAction("Minimize",
721 726 self.window,
722 727 shortcut="Ctrl+m",
723 728 statusTip="Minimize the window/Restore Normal Size",
724 729 triggered=self.toggleMinimized)
725 730 self.maximizeAct = QtGui.QAction("Maximize",
726 731 self.window,
727 732 shortcut="Ctrl+Shift+M",
728 733 statusTip="Maximize the window/Restore Normal Size",
729 734 triggered=self.toggleMaximized)
730 735
731 736 self.onlineHelpAct = QtGui.QAction("Open Online Help",
732 737 self.window,
733 738 triggered=self._open_online_help)
734 739
735 740 self.windowMenu = self.window.windowMenu
736 741 self.windowMenu.addAction(self.minimizeAct)
737 742 self.windowMenu.addAction(self.maximizeAct)
738 743 self.windowMenu.addSeparator()
739 744 self.windowMenu.addAction(self.fullScreenAct)
740 745
741 746 self.window.helpMenu.addAction(self.onlineHelpAct)
742 747 else:
743 748 # if we don't put it in a menu, we add it to the window so
744 749 # that it can still be triggerd by shortcut
745 750 self.window.addAction(self.fullScreenAct)
746 751
747 752 def toggleMinimized(self):
748 753 if not self.window.isMinimized():
749 754 self.window.showMinimized()
750 755 else:
751 756 self.window.showNormal()
752 757
753 758 def _open_online_help(self):
754 759 QtGui.QDesktopServices.openUrl(
755 760 QtCore.QUrl("http://ipython.org/documentation.html",
756 761 QtCore.QUrl.TolerantMode)
757 762 )
758 763
759 764 def toggleMaximized(self):
760 765 if not self.window.isMaximized():
761 766 self.window.showMaximized()
762 767 else:
763 768 self.window.showNormal()
764 769
765 770 # Min/Max imizing while in full screen give a bug
766 771 # when going out of full screen, at least on OSX
767 772 def toggleFullScreen(self):
768 773 if not self.window.isFullScreen():
769 774 self.window.showFullScreen()
770 775 if sys.platform == 'darwin':
771 776 self.maximizeAct.setEnabled(False)
772 777 self.minimizeAct.setEnabled(False)
773 778 else:
774 779 self.window.showNormal()
775 780 if sys.platform == 'darwin':
776 781 self.maximizeAct.setEnabled(True)
777 782 self.minimizeAct.setEnabled(True)
778 783
779 784 def start(self):
780 785
781 786 # draw the window
782 787 self.window.show()
783 788
784 789 # Start the application main loop.
785 790 self.app.exec_()
786 791
787 792 #-----------------------------------------------------------------------------
788 793 # Main entry point
789 794 #-----------------------------------------------------------------------------
790 795
791 796 def main():
792 797 app = IPythonQtConsoleApp()
793 798 app.initialize()
794 799 app.start()
795 800
796 801
797 802 if __name__ == '__main__':
798 803 main()
General Comments 0
You need to be logged in to leave comments. Login now