##// END OF EJS Templates
Create Keyboard Accelerator for MenuBar
Matthias BUSSONNIER -
Show More
@@ -1,1094 +1,1094 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
91 91 super(MainWindow, self).__init__()
92 92 self._app = app
93 93
94 94 self.tabWidget = QtGui.QTabWidget(self)
95 95 self.tabWidget.setDocumentMode(True)
96 96 self.tabWidget.setTabsClosable(True)
97 97 self.tabWidget.tabCloseRequested[int].connect(self.closeTab)
98 98
99 99 self.setCentralWidget(self.tabWidget)
100 100 self.updateTabBarVisibility()
101 101
102 102 def updateTabBarVisibility(self):
103 103 """ update visibility of the tabBar depending of the number of tab
104 104
105 105 0 or 1 tab, tabBar hidden
106 106 2+ tabs, tabBar visible
107 107
108 108 send a self.close if number of tab ==0
109 109
110 110 need to be called explicitely, or be connected to tabInserted/tabRemoved
111 111 """
112 112 if self.tabWidget.count() <= 1:
113 113 self.tabWidget.tabBar().setVisible(False)
114 114 else:
115 115 self.tabWidget.tabBar().setVisible(True)
116 116 if self.tabWidget.count()==0 :
117 117 self.close()
118 118
119 119 @property
120 120 def activeFrontend(self):
121 121 return self.tabWidget.currentWidget()
122 122
123 123 def closeTab(self,currentTab):
124 124 """ Called when you need to try to close a tab.
125 125
126 126 It takes the number of the tab to be closed as argument, or a referece
127 127 to the wiget insite this tab
128 128 """
129 129
130 130 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
131 131 # and a reference to the trontend to close
132 132 if type(currentTab) is not int :
133 133 currentTab = self.tabWidget.indexOf(currentTab)
134 134 closing_widget=self.tabWidget.widget(currentTab)
135 135
136 136
137 137 # when trying to be closed, widget might re-send a request to be closed again, but will
138 138 # be deleted when event will be processed. So need to check that widget still exist and
139 139 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
140 140 # re-send by this fonction on the master widget, which ask all slaves widget to exit
141 141 if closing_widget==None:
142 142 return
143 143
144 144 #get a list of all wwidget not owning the kernel.
145 145 slaveTabs=self.findSlavesTabs(closing_widget)
146 146
147 147 keepkernel = None #Use the prompt by default
148 148 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
149 149 keepkernel = closing_widget._keep_kernel_on_exit
150 150 # If signal sent by exist magic (_keep_kernel_on_exit, exist and not None)
151 151 # we set local slave tabs._hidden to True to avoit prompting for kernel
152 152 # restart when they litt get the signal. and the "forward" the 'exit'
153 153 # to the main win
154 154 if keepkernel is not None:
155 155 for tab in slaveTabs:
156 156 tab._hidden = True
157 157 if closing_widget in slaveTabs :
158 158 try :
159 159 self.findMasterTab(closing_widget).pasteMagic('exit')
160 160 except AttributeError:
161 161 self.log.info("Master already closed or not local, closing only current tab")
162 162 self.tabWidget.removeTab(currentTab)
163 163 return
164 164
165 165 kernel_manager = closing_widget.kernel_manager
166 166
167 167 if keepkernel is None and not closing_widget._confirm_exit:
168 168 # don't prompt, just terminate the kernel if we own it
169 169 # or leave it alone if we don't
170 170 keepkernel = not closing_widget._existing
171 171
172 172 if keepkernel is None: #show prompt
173 173 if kernel_manager and kernel_manager.channels_running:
174 174 title = self.window().windowTitle()
175 175 cancel = QtGui.QMessageBox.Cancel
176 176 okay = QtGui.QMessageBox.Ok
177 177 if closing_widget._may_close:
178 178 msg = "You are closing the tab : "+'"'+self.tabWidget.tabText(currentTab)+'"'
179 179 info = "Would you like to quit the Kernel and all attached Consoles as well?"
180 180 justthis = QtGui.QPushButton("&No, just this Console", self)
181 181 justthis.setShortcut('N')
182 182 closeall = QtGui.QPushButton("&Yes, quit everything", self)
183 183 closeall.setShortcut('Y')
184 184 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
185 185 title, msg)
186 186 box.setInformativeText(info)
187 187 box.addButton(cancel)
188 188 box.addButton(justthis, QtGui.QMessageBox.NoRole)
189 189 box.addButton(closeall, QtGui.QMessageBox.YesRole)
190 190 box.setDefaultButton(closeall)
191 191 box.setEscapeButton(cancel)
192 192 pixmap = QtGui.QPixmap(':/icon/IPythonConsole.png')
193 193 scaledpixmap = pixmap.scaledToWidth(64,mode=QtCore.Qt.SmoothTransformation)
194 194 box.setIconPixmap(scaledpixmap)
195 195 reply = box.exec_()
196 196 if reply == 1: # close All
197 197 for slave in slaveTabs:
198 198 self.tabWidget.removeTab(self.tabWidget.indexOf(slave))
199 199 closing_widget.pasteMagic("exit")
200 200 self.tabWidget.removeTab(currentTab)
201 201 elif reply == 0: # close Console
202 202 if not closing_widget._existing:
203 203 # Have kernel: don't quit, just close the window
204 204 closing_widget.pasteMagic("exit True")
205 205 else:
206 206 reply = QtGui.QMessageBox.question(self, title,
207 207 "Are you sure you want to close this Console?"+
208 208 "\nThe Kernel and other Consoles will remain active.",
209 209 okay|cancel,
210 210 defaultButton=okay
211 211 )
212 212 if reply == okay:
213 213 self.tabWidget.removeTab(currentTab)
214 214 elif keepkernel: #close console but leave kernel running (no prompt)
215 215 if kernel_manager and kernel_manager.channels_running:
216 216 if not closing_widget._existing:
217 217 # I have the kernel: don't quit, just close the window
218 218 self.tabWidget.removeTab(currentTab)
219 219 else: #close console and kernel (no prompt)
220 220 if kernel_manager and kernel_manager.channels_running:
221 221 for slave in slaveTabs:
222 222 self.tabWidget.removeTab(self.tabWidget.indexOf(slave))
223 223 self.tabWidget.removeTab(currentTab)
224 224 kernel_manager.shutdown_kernel()
225 225 self.updateTabBarVisibility()
226 226
227 227 def addTabWithFrontend(self,frontend,name=None):
228 228 """ insert a tab with a given frontend in the tab bar, and give it a name
229 229
230 230 """
231 231 if not name:
232 232 name=str('kernel '+str(self.tabWidget.count()))
233 233 self.tabWidget.addTab(frontend,name)
234 234 self.updateTabBarVisibility()
235 235 self.makeFrontendVisible(frontend)
236 236 frontend.exit_requested.connect(self.closeTab)
237 237
238 238 def nextTab(self):
239 239 self.tabWidget.setCurrentIndex((self.tabWidget.currentIndex()+1))
240 240
241 241 def prevTab(self):
242 242 self.tabWidget.setCurrentIndex((self.tabWidget.currentIndex()-1))
243 243
244 244 def makeFrontendVisible(self,frontend):
245 245 widgetIndex=self.tabWidget.indexOf(frontend)
246 246 if widgetIndex > 0 :
247 247 self.tabWidget.setCurrentIndex(widgetIndex)
248 248
249 249 def findMasterTab(self,tab,asList=False):
250 250 """
251 251 Try to return the frontend that own the kernel attached to the given widget/tab.
252 252
253 253 Only find frontend owed by the current application. Selection
254 254 based on port of the kernel, might be inacurate if several kernel
255 255 on different ip use same port number.
256 256
257 257 This fonction does the conversion tabNumber/widget if needed.
258 258 Might return None if no master widget (non local kernel)
259 259 Will crash IPython if more than 1 masterWidget
260 260
261 261 When asList set to True, always return a list of widget(s) owning
262 262 the kernel. The list might be empty or containing several Widget.
263 263 """
264 264
265 265 #convert from/to int/richIpythonWidget if needed
266 266 if type(tab) == int:
267 267 tab = self.tabWidget.widget(tab)
268 268 km=tab.kernel_manager;
269 269
270 270 #build list of all widgets
271 271 widgetList = [self.tabWidget.widget(i) for i in range(self.tabWidget.count())]
272 272
273 273 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
274 274 # And should have a _may_close attribute
275 275 filtredwidgetList = [ widget for widget in widgetList if
276 276 widget.kernel_manager.shell_address == km.shell_address and
277 277 widget.kernel_manager.sub_address == km.sub_address and
278 278 widget.kernel_manager.stdin_address == km.stdin_address and
279 279 widget.kernel_manager.hb_address == km.hb_address and
280 280 hasattr(widget,'_may_close') ]
281 281 # the master widget is the one that may close the kernel
282 282 masterWidget= [ widget for widget in filtredwidgetList if widget._may_close]
283 283 if asList:
284 284 return masterWidget
285 285 assert(len(masterWidget)<=1 )
286 286 if len(masterWidget)==0:
287 287 return None
288 288
289 289 return masterWidget[0]
290 290
291 291 def findSlavesTabs(self,tab):
292 292 """
293 293 Try to return all the frontend that do not own the kernel attached to the given widget/tab.
294 294
295 295 Only find frontend owed by the current application. Selection
296 296 based on port of the kernel, might be innacurate if several kernel
297 297 on different ip use same port number.
298 298
299 299 This fonction does the conversion tabNumber/widget if needed.
300 300 """
301 301 #convert from/to int/richIpythonWidget if needed
302 302 if type(tab) == int:
303 303 tab = self.tabWidget.widget(tab)
304 304 km=tab.kernel_manager;
305 305
306 306 #build list of all widgets
307 307 widgetList = [self.tabWidget.widget(i) for i in range(self.tabWidget.count())]
308 308
309 309 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
310 310 filtredWidgetList = ( widget for widget in widgetList if
311 311 widget.kernel_manager.shell_address == km.shell_address and
312 312 widget.kernel_manager.sub_address == km.sub_address and
313 313 widget.kernel_manager.stdin_address == km.stdin_address and
314 314 widget.kernel_manager.hb_address == km.hb_address)
315 315 # Get a list of all widget owning the same kernel and removed it from
316 316 # the previous cadidate. (better using sets ?)
317 317 masterWidgetlist = self.findMasterTab(tab,asList=True)
318 318 slaveList = [widget for widget in filtredWidgetList if widget not in masterWidgetlist]
319 319
320 320 return slaveList
321 321
322 322 # MenuBar is always present on Mac Os, so let's populate it with possible
323 323 # action, don't do it on other platform as some user might not want the
324 324 # menu bar, or give them an option to remove it
325 325 def initMenuBar(self):
326 326 #create menu in the order they should appear in the menu bar
327 self.fileMenu = self.menuBar().addMenu("File")
328 self.editMenu = self.menuBar().addMenu("Edit")
329 self.fontMenu = self.menuBar().addMenu("Font")
330 self.windowMenu = self.menuBar().addMenu("Window")
331 self.magicMenu = self.menuBar().addMenu("Magic")
327 self.fileMenu = self.menuBar().addMenu("&File")
328 self.editMenu = self.menuBar().addMenu("&Edit")
329 self.fontMenu = self.menuBar().addMenu("F&ont")
330 self.windowMenu = self.menuBar().addMenu("&Window")
331 self.magicMenu = self.menuBar().addMenu("&Magic")
332 332
333 333 # please keep the Help menu in Mac Os even if empty. It will
334 334 # automatically contain a search field to search inside menus and
335 335 # please keep it spelled in English, as long as Qt Doesn't support
336 336 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
337 337 # this search field fonctionnality
338 338
339 self.helpMenu = self.menuBar().addMenu("Help")
339 self.helpMenu = self.menuBar().addMenu("&Help")
340 340
341 341 # sould wrap every line of the following block into a try/except,
342 342 # as we are not sure of instanciating a _frontend which support all
343 343 # theses actions, but there might be a better way
344 344 try:
345 self.print_action = QtGui.QAction("Print",
345 self.print_action = QtGui.QAction("&Print",
346 346 self,
347 347 shortcut="Ctrl+P",
348 348 triggered=self.print_action_active_frontend)
349 349 self.fileMenu.addAction(self.print_action)
350 350 except AttributeError:
351 351 self.log.error("trying to add unexisting action (print), skipping")
352 352
353 353 try:
354 self.export_action=QtGui.QAction("Export",
354 self.export_action=QtGui.QAction("E&xport",
355 355 self,
356 356 shortcut="Ctrl+S",
357 357 triggered=self.export_action_active_frontend
358 358 )
359 359 self.fileMenu.addAction(self.export_action)
360 360 except AttributeError:
361 361 self.log.error("trying to add unexisting action (Export), skipping")
362 362
363 363 try:
364 self.select_all_action = QtGui.QAction("Select All",
364 self.select_all_action = QtGui.QAction("Select &All",
365 365 self,
366 366 shortcut="Ctrl+A",
367 367 triggered=self.select_all_active_frontend
368 368 )
369 369 self.fileMenu.addAction(self.select_all_action)
370 370 except AttributeError:
371 371 self.log.error("trying to add unexisting action (select all), skipping")
372 372
373 373 try:
374 self.undo_action = QtGui.QAction("Undo",
374 self.undo_action = QtGui.QAction("&Undo",
375 375 self,
376 376 shortcut="Ctrl+Z",
377 377 statusTip="Undo last action if possible",
378 378 triggered=self.undo_active_frontend
379 379 )
380 380 self.editMenu.addAction(self.undo_action)
381 381 except AttributeError:
382 382 self.log.error("trying to add unexisting action (undo), skipping")
383 383
384 384 try:
385 self.redo_action = QtGui.QAction("Redo",
385 self.redo_action = QtGui.QAction("&Redo",
386 386 self,
387 387 shortcut="Ctrl+Shift+Z",
388 388 statusTip="Redo last action if possible",
389 389 triggered=self.redo_active_frontend)
390 390 self.editMenu.addAction(self.redo_action)
391 391 except AttributeError:
392 392 self.log.error("trying to add unexisting action (redo), skipping")
393 393
394 394 try:
395 self.increase_font_size = QtGui.QAction("Increase Font Size",
395 self.increase_font_size = QtGui.QAction("&Increase Font Size",
396 396 self,
397 397 shortcut="Ctrl++",
398 398 triggered=self.increase_font_size_active_frontend
399 399 )
400 400 self.fontMenu.addAction(self.increase_font_size)
401 401 except AttributeError:
402 402 self.log.error("trying to add unexisting action (increase font size), skipping")
403 403
404 404 try:
405 self.decrease_font_size = QtGui.QAction("Decrease Font Size",
405 self.decrease_font_size = QtGui.QAction("&Decrease Font Size",
406 406 self,
407 407 shortcut="Ctrl+-",
408 408 triggered=self.decrease_font_size_active_frontend
409 409 )
410 410 self.fontMenu.addAction(self.decrease_font_size)
411 411 except AttributeError:
412 412 self.log.error("trying to add unexisting action (decrease font size), skipping")
413 413
414 414 try:
415 self.reset_font_size = QtGui.QAction("Reset Font Size",
415 self.reset_font_size = QtGui.QAction("&Reset Font Size",
416 416 self,
417 417 shortcut="Ctrl+0",
418 418 triggered=self.reset_font_size_active_frontend
419 419 )
420 420 self.fontMenu.addAction(self.reset_font_size)
421 421 except AttributeError:
422 422 self.log.error("trying to add unexisting action (reset font size), skipping")
423 423
424 424 try:
425 self.reset_action = QtGui.QAction("Reset",
425 self.reset_action = QtGui.QAction("&Reset",
426 426 self,
427 427 statusTip="Clear all varible from workspace",
428 428 triggered=self.reset_magic_active_frontend)
429 429 self.magicMenu.addAction(self.reset_action)
430 430 except AttributeError:
431 431 self.log.error("trying to add unexisting action (reset), skipping")
432 432
433 433 try:
434 self.history_action = QtGui.QAction("History",
434 self.history_action = QtGui.QAction("&History",
435 435 self,
436 436 statusTip="show command history",
437 437 triggered=self.history_magic_active_frontend)
438 438 self.magicMenu.addAction(self.history_action)
439 439 except AttributeError:
440 440 self.log.error("trying to add unexisting action (history), skipping")
441 441
442 442 try:
443 self.save_action = QtGui.QAction("Export History ",
443 self.save_action = QtGui.QAction("E&xport History ",
444 444 self,
445 445 statusTip="Export History as Python File",
446 446 triggered=self.save_magic_active_frontend)
447 447 self.magicMenu.addAction(self.save_action)
448 448 except AttributeError:
449 449 self.log.error("trying to add unexisting action (save), skipping")
450 450
451 451 try:
452 self.clear_action = QtGui.QAction("Clear",
452 self.clear_action = QtGui.QAction("&Clear",
453 453 self,
454 454 statusTip="Clear the console",
455 455 triggered=self.clear_magic_active_frontend)
456 456 self.magicMenu.addAction(self.clear_action)
457 457 except AttributeError:
458 458 self.log.error("trying to add unexisting action, skipping")
459 459
460 460 try:
461 self.who_action = QtGui.QAction("Who",
461 self.who_action = QtGui.QAction("&Who",
462 462 self,
463 463 statusTip="List interactive variable",
464 464 triggered=self.who_magic_active_frontend)
465 465 self.magicMenu.addAction(self.who_action)
466 466 except AttributeError:
467 467 self.log.error("trying to add unexisting action (who), skipping")
468 468
469 469 try:
470 self.who_ls_action = QtGui.QAction("Who ls",
470 self.who_ls_action = QtGui.QAction("Wh&o ls",
471 471 self,
472 472 statusTip="Return a list of interactive variable",
473 473 triggered=self.who_ls_magic_active_frontend)
474 474 self.magicMenu.addAction(self.who_ls_action)
475 475 except AttributeError:
476 476 self.log.error("trying to add unexisting action (who_ls), skipping")
477 477
478 478 try:
479 self.whos_action = QtGui.QAction("Whos",
479 self.whos_action = QtGui.QAction("Who&s",
480 480 self,
481 481 statusTip="List interactive variable with detail",
482 482 triggered=self.whos_magic_active_frontend)
483 483 self.magicMenu.addAction(self.whos_action)
484 484 except AttributeError:
485 485 self.log.error("trying to add unexisting action (whos), skipping")
486 486
487 487 def undo_active_frontend(self):
488 488 self.activeFrontend.undo()
489 489
490 490 def redo_active_frontend(self):
491 491 self.activeFrontend.redo()
492 492 def reset_magic_active_frontend(self):
493 493 self.activeFrontend.reset_magic()
494 494 def history_magic_active_frontend(self):
495 495 self.activeFrontend.history_magic()
496 496 def save_magic_active_frontend(self):
497 497 self.activeFrontend.save_magic()
498 498 def clear_magic_active_frontend(self):
499 499 self.activeFrontend.clear_magic()
500 500 def who_magic_active_frontend(self):
501 501 self.activeFrontend.who_magic()
502 502 def who_ls_magic_active_frontend(self):
503 503 self.activeFrontend.who_ls_magic()
504 504 def whos_magic_active_frontend(self):
505 505 self.activeFrontend.whos_magic()
506 506
507 507 def print_action_active_frontend(self):
508 508 self.activeFrontend.print_action.trigger()
509 509
510 510 def export_action_active_frontend(self):
511 511 self.activeFrontend.export_action.trigger()
512 512
513 513 def select_all_active_frontend(self):
514 514 self.activeFrontend.select_all_action.trigger()
515 515
516 516 def increase_font_size_active_frontend(self):
517 517 self.activeFrontend.increase_font_size.trigger()
518 518 def decrease_font_size_active_frontend(self):
519 519 self.activeFrontend.decrease_font_size.trigger()
520 520 def reset_font_size_active_frontend(self):
521 521 self.activeFrontend.reset_font_size.trigger()
522 522 #---------------------------------------------------------------------------
523 523 # QWidget interface
524 524 #---------------------------------------------------------------------------
525 525
526 526 def closeEvent(self, event):
527 527 """ Forward the close event to every tabs contained by the windows
528 528 """
529 529 # Do Not loop on the widget count as it change while closing
530 530 widgetList=[ self.tabWidget.widget(i) for i in range(self.tabWidget.count())]
531 531 for widget in widgetList:
532 532 self.closeTab(widget)
533 533 event.accept()
534 534
535 535 #-----------------------------------------------------------------------------
536 536 # Aliases and Flags
537 537 #-----------------------------------------------------------------------------
538 538
539 539 flags = dict(ipkernel_flags)
540 540 qt_flags = {
541 541 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
542 542 "Connect to an existing kernel. If no argument specified, guess most recent"),
543 543 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
544 544 "Use a pure Python kernel instead of an IPython kernel."),
545 545 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
546 546 "Disable rich text support."),
547 547 }
548 548 qt_flags.update(boolean_flag(
549 549 'gui-completion', 'ConsoleWidget.gui_completion',
550 550 "use a GUI widget for tab completion",
551 551 "use plaintext output for completion"
552 552 ))
553 553 qt_flags.update(boolean_flag(
554 554 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
555 555 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
556 556 to force a direct exit without any confirmation.
557 557 """,
558 558 """Don't prompt the user when exiting. This will terminate the kernel
559 559 if it is owned by the frontend, and leave it alive if it is external.
560 560 """
561 561 ))
562 562 flags.update(qt_flags)
563 563
564 564 aliases = dict(ipkernel_aliases)
565 565
566 566 qt_aliases = dict(
567 567 hb = 'IPythonQtConsoleApp.hb_port',
568 568 shell = 'IPythonQtConsoleApp.shell_port',
569 569 iopub = 'IPythonQtConsoleApp.iopub_port',
570 570 stdin = 'IPythonQtConsoleApp.stdin_port',
571 571 ip = 'IPythonQtConsoleApp.ip',
572 572 existing = 'IPythonQtConsoleApp.existing',
573 573 f = 'IPythonQtConsoleApp.connection_file',
574 574
575 575 style = 'IPythonWidget.syntax_style',
576 576 stylesheet = 'IPythonQtConsoleApp.stylesheet',
577 577 colors = 'ZMQInteractiveShell.colors',
578 578
579 579 editor = 'IPythonWidget.editor',
580 580 paging = 'ConsoleWidget.paging',
581 581 ssh = 'IPythonQtConsoleApp.sshserver',
582 582 )
583 583 aliases.update(qt_aliases)
584 584
585 585
586 586 #-----------------------------------------------------------------------------
587 587 # IPythonQtConsole
588 588 #-----------------------------------------------------------------------------
589 589
590 590
591 591 class IPythonQtConsoleApp(BaseIPythonApplication):
592 592 name = 'ipython-qtconsole'
593 593 default_config_file_name='ipython_config.py'
594 594
595 595 description = """
596 596 The IPython QtConsole.
597 597
598 598 This launches a Console-style application using Qt. It is not a full
599 599 console, in that launched terminal subprocesses will not be able to accept
600 600 input.
601 601
602 602 The QtConsole supports various extra features beyond the Terminal IPython
603 603 shell, such as inline plotting with matplotlib, via:
604 604
605 605 ipython qtconsole --pylab=inline
606 606
607 607 as well as saving your session as HTML, and printing the output.
608 608
609 609 """
610 610 examples = _examples
611 611
612 612 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
613 613 flags = Dict(flags)
614 614 aliases = Dict(aliases)
615 615
616 616 kernel_argv = List(Unicode)
617 617
618 618 # create requested profiles by default, if they don't exist:
619 619 auto_create = CBool(True)
620 620 # connection info:
621 621 ip = Unicode(LOCALHOST, config=True,
622 622 help="""Set the kernel\'s IP address [default localhost].
623 623 If the IP address is something other than localhost, then
624 624 Consoles on other machines will be able to connect
625 625 to the Kernel, so be careful!"""
626 626 )
627 627
628 628 sshserver = Unicode('', config=True,
629 629 help="""The SSH server to use to connect to the kernel.""")
630 630 sshkey = Unicode('', config=True,
631 631 help="""Path to the ssh key to use for logging in to the ssh server.""")
632 632
633 633 hb_port = Int(0, config=True,
634 634 help="set the heartbeat port [default: random]")
635 635 shell_port = Int(0, config=True,
636 636 help="set the shell (XREP) port [default: random]")
637 637 iopub_port = Int(0, config=True,
638 638 help="set the iopub (PUB) port [default: random]")
639 639 stdin_port = Int(0, config=True,
640 640 help="set the stdin (XREQ) port [default: random]")
641 641 connection_file = Unicode('', config=True,
642 642 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
643 643
644 644 This file will contain the IP, ports, and authentication key needed to connect
645 645 clients to this kernel. By default, this file will be created in the security-dir
646 646 of the current profile, but can be specified by absolute path.
647 647 """)
648 648 def _connection_file_default(self):
649 649 return 'kernel-%i.json' % os.getpid()
650 650
651 651 existing = Unicode('', config=True,
652 652 help="""Connect to an already running kernel""")
653 653
654 654 stylesheet = Unicode('', config=True,
655 655 help="path to a custom CSS stylesheet")
656 656
657 657 pure = CBool(False, config=True,
658 658 help="Use a pure Python kernel instead of an IPython kernel.")
659 659 plain = CBool(False, config=True,
660 660 help="Use a plaintext widget instead of rich text (plain can't print/save).")
661 661
662 662 def _pure_changed(self, name, old, new):
663 663 kind = 'plain' if self.plain else 'rich'
664 664 self.config.ConsoleWidget.kind = kind
665 665 if self.pure:
666 666 self.widget_factory = FrontendWidget
667 667 elif self.plain:
668 668 self.widget_factory = IPythonWidget
669 669 else:
670 670 self.widget_factory = RichIPythonWidget
671 671
672 672 _plain_changed = _pure_changed
673 673
674 674 confirm_exit = CBool(True, config=True,
675 675 help="""
676 676 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
677 677 to force a direct exit without any confirmation.""",
678 678 )
679 679
680 680 # the factory for creating a widget
681 681 widget_factory = Any(RichIPythonWidget)
682 682
683 683 def parse_command_line(self, argv=None):
684 684 super(IPythonQtConsoleApp, self).parse_command_line(argv)
685 685 if argv is None:
686 686 argv = sys.argv[1:]
687 687
688 688 self.kernel_argv = list(argv) # copy
689 689 # kernel should inherit default config file from frontend
690 690 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
691 691 # Scrub frontend-specific flags
692 692 for a in argv:
693 693 if a.startswith('-') and a.lstrip('-') in qt_flags:
694 694 self.kernel_argv.remove(a)
695 695 swallow_next = False
696 696 for a in argv:
697 697 if swallow_next:
698 698 self.kernel_argv.remove(a)
699 699 swallow_next = False
700 700 continue
701 701 if a.startswith('-'):
702 702 split = a.lstrip('-').split('=')
703 703 alias = split[0]
704 704 if alias in qt_aliases:
705 705 self.kernel_argv.remove(a)
706 706 if len(split) == 1:
707 707 # alias passed with arg via space
708 708 swallow_next = True
709 709
710 710 def init_connection_file(self):
711 711 """find the connection file, and load the info if found.
712 712
713 713 The current working directory and the current profile's security
714 714 directory will be searched for the file if it is not given by
715 715 absolute path.
716 716
717 717 When attempting to connect to an existing kernel and the `--existing`
718 718 argument does not match an existing file, it will be interpreted as a
719 719 fileglob, and the matching file in the current profile's security dir
720 720 with the latest access time will be used.
721 721 """
722 722 if self.existing:
723 723 try:
724 724 cf = find_connection_file(self.existing)
725 725 except Exception:
726 726 self.log.critical("Could not find existing kernel connection file %s", self.existing)
727 727 self.exit(1)
728 728 self.log.info("Connecting to existing kernel: %s" % cf)
729 729 self.connection_file = cf
730 730 # should load_connection_file only be used for existing?
731 731 # as it is now, this allows reusing ports if an existing
732 732 # file is requested
733 733 try:
734 734 self.load_connection_file()
735 735 except Exception:
736 736 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
737 737 self.exit(1)
738 738
739 739 def load_connection_file(self):
740 740 """load ip/port/hmac config from JSON connection file"""
741 741 # this is identical to KernelApp.load_connection_file
742 742 # perhaps it can be centralized somewhere?
743 743 try:
744 744 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
745 745 except IOError:
746 746 self.log.debug("Connection File not found: %s", self.connection_file)
747 747 return
748 748 self.log.debug(u"Loading connection file %s", fname)
749 749 with open(fname) as f:
750 750 s = f.read()
751 751 cfg = json.loads(s)
752 752 if self.ip == LOCALHOST and 'ip' in cfg:
753 753 # not overridden by config or cl_args
754 754 self.ip = cfg['ip']
755 755 for channel in ('hb', 'shell', 'iopub', 'stdin'):
756 756 name = channel + '_port'
757 757 if getattr(self, name) == 0 and name in cfg:
758 758 # not overridden by config or cl_args
759 759 setattr(self, name, cfg[name])
760 760 if 'key' in cfg:
761 761 self.config.Session.key = str_to_bytes(cfg['key'])
762 762
763 763 def init_ssh(self):
764 764 """set up ssh tunnels, if needed."""
765 765 if not self.sshserver and not self.sshkey:
766 766 return
767 767
768 768 if self.sshkey and not self.sshserver:
769 769 # specifying just the key implies that we are connecting directly
770 770 self.sshserver = self.ip
771 771 self.ip = LOCALHOST
772 772
773 773 # build connection dict for tunnels:
774 774 info = dict(ip=self.ip,
775 775 shell_port=self.shell_port,
776 776 iopub_port=self.iopub_port,
777 777 stdin_port=self.stdin_port,
778 778 hb_port=self.hb_port
779 779 )
780 780
781 781 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
782 782
783 783 # tunnels return a new set of ports, which will be on localhost:
784 784 self.ip = LOCALHOST
785 785 try:
786 786 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
787 787 except:
788 788 # even catch KeyboardInterrupt
789 789 self.log.error("Could not setup tunnels", exc_info=True)
790 790 self.exit(1)
791 791
792 792 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
793 793
794 794 cf = self.connection_file
795 795 base,ext = os.path.splitext(cf)
796 796 base = os.path.basename(base)
797 797 self.connection_file = os.path.basename(base)+'-ssh'+ext
798 798 self.log.critical("To connect another client via this tunnel, use:")
799 799 self.log.critical("--existing %s" % self.connection_file)
800 800
801 801 def init_kernel_manager(self):
802 802 # Don't let Qt or ZMQ swallow KeyboardInterupts.
803 803 signal.signal(signal.SIGINT, signal.SIG_DFL)
804 804 sec = self.profile_dir.security_dir
805 805 try:
806 806 cf = filefind(self.connection_file, ['.', sec])
807 807 except IOError:
808 808 # file might not exist
809 809 if self.connection_file == os.path.basename(self.connection_file):
810 810 # just shortname, put it in security dir
811 811 cf = os.path.join(sec, self.connection_file)
812 812 else:
813 813 cf = self.connection_file
814 814
815 815 # Create a KernelManager and start a kernel.
816 816 self.kernel_manager = QtKernelManager(
817 817 ip=self.ip,
818 818 shell_port=self.shell_port,
819 819 iopub_port=self.iopub_port,
820 820 stdin_port=self.stdin_port,
821 821 hb_port=self.hb_port,
822 822 connection_file=cf,
823 823 config=self.config,
824 824 )
825 825 # start the kernel
826 826 if not self.existing:
827 827 kwargs = dict(ipython=not self.pure)
828 828 kwargs['extra_arguments'] = self.kernel_argv
829 829 self.kernel_manager.start_kernel(**kwargs)
830 830 elif self.sshserver:
831 831 # ssh, write new connection file
832 832 self.kernel_manager.write_connection_file()
833 833 self.kernel_manager.start_channels()
834 834
835 835 def createTabWithNewFrontend(self):
836 836 """ Create new tab attached to new kernel, launched on localhost.
837 837 """
838 838 kernel_manager = QtKernelManager(
839 839 shell_address=(LOCALHOST,0 ),
840 840 sub_address=(LOCALHOST, 0),
841 841 stdin_address=(LOCALHOST, 0),
842 842 hb_address=(LOCALHOST, 0),
843 843 config=self.config
844 844 )
845 845 # start the kernel
846 846 kwargs = dict(ip=LOCALHOST, ipython=not self.pure)
847 847 kwargs['extra_arguments'] = self.kernel_argv
848 848 kernel_manager.start_kernel(**kwargs)
849 849 kernel_manager.start_channels()
850 850 local_kernel = (not False) or self.ip in LOCAL_IPS
851 851 widget = self.widget_factory(config=self.config,
852 852 local_kernel=local_kernel)
853 853 widget.kernel_manager = kernel_manager
854 854 widget._existing=False;
855 855 widget._confirm_exit=True;
856 856 widget._may_close=True;
857 857 self.window.addTabWithFrontend(widget)
858 858
859 859 def createTabAttachedToCurrentTabKernel(self):
860 860 currentWidget = self.window.tabWidget.currentWidget()
861 861 currentWidgetIndex = self.window.tabWidget.indexOf(currentWidget)
862 862 currentWidget.kernel_manager = currentWidget.kernel_manager;
863 863 currentWidgetName = self.window.tabWidget.tabText(currentWidgetIndex);
864 864 kernel_manager = QtKernelManager(
865 865 shell_address = currentWidget.kernel_manager.shell_address,
866 866 sub_address = currentWidget.kernel_manager.sub_address,
867 867 stdin_address = currentWidget.kernel_manager.stdin_address,
868 868 hb_address = currentWidget.kernel_manager.hb_address,
869 869 config = self.config
870 870 )
871 871 kernel_manager.start_channels()
872 872 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
873 873 widget = self.widget_factory(config=self.config,
874 874 local_kernel=False)
875 875 widget._confirm_exit=True;
876 876 widget._may_close=False;
877 877 widget.kernel_manager = kernel_manager
878 878 self.window.addTabWithFrontend(widget,name=str('('+currentWidgetName+') slave'))
879 879
880 880 def init_qt_elements(self):
881 881 # Create the widget.
882 882 self.app = QtGui.QApplication([])
883 883 pixmap=QtGui.QPixmap(':/icon/IPythonConsole.png')
884 884 icon=QtGui.QIcon(pixmap)
885 885 QtGui.QApplication.setWindowIcon(icon)
886 886
887 887 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
888 888 self.widget = self.widget_factory(config=self.config,
889 889 local_kernel=local_kernel)
890 890 self.widget._existing = self.existing;
891 891 self.widget._may_close = not self.existing;
892 892 self.widget._confirm_exit = not self.existing;
893 893
894 894 self.widget.kernel_manager = self.kernel_manager
895 895 self.window = MainWindow(self.app, self.widget, self.existing,
896 896 may_close=local_kernel,
897 897 confirm_exit=self.confirm_exit)
898 898 self.window.log = self.log
899 899 self.window.addTabWithFrontend(self.widget)
900 900 self.window.initMenuBar()
901 901 self.window.setWindowTitle('Python' if self.pure else 'IPython')
902 902
903 903 def init_colors(self):
904 904 """Configure the coloring of the widget"""
905 905 # Note: This will be dramatically simplified when colors
906 906 # are removed from the backend.
907 907
908 908 if self.pure:
909 909 # only IPythonWidget supports styling
910 910 return
911 911
912 912 # parse the colors arg down to current known labels
913 913 try:
914 914 colors = self.config.ZMQInteractiveShell.colors
915 915 except AttributeError:
916 916 colors = None
917 917 try:
918 918 style = self.config.IPythonWidget.colors
919 919 except AttributeError:
920 920 style = None
921 921
922 922 # find the value for colors:
923 923 if colors:
924 924 colors=colors.lower()
925 925 if colors in ('lightbg', 'light'):
926 926 colors='lightbg'
927 927 elif colors in ('dark', 'linux'):
928 928 colors='linux'
929 929 else:
930 930 colors='nocolor'
931 931 elif style:
932 932 if style=='bw':
933 933 colors='nocolor'
934 934 elif styles.dark_style(style):
935 935 colors='linux'
936 936 else:
937 937 colors='lightbg'
938 938 else:
939 939 colors=None
940 940
941 941 # Configure the style.
942 942 widget = self.widget
943 943 if style:
944 944 widget.style_sheet = styles.sheet_from_template(style, colors)
945 945 widget.syntax_style = style
946 946 widget._syntax_style_changed()
947 947 widget._style_sheet_changed()
948 948 elif colors:
949 949 # use a default style
950 950 widget.set_default_style(colors=colors)
951 951 else:
952 952 # this is redundant for now, but allows the widget's
953 953 # defaults to change
954 954 widget.set_default_style()
955 955
956 956 if self.stylesheet:
957 957 # we got an expicit stylesheet
958 958 if os.path.isfile(self.stylesheet):
959 959 with open(self.stylesheet) as f:
960 960 sheet = f.read()
961 961 widget.style_sheet = sheet
962 962 widget._style_sheet_changed()
963 963 else:
964 964 raise IOError("Stylesheet %r not found."%self.stylesheet)
965 965
966 966 def initialize(self, argv=None):
967 967 super(IPythonQtConsoleApp, self).initialize(argv)
968 968 self.init_connection_file()
969 969 default_secure(self.config)
970 970 self.init_ssh()
971 971 self.init_kernel_manager()
972 972 self.init_qt_elements()
973 973 self.init_colors()
974 974 self.init_window_shortcut()
975 975
976 976 def init_window_shortcut(self):
977 977
978 978 self.prevTabAct = QtGui.QAction("Pre&vious Tab",
979 979 self.window,
980 980 shortcut="Ctrl+PgDown",
981 981 statusTip="Cahange to next tab",
982 982 triggered=self.window.prevTab)
983 983
984 984 self.nextTabAct = QtGui.QAction("Ne&xt Tab",
985 985 self.window,
986 986 shortcut="Ctrl+PgUp",
987 987 statusTip="Cahange to next tab",
988 988 triggered=self.window.nextTab)
989 989
990 self.fullScreenAct = QtGui.QAction("Full Screen",
990 self.fullScreenAct = QtGui.QAction("&Full Screen",
991 991 self.window,
992 992 shortcut="Ctrl+Meta+Space",
993 993 statusTip="Toggle between Fullscreen and Normal Size",
994 994 triggered=self.toggleFullScreen)
995 995
996 self.tabAndNewKernelAct =QtGui.QAction("Tab with New kernel",
996 self.tabAndNewKernelAct =QtGui.QAction("Tab with &New kernel",
997 997 self.window,
998 998 shortcut="Ctrl+T",
999 999 triggered=self.createTabWithNewFrontend)
1000 1000 self.window.windowMenu.addAction(self.tabAndNewKernelAct)
1001 self.tabSameKernalAct =QtGui.QAction("Tab with Same kernel",
1001 self.tabSameKernalAct =QtGui.QAction("Tab with Sa&me kernel",
1002 1002 self.window,
1003 1003 shortcut="Ctrl+Shift+T",
1004 1004 triggered=self.createTabAttachedToCurrentTabKernel)
1005 1005 self.window.windowMenu.addAction(self.tabSameKernalAct)
1006 1006 self.window.windowMenu.addSeparator()
1007 1007
1008 1008 # creating shortcut in menubar only for Mac OS as I don't
1009 1009 # know the shortcut or if the windows manager assign it in
1010 1010 # other platform.
1011 1011 if sys.platform == 'darwin':
1012 self.minimizeAct = QtGui.QAction("Minimize",
1012 self.minimizeAct = QtGui.QAction("Mini&mize",
1013 1013 self.window,
1014 1014 shortcut="Ctrl+m",
1015 1015 statusTip="Minimize the window/Restore Normal Size",
1016 1016 triggered=self.toggleMinimized)
1017 self.maximizeAct = QtGui.QAction("Maximize",
1017 self.maximizeAct = QtGui.QAction("Ma&ximize",
1018 1018 self.window,
1019 1019 shortcut="Ctrl+Shift+M",
1020 1020 statusTip="Maximize the window/Restore Normal Size",
1021 1021 triggered=self.toggleMaximized)
1022 1022
1023 self.onlineHelpAct = QtGui.QAction("Open Online Help",
1023 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
1024 1024 self.window,
1025 1025 triggered=self._open_online_help)
1026 1026
1027 1027 self.windowMenu = self.window.windowMenu
1028 1028
1029 1029 self.windowMenu.addAction(self.nextTabAct)
1030 1030 self.windowMenu.addAction(self.prevTabAct)
1031 1031 self.windowMenu.addSeparator()
1032 1032 self.windowMenu.addAction(self.minimizeAct)
1033 1033 self.windowMenu.addAction(self.maximizeAct)
1034 1034 self.windowMenu.addSeparator()
1035 1035 self.windowMenu.addAction(self.fullScreenAct)
1036 1036
1037 1037 self.window.helpMenu.addAction(self.onlineHelpAct)
1038 1038 else:
1039 1039 # if we don't put it in a menu, we add it to the window so
1040 1040 # that it can still be triggerd by shortcut
1041 1041 self.window.addAction(self.fullScreenAct)
1042 1042
1043 1043 def toggleMinimized(self):
1044 1044 if not self.window.isMinimized():
1045 1045 self.window.showMinimized()
1046 1046 else:
1047 1047 self.window.showNormal()
1048 1048
1049 1049 def _open_online_help(self):
1050 1050 QtGui.QDesktopServices.openUrl(
1051 1051 QtCore.QUrl("http://ipython.org/documentation.html",
1052 1052 QtCore.QUrl.TolerantMode)
1053 1053 )
1054 1054
1055 1055 def toggleMaximized(self):
1056 1056 if not self.window.isMaximized():
1057 1057 self.window.showMaximized()
1058 1058 else:
1059 1059 self.window.showNormal()
1060 1060
1061 1061 # Min/Max imizing while in full screen give a bug
1062 1062 # when going out of full screen, at least on OSX
1063 1063 def toggleFullScreen(self):
1064 1064 if not self.window.isFullScreen():
1065 1065 self.window.showFullScreen()
1066 1066 if sys.platform == 'darwin':
1067 1067 self.maximizeAct.setEnabled(False)
1068 1068 self.minimizeAct.setEnabled(False)
1069 1069 else:
1070 1070 self.window.showNormal()
1071 1071 if sys.platform == 'darwin':
1072 1072 self.maximizeAct.setEnabled(True)
1073 1073 self.minimizeAct.setEnabled(True)
1074 1074
1075 1075 def start(self):
1076 1076
1077 1077 # draw the window
1078 1078 self.window.show()
1079 1079
1080 1080 # Start the application main loop.
1081 1081 self.app.exec_()
1082 1082
1083 1083 #-----------------------------------------------------------------------------
1084 1084 # Main entry point
1085 1085 #-----------------------------------------------------------------------------
1086 1086
1087 1087 def main():
1088 1088 app = IPythonQtConsoleApp()
1089 1089 app.initialize()
1090 1090 app.start()
1091 1091
1092 1092
1093 1093 if __name__ == '__main__':
1094 1094 main()
General Comments 0
You need to be logged in to leave comments. Login now