##// END OF EJS Templates
Temporary fix to work around #1057....
Fernando Perez -
Show More
@@ -1,906 +1,957 b''
1 1 """The Qt MainWindow for the QtConsole
2 2
3 3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
4 4 common actions.
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 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # stdlib imports
22 22 import sys
23 23 import re
24 24 import webbrowser
25 25 from threading import Thread
26 26
27 27 # System library imports
28 28 from IPython.external.qt import QtGui,QtCore
29 29
30 30 def background(f):
31 31 """call a function in a simple thread, to prevent blocking"""
32 32 t = Thread(target=f)
33 33 t.start()
34 34 return t
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Classes
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class MainWindow(QtGui.QMainWindow):
41 41
42 42 #---------------------------------------------------------------------------
43 43 # 'object' interface
44 44 #---------------------------------------------------------------------------
45 45
46 46 def __init__(self, app,
47 47 confirm_exit=True,
48 48 new_frontend_factory=None, slave_frontend_factory=None,
49 49 ):
50 50 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
51 51
52 52 Parameters
53 53 ----------
54 54
55 55 app : reference to QApplication parent
56 56 confirm_exit : bool, optional
57 57 Whether we should prompt on close of tabs
58 58 new_frontend_factory : callable
59 59 A callable that returns a new IPythonWidget instance, attached to
60 60 its own running kernel.
61 61 slave_frontend_factory : callable
62 62 A callable that takes an existing IPythonWidget, and returns a new
63 63 IPythonWidget instance, attached to the same kernel.
64 64 """
65 65
66 66 super(MainWindow, self).__init__()
67 67 self._kernel_counter = 0
68 68 self._app = app
69 69 self.confirm_exit = confirm_exit
70 70 self.new_frontend_factory = new_frontend_factory
71 71 self.slave_frontend_factory = slave_frontend_factory
72 72
73 73 self.tab_widget = QtGui.QTabWidget(self)
74 74 self.tab_widget.setDocumentMode(True)
75 75 self.tab_widget.setTabsClosable(True)
76 76 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
77 77
78 78 self.setCentralWidget(self.tab_widget)
79 79 # hide tab bar at first, since we have no tabs:
80 80 self.tab_widget.tabBar().setVisible(False)
81 81 # prevent focus in tab bar
82 82 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
83 83
84 84 def update_tab_bar_visibility(self):
85 85 """ update visibility of the tabBar depending of the number of tab
86 86
87 87 0 or 1 tab, tabBar hidden
88 88 2+ tabs, tabBar visible
89 89
90 90 send a self.close if number of tab ==0
91 91
92 92 need to be called explicitely, or be connected to tabInserted/tabRemoved
93 93 """
94 94 if self.tab_widget.count() <= 1:
95 95 self.tab_widget.tabBar().setVisible(False)
96 96 else:
97 97 self.tab_widget.tabBar().setVisible(True)
98 98 if self.tab_widget.count()==0 :
99 99 self.close()
100 100
101 101 @property
102 102 def next_kernel_id(self):
103 103 """constantly increasing counter for kernel IDs"""
104 104 c = self._kernel_counter
105 105 self._kernel_counter += 1
106 106 return c
107 107
108 108 @property
109 109 def active_frontend(self):
110 110 return self.tab_widget.currentWidget()
111 111
112 112 def create_tab_with_new_frontend(self):
113 113 """create a new frontend and attach it to a new tab"""
114 114 widget = self.new_frontend_factory()
115 115 self.add_tab_with_frontend(widget)
116 116
117 117 def create_tab_with_current_kernel(self):
118 118 """create a new frontend attached to the same kernel as the current tab"""
119 119 current_widget = self.tab_widget.currentWidget()
120 120 current_widget_index = self.tab_widget.indexOf(current_widget)
121 121 current_widget_name = self.tab_widget.tabText(current_widget_index)
122 122 widget = self.slave_frontend_factory(current_widget)
123 123 if 'slave' in current_widget_name:
124 124 # don't keep stacking slaves
125 125 name = current_widget_name
126 126 else:
127 127 name = '(%s) slave' % current_widget_name
128 128 self.add_tab_with_frontend(widget,name=name)
129 129
130 130 def close_tab(self,current_tab):
131 131 """ Called when you need to try to close a tab.
132 132
133 133 It takes the number of the tab to be closed as argument, or a referece
134 134 to the wiget insite this tab
135 135 """
136 136
137 137 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
138 138 # and a reference to the trontend to close
139 139 if type(current_tab) is not int :
140 140 current_tab = self.tab_widget.indexOf(current_tab)
141 141 closing_widget=self.tab_widget.widget(current_tab)
142 142
143 143
144 144 # when trying to be closed, widget might re-send a request to be closed again, but will
145 145 # be deleted when event will be processed. So need to check that widget still exist and
146 146 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
147 147 # re-send by this fonction on the master widget, which ask all slaves widget to exit
148 148 if closing_widget==None:
149 149 return
150 150
151 151 #get a list of all slave widgets on the same kernel.
152 152 slave_tabs = self.find_slave_widgets(closing_widget)
153 153
154 154 keepkernel = None #Use the prompt by default
155 155 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
156 156 keepkernel = closing_widget._keep_kernel_on_exit
157 157 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
158 158 # we set local slave tabs._hidden to True to avoid prompting for kernel
159 159 # restart when they get the signal. and then "forward" the 'exit'
160 160 # to the main window
161 161 if keepkernel is not None:
162 162 for tab in slave_tabs:
163 163 tab._hidden = True
164 164 if closing_widget in slave_tabs:
165 165 try :
166 166 self.find_master_tab(closing_widget).execute('exit')
167 167 except AttributeError:
168 168 self.log.info("Master already closed or not local, closing only current tab")
169 169 self.tab_widget.removeTab(current_tab)
170 170 self.update_tab_bar_visibility()
171 171 return
172 172
173 173 kernel_manager = closing_widget.kernel_manager
174 174
175 175 if keepkernel is None and not closing_widget._confirm_exit:
176 176 # don't prompt, just terminate the kernel if we own it
177 177 # or leave it alone if we don't
178 178 keepkernel = closing_widget._existing
179 179 if keepkernel is None: #show prompt
180 180 if kernel_manager and kernel_manager.channels_running:
181 181 title = self.window().windowTitle()
182 182 cancel = QtGui.QMessageBox.Cancel
183 183 okay = QtGui.QMessageBox.Ok
184 184 if closing_widget._may_close:
185 185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
186 186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
187 187 justthis = QtGui.QPushButton("&No, just this Tab", self)
188 188 justthis.setShortcut('N')
189 189 closeall = QtGui.QPushButton("&Yes, close all", self)
190 190 closeall.setShortcut('Y')
191 191 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
192 192 title, msg)
193 193 box.setInformativeText(info)
194 194 box.addButton(cancel)
195 195 box.addButton(justthis, QtGui.QMessageBox.NoRole)
196 196 box.addButton(closeall, QtGui.QMessageBox.YesRole)
197 197 box.setDefaultButton(closeall)
198 198 box.setEscapeButton(cancel)
199 199 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
200 200 box.setIconPixmap(pixmap)
201 201 reply = box.exec_()
202 202 if reply == 1: # close All
203 203 for slave in slave_tabs:
204 204 background(slave.kernel_manager.stop_channels)
205 205 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
206 206 closing_widget.execute("exit")
207 207 self.tab_widget.removeTab(current_tab)
208 208 background(kernel_manager.stop_channels)
209 209 elif reply == 0: # close Console
210 210 if not closing_widget._existing:
211 211 # Have kernel: don't quit, just close the tab
212 212 closing_widget.execute("exit True")
213 213 self.tab_widget.removeTab(current_tab)
214 214 background(kernel_manager.stop_channels)
215 215 else:
216 216 reply = QtGui.QMessageBox.question(self, title,
217 217 "Are you sure you want to close this Console?"+
218 218 "\nThe Kernel and other Consoles will remain active.",
219 219 okay|cancel,
220 220 defaultButton=okay
221 221 )
222 222 if reply == okay:
223 223 self.tab_widget.removeTab(current_tab)
224 224 elif keepkernel: #close console but leave kernel running (no prompt)
225 225 self.tab_widget.removeTab(current_tab)
226 226 background(kernel_manager.stop_channels)
227 227 else: #close console and kernel (no prompt)
228 228 self.tab_widget.removeTab(current_tab)
229 229 if kernel_manager and kernel_manager.channels_running:
230 230 for slave in slave_tabs:
231 231 background(slave.kernel_manager.stop_channels)
232 232 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
233 233 kernel_manager.shutdown_kernel()
234 234 background(kernel_manager.stop_channels)
235 235
236 236 self.update_tab_bar_visibility()
237 237
238 238 def add_tab_with_frontend(self,frontend,name=None):
239 239 """ insert a tab with a given frontend in the tab bar, and give it a name
240 240
241 241 """
242 242 if not name:
243 243 name = 'kernel %i' % self.next_kernel_id
244 244 self.tab_widget.addTab(frontend,name)
245 245 self.update_tab_bar_visibility()
246 246 self.make_frontend_visible(frontend)
247 247 frontend.exit_requested.connect(self.close_tab)
248 248
249 249 def next_tab(self):
250 250 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
251 251
252 252 def prev_tab(self):
253 253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
254 254
255 255 def make_frontend_visible(self,frontend):
256 256 widget_index=self.tab_widget.indexOf(frontend)
257 257 if widget_index > 0 :
258 258 self.tab_widget.setCurrentIndex(widget_index)
259 259
260 260 def find_master_tab(self,tab,as_list=False):
261 261 """
262 262 Try to return the frontend that own the kernel attached to the given widget/tab.
263 263
264 264 Only find frontend owed by the current application. Selection
265 265 based on port of the kernel, might be inacurate if several kernel
266 266 on different ip use same port number.
267 267
268 268 This fonction does the conversion tabNumber/widget if needed.
269 269 Might return None if no master widget (non local kernel)
270 270 Will crash IPython if more than 1 masterWidget
271 271
272 272 When asList set to True, always return a list of widget(s) owning
273 273 the kernel. The list might be empty or containing several Widget.
274 274 """
275 275
276 276 #convert from/to int/richIpythonWidget if needed
277 277 if isinstance(tab, int):
278 278 tab = self.tab_widget.widget(tab)
279 279 km=tab.kernel_manager
280 280
281 281 #build list of all widgets
282 282 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
283 283
284 284 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
285 285 # And should have a _may_close attribute
286 286 filtered_widget_list = [ widget for widget in widget_list if
287 287 widget.kernel_manager.connection_file == km.connection_file and
288 288 hasattr(widget,'_may_close') ]
289 289 # the master widget is the one that may close the kernel
290 290 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
291 291 if as_list:
292 292 return master_widget
293 293 assert(len(master_widget)<=1 )
294 294 if len(master_widget)==0:
295 295 return None
296 296
297 297 return master_widget[0]
298 298
299 299 def find_slave_widgets(self,tab):
300 300 """return all the frontends that do not own the kernel attached to the given widget/tab.
301 301
302 302 Only find frontends owned by the current application. Selection
303 303 based on connection file of the kernel.
304 304
305 305 This function does the conversion tabNumber/widget if needed.
306 306 """
307 307 #convert from/to int/richIpythonWidget if needed
308 308 if isinstance(tab, int):
309 309 tab = self.tab_widget.widget(tab)
310 310 km=tab.kernel_manager
311 311
312 312 #build list of all widgets
313 313 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
314 314
315 315 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
316 316 filtered_widget_list = ( widget for widget in widget_list if
317 317 widget.kernel_manager.connection_file == km.connection_file)
318 318 # Get a list of all widget owning the same kernel and removed it from
319 319 # the previous cadidate. (better using sets ?)
320 320 master_widget_list = self.find_master_tab(tab, as_list=True)
321 321 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
322 322
323 323 return slave_list
324 324
325 325 # Populate the menu bar with common actions and shortcuts
326 326 def add_menu_action(self, menu, action, defer_shortcut=False):
327 327 """Add action to menu as well as self
328 328
329 329 So that when the menu bar is invisible, its actions are still available.
330 330
331 331 If defer_shortcut is True, set the shortcut context to widget-only,
332 332 where it will avoid conflict with shortcuts already bound to the
333 333 widgets themselves.
334 334 """
335 335 menu.addAction(action)
336 336 self.addAction(action)
337 337
338 338 if defer_shortcut:
339 339 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
340 340
341 341 def init_menu_bar(self):
342 342 #create menu in the order they should appear in the menu bar
343 343 self.init_file_menu()
344 344 self.init_edit_menu()
345 345 self.init_view_menu()
346 346 self.init_kernel_menu()
347 347 self.init_magic_menu()
348 348 self.init_window_menu()
349 349 self.init_help_menu()
350 350
351 351 def init_file_menu(self):
352 352 self.file_menu = self.menuBar().addMenu("&File")
353 353
354 354 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
355 355 self,
356 356 shortcut="Ctrl+T",
357 357 triggered=self.create_tab_with_new_frontend)
358 358 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
359 359
360 360 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
361 361 self,
362 362 shortcut="Ctrl+Shift+T",
363 363 triggered=self.create_tab_with_current_kernel)
364 364 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
365 365
366 366 self.file_menu.addSeparator()
367 367
368 368 self.close_action=QtGui.QAction("&Close Tab",
369 369 self,
370 370 shortcut=QtGui.QKeySequence.Close,
371 371 triggered=self.close_active_frontend
372 372 )
373 373 self.add_menu_action(self.file_menu, self.close_action)
374 374
375 375 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
376 376 self,
377 377 shortcut=QtGui.QKeySequence.Save,
378 378 triggered=self.export_action_active_frontend
379 379 )
380 380 self.add_menu_action(self.file_menu, self.export_action, True)
381 381
382 382 self.file_menu.addSeparator()
383 383
384 384 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
385 385 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
386 386 # Only override the default if there is a collision.
387 387 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
388 388 printkey = "Ctrl+Shift+P"
389 389 self.print_action = QtGui.QAction("&Print",
390 390 self,
391 391 shortcut=printkey,
392 392 triggered=self.print_action_active_frontend)
393 393 self.add_menu_action(self.file_menu, self.print_action, True)
394 394
395 395 if sys.platform != 'darwin':
396 396 # OSX always has Quit in the Application menu, only add it
397 397 # to the File menu elsewhere.
398 398
399 399 self.file_menu.addSeparator()
400 400
401 401 self.quit_action = QtGui.QAction("&Quit",
402 402 self,
403 403 shortcut=QtGui.QKeySequence.Quit,
404 404 triggered=self.close,
405 405 )
406 406 self.add_menu_action(self.file_menu, self.quit_action)
407 407
408 408
409 409 def init_edit_menu(self):
410 410 self.edit_menu = self.menuBar().addMenu("&Edit")
411 411
412 412 self.undo_action = QtGui.QAction("&Undo",
413 413 self,
414 414 shortcut=QtGui.QKeySequence.Undo,
415 415 statusTip="Undo last action if possible",
416 416 triggered=self.undo_active_frontend
417 417 )
418 418 self.add_menu_action(self.edit_menu, self.undo_action)
419 419
420 420 self.redo_action = QtGui.QAction("&Redo",
421 421 self,
422 422 shortcut=QtGui.QKeySequence.Redo,
423 423 statusTip="Redo last action if possible",
424 424 triggered=self.redo_active_frontend)
425 425 self.add_menu_action(self.edit_menu, self.redo_action)
426 426
427 427 self.edit_menu.addSeparator()
428 428
429 429 self.cut_action = QtGui.QAction("&Cut",
430 430 self,
431 431 shortcut=QtGui.QKeySequence.Cut,
432 432 triggered=self.cut_active_frontend
433 433 )
434 434 self.add_menu_action(self.edit_menu, self.cut_action, True)
435 435
436 436 self.copy_action = QtGui.QAction("&Copy",
437 437 self,
438 438 shortcut=QtGui.QKeySequence.Copy,
439 439 triggered=self.copy_active_frontend
440 440 )
441 441 self.add_menu_action(self.edit_menu, self.copy_action, True)
442 442
443 443 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
444 444 self,
445 445 shortcut="Ctrl+Shift+C",
446 446 triggered=self.copy_raw_active_frontend
447 447 )
448 448 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
449 449
450 450 self.paste_action = QtGui.QAction("&Paste",
451 451 self,
452 452 shortcut=QtGui.QKeySequence.Paste,
453 453 triggered=self.paste_active_frontend
454 454 )
455 455 self.add_menu_action(self.edit_menu, self.paste_action, True)
456 456
457 457 self.edit_menu.addSeparator()
458 458
459 459 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
460 460 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
461 461 # Only override the default if there is a collision.
462 462 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
463 463 selectall = "Ctrl+Shift+A"
464 464 self.select_all_action = QtGui.QAction("Select &All",
465 465 self,
466 466 shortcut=selectall,
467 467 triggered=self.select_all_active_frontend
468 468 )
469 469 self.add_menu_action(self.edit_menu, self.select_all_action, True)
470 470
471 471
472 472 def init_view_menu(self):
473 473 self.view_menu = self.menuBar().addMenu("&View")
474 474
475 475 if sys.platform != 'darwin':
476 476 # disable on OSX, where there is always a menu bar
477 477 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
478 478 self,
479 479 shortcut="Ctrl+Shift+M",
480 480 statusTip="Toggle visibility of menubar",
481 481 triggered=self.toggle_menu_bar)
482 482 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
483 483
484 484 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
485 485 self.full_screen_act = QtGui.QAction("&Full Screen",
486 486 self,
487 487 shortcut=fs_key,
488 488 statusTip="Toggle between Fullscreen and Normal Size",
489 489 triggered=self.toggleFullScreen)
490 490 self.add_menu_action(self.view_menu, self.full_screen_act)
491 491
492 492 self.view_menu.addSeparator()
493 493
494 494 self.increase_font_size = QtGui.QAction("Zoom &In",
495 495 self,
496 496 shortcut=QtGui.QKeySequence.ZoomIn,
497 497 triggered=self.increase_font_size_active_frontend
498 498 )
499 499 self.add_menu_action(self.view_menu, self.increase_font_size, True)
500 500
501 501 self.decrease_font_size = QtGui.QAction("Zoom &Out",
502 502 self,
503 503 shortcut=QtGui.QKeySequence.ZoomOut,
504 504 triggered=self.decrease_font_size_active_frontend
505 505 )
506 506 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
507 507
508 508 self.reset_font_size = QtGui.QAction("Zoom &Reset",
509 509 self,
510 510 shortcut="Ctrl+0",
511 511 triggered=self.reset_font_size_active_frontend
512 512 )
513 513 self.add_menu_action(self.view_menu, self.reset_font_size, True)
514 514
515 515 self.view_menu.addSeparator()
516 516
517 517 self.clear_action = QtGui.QAction("&Clear Screen",
518 518 self,
519 519 shortcut='Ctrl+L',
520 520 statusTip="Clear the console",
521 521 triggered=self.clear_magic_active_frontend)
522 522 self.add_menu_action(self.view_menu, self.clear_action)
523 523
524 524 def init_kernel_menu(self):
525 525 self.kernel_menu = self.menuBar().addMenu("&Kernel")
526 526 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
527 527 # keep the signal shortcuts to ctrl, rather than
528 528 # platform-default like we do elsewhere.
529 529
530 530 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
531 531
532 532 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
533 533 self,
534 534 triggered=self.interrupt_kernel_active_frontend,
535 535 shortcut=ctrl+"+C",
536 536 )
537 537 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
538 538
539 539 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
540 540 self,
541 541 triggered=self.restart_kernel_active_frontend,
542 542 shortcut=ctrl+"+.",
543 543 )
544 544 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
545 545
546 546 self.kernel_menu.addSeparator()
547 547
548 548 def _make_dynamic_magic(self,magic):
549 549 """Return a function `fun` that will execute `magic` on active frontend.
550 550
551 551 Parameters
552 552 ----------
553 553 magic : string
554 554 string that will be executed as is when the returned function is called
555 555
556 556 Returns
557 557 -------
558 558 fun : function
559 559 function with no parameters, when called will execute `magic` on the
560 560 current active frontend at call time
561 561
562 562 See Also
563 563 --------
564 564 populate_all_magic_menu : generate the "All Magics..." menu
565 565
566 566 Notes
567 567 -----
568 568 `fun` execute `magic` an active frontend at the moment it is triggerd,
569 569 not the active frontend at the moment it has been created.
570 570
571 571 This function is mostly used to create the "All Magics..." Menu at run time.
572 572 """
573 573 # need to level nested function to be sure to past magic
574 574 # on active frontend **at run time**.
575 575 def inner_dynamic_magic():
576 576 self.active_frontend.execute(magic)
577 577 inner_dynamic_magic.__name__ = "dynamics_magic_s"
578 578 return inner_dynamic_magic
579 579
580 580 def populate_all_magic_menu(self, listofmagic=None):
581 581 """Clean "All Magics..." menu and repopulate it with `listofmagic`
582 582
583 583 Parameters
584 584 ----------
585 585 listofmagic : string,
586 586 repr() of a list of strings, send back by the kernel
587 587
588 588 Notes
589 589 -----
590 590 `listofmagic`is a repr() of list because it is fed with the result of
591 591 a 'user_expression'
592 592 """
593 593 alm_magic_menu = self.all_magic_menu
594 594 alm_magic_menu.clear()
595 595
596 596 # list of protected magic that don't like to be called without argument
597 597 # append '?' to the end to print the docstring when called from the menu
598 598 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
599 599 magics=re.findall('\w+', listofmagic)
600 600 for magic in magics:
601 601 if magic in protected_magic:
602 602 pmagic = '%s%s%s'%('%',magic,'?')
603 603 else:
604 604 pmagic = '%s%s'%('%',magic)
605 605 xaction = QtGui.QAction(pmagic,
606 606 self,
607 607 triggered=self._make_dynamic_magic(pmagic)
608 608 )
609 609 alm_magic_menu.addAction(xaction)
610 610
611 611 def update_all_magic_menu(self):
612 612 """ Update the list on magic in the "All Magics..." Menu
613 613
614 614 Request the kernel with the list of availlable magic and populate the
615 615 menu with the list received back
616 616
617 617 """
618 618 # first define a callback which will get the list of all magic and put it in the menu.
619 619 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu)
620 620
621 621 def init_magic_menu(self):
622 622 self.magic_menu = self.menuBar().addMenu("&Magic")
623 623 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
624 624
625 625 # This action should usually not appear as it will be cleared when menu
626 626 # is updated at first kernel response. Though, it is necessary when
627 627 # connecting through X-forwarding, as in this case, the menu is not
628 628 # auto updated, SO DO NOT DELETE.
629 self.pop = QtGui.QAction("&Update All Magic Menu ",
630 self, triggered=self.update_all_magic_menu)
631 self.add_menu_action(self.all_magic_menu, self.pop)
629
630 ########################################################################
631 ## TEMPORARILY DISABLED - see #1057 for details. Uncomment this
632 ## section when a proper fix is found
633
634 ## self.pop = QtGui.QAction("&Update All Magic Menu ",
635 ## self, triggered=self.update_all_magic_menu)
636 ## self.add_menu_action(self.all_magic_menu, self.pop)
637
638 ## END TEMPORARY FIX
639 ########################################################################
632 640
633 641 self.reset_action = QtGui.QAction("&Reset",
634 642 self,
635 643 statusTip="Clear all varible from workspace",
636 644 triggered=self.reset_magic_active_frontend)
637 645 self.add_menu_action(self.magic_menu, self.reset_action)
638 646
639 647 self.history_action = QtGui.QAction("&History",
640 648 self,
641 649 statusTip="show command history",
642 650 triggered=self.history_magic_active_frontend)
643 651 self.add_menu_action(self.magic_menu, self.history_action)
644 652
645 653 self.save_action = QtGui.QAction("E&xport History ",
646 654 self,
647 655 statusTip="Export History as Python File",
648 656 triggered=self.save_magic_active_frontend)
649 657 self.add_menu_action(self.magic_menu, self.save_action)
650 658
651 659 self.who_action = QtGui.QAction("&Who",
652 660 self,
653 661 statusTip="List interactive variable",
654 662 triggered=self.who_magic_active_frontend)
655 663 self.add_menu_action(self.magic_menu, self.who_action)
656 664
657 665 self.who_ls_action = QtGui.QAction("Wh&o ls",
658 666 self,
659 667 statusTip="Return a list of interactive variable",
660 668 triggered=self.who_ls_magic_active_frontend)
661 669 self.add_menu_action(self.magic_menu, self.who_ls_action)
662 670
663 671 self.whos_action = QtGui.QAction("Who&s",
664 672 self,
665 673 statusTip="List interactive variable with detail",
666 674 triggered=self.whos_magic_active_frontend)
667 675 self.add_menu_action(self.magic_menu, self.whos_action)
668 676
677
678 ########################################################################
679 ## TEMPORARILY ADDED BACK - see #1057 for details. The magic menu is
680 ## supposed to be dynamic, but the mechanism merged in #1057 has a race
681 ## condition that locks up the console very often. We're putting back
682 ## the static list temporarily/ Remove this code when a proper fix is
683 ## found.
684
685 # allmagics submenu.
686
687 # for now this is just a copy and paste, but we should get this
688 # dynamically
689 magiclist = ["%alias", "%autocall", "%automagic", "%bookmark", "%cd",
690 "%clear", "%colors", "%debug", "%dhist", "%dirs", "%doctest_mode",
691 "%ed", "%edit", "%env", "%gui", "%guiref", "%hist", "%history",
692 "%install_default_config", "%install_profiles", "%less", "%load_ext",
693 "%loadpy", "%logoff", "%logon", "%logstart", "%logstate", "%logstop",
694 "%lsmagic", "%macro", "%magic", "%man", "%more", "%notebook", "%page",
695 "%pastebin", "%pdb", "%pdef", "%pdoc", "%pfile", "%pinfo", "%pinfo2",
696 "%popd", "%pprint", "%precision", "%profile", "%prun", "%psearch",
697 "%psource", "%pushd", "%pwd", "%pycat", "%pylab", "%quickref",
698 "%recall", "%rehashx", "%reload_ext", "%rep", "%rerun", "%reset",
699 "%reset_selective", "%run", "%save", "%sc", "%sx", "%tb", "%time",
700 "%timeit", "%unalias", "%unload_ext", "%who", "%who_ls", "%whos",
701 "%xdel", "%xmode"]
702
703 def make_dynamic_magic(i):
704 def inner_dynamic_magic():
705 self.active_frontend.execute(i)
706 inner_dynamic_magic.__name__ = "dynamics_magic_%s" % i
707 return inner_dynamic_magic
708
709 for magic in magiclist:
710 xaction = QtGui.QAction(magic,
711 self,
712 triggered=make_dynamic_magic(magic)
713 )
714 self.all_magic_menu.addAction(xaction)
715
716 ## END TEMPORARY FIX
717 ########################################################################
718
719
669 720 def init_window_menu(self):
670 721 self.window_menu = self.menuBar().addMenu("&Window")
671 722 if sys.platform == 'darwin':
672 723 # add min/maximize actions to OSX, which lacks default bindings.
673 724 self.minimizeAct = QtGui.QAction("Mini&mize",
674 725 self,
675 726 shortcut="Ctrl+m",
676 727 statusTip="Minimize the window/Restore Normal Size",
677 728 triggered=self.toggleMinimized)
678 729 # maximize is called 'Zoom' on OSX for some reason
679 730 self.maximizeAct = QtGui.QAction("&Zoom",
680 731 self,
681 732 shortcut="Ctrl+Shift+M",
682 733 statusTip="Maximize the window/Restore Normal Size",
683 734 triggered=self.toggleMaximized)
684 735
685 736 self.add_menu_action(self.window_menu, self.minimizeAct)
686 737 self.add_menu_action(self.window_menu, self.maximizeAct)
687 738 self.window_menu.addSeparator()
688 739
689 740 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
690 741 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
691 742 self,
692 743 shortcut=prev_key,
693 744 statusTip="Select previous tab",
694 745 triggered=self.prev_tab)
695 746 self.add_menu_action(self.window_menu, self.prev_tab_act)
696 747
697 748 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
698 749 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
699 750 self,
700 751 shortcut=next_key,
701 752 statusTip="Select next tab",
702 753 triggered=self.next_tab)
703 754 self.add_menu_action(self.window_menu, self.next_tab_act)
704 755
705 756 def init_help_menu(self):
706 757 # please keep the Help menu in Mac Os even if empty. It will
707 758 # automatically contain a search field to search inside menus and
708 759 # please keep it spelled in English, as long as Qt Doesn't support
709 760 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
710 761 # this search field fonctionality
711 762
712 763 self.help_menu = self.menuBar().addMenu("&Help")
713 764
714 765
715 766 # Help Menu
716 767
717 768 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
718 769 self,
719 770 triggered=self.intro_active_frontend
720 771 )
721 772 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
722 773
723 774 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
724 775 self,
725 776 triggered=self.quickref_active_frontend
726 777 )
727 778 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
728 779
729 780 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
730 781 self,
731 782 triggered=self.guiref_active_frontend
732 783 )
733 784 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
734 785
735 786 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
736 787 self,
737 788 triggered=self._open_online_help)
738 789 self.add_menu_action(self.help_menu, self.onlineHelpAct)
739 790
740 791 # minimize/maximize/fullscreen actions:
741 792
742 793 def toggle_menu_bar(self):
743 794 menu_bar = self.menuBar()
744 795 if menu_bar.isVisible():
745 796 menu_bar.setVisible(False)
746 797 else:
747 798 menu_bar.setVisible(True)
748 799
749 800 def toggleMinimized(self):
750 801 if not self.isMinimized():
751 802 self.showMinimized()
752 803 else:
753 804 self.showNormal()
754 805
755 806 def _open_online_help(self):
756 807 filename="http://ipython.org/ipython-doc/stable/index.html"
757 808 webbrowser.open(filename, new=1, autoraise=True)
758 809
759 810 def toggleMaximized(self):
760 811 if not self.isMaximized():
761 812 self.showMaximized()
762 813 else:
763 814 self.showNormal()
764 815
765 816 # Min/Max imizing while in full screen give a bug
766 817 # when going out of full screen, at least on OSX
767 818 def toggleFullScreen(self):
768 819 if not self.isFullScreen():
769 820 self.showFullScreen()
770 821 if sys.platform == 'darwin':
771 822 self.maximizeAct.setEnabled(False)
772 823 self.minimizeAct.setEnabled(False)
773 824 else:
774 825 self.showNormal()
775 826 if sys.platform == 'darwin':
776 827 self.maximizeAct.setEnabled(True)
777 828 self.minimizeAct.setEnabled(True)
778 829
779 830 def close_active_frontend(self):
780 831 self.close_tab(self.active_frontend)
781 832
782 833 def restart_kernel_active_frontend(self):
783 834 self.active_frontend.request_restart_kernel()
784 835
785 836 def interrupt_kernel_active_frontend(self):
786 837 self.active_frontend.request_interrupt_kernel()
787 838
788 839 def cut_active_frontend(self):
789 840 widget = self.active_frontend
790 841 if widget.can_cut():
791 842 widget.cut()
792 843
793 844 def copy_active_frontend(self):
794 845 widget = self.active_frontend
795 846 if widget.can_copy():
796 847 widget.copy()
797 848
798 849 def copy_raw_active_frontend(self):
799 850 self.active_frontend._copy_raw_action.trigger()
800 851
801 852 def paste_active_frontend(self):
802 853 widget = self.active_frontend
803 854 if widget.can_paste():
804 855 widget.paste()
805 856
806 857 def undo_active_frontend(self):
807 858 self.active_frontend.undo()
808 859
809 860 def redo_active_frontend(self):
810 861 self.active_frontend.redo()
811 862
812 863 def reset_magic_active_frontend(self):
813 864 self.active_frontend.execute("%reset")
814 865
815 866 def history_magic_active_frontend(self):
816 867 self.active_frontend.execute("%history")
817 868
818 869 def save_magic_active_frontend(self):
819 870 self.active_frontend.save_magic()
820 871
821 872 def clear_magic_active_frontend(self):
822 873 self.active_frontend.execute("%clear")
823 874
824 875 def who_magic_active_frontend(self):
825 876 self.active_frontend.execute("%who")
826 877
827 878 def who_ls_magic_active_frontend(self):
828 879 self.active_frontend.execute("%who_ls")
829 880
830 881 def whos_magic_active_frontend(self):
831 882 self.active_frontend.execute("%whos")
832 883
833 884 def print_action_active_frontend(self):
834 885 self.active_frontend.print_action.trigger()
835 886
836 887 def export_action_active_frontend(self):
837 888 self.active_frontend.export_action.trigger()
838 889
839 890 def select_all_active_frontend(self):
840 891 self.active_frontend.select_all_action.trigger()
841 892
842 893 def increase_font_size_active_frontend(self):
843 894 self.active_frontend.increase_font_size.trigger()
844 895
845 896 def decrease_font_size_active_frontend(self):
846 897 self.active_frontend.decrease_font_size.trigger()
847 898
848 899 def reset_font_size_active_frontend(self):
849 900 self.active_frontend.reset_font_size.trigger()
850 901
851 902 def guiref_active_frontend(self):
852 903 self.active_frontend.execute("%guiref")
853 904
854 905 def intro_active_frontend(self):
855 906 self.active_frontend.execute("?")
856 907
857 908 def quickref_active_frontend(self):
858 909 self.active_frontend.execute("%quickref")
859 910 #---------------------------------------------------------------------------
860 911 # QWidget interface
861 912 #---------------------------------------------------------------------------
862 913
863 914 def closeEvent(self, event):
864 915 """ Forward the close event to every tabs contained by the windows
865 916 """
866 917 if self.tab_widget.count() == 0:
867 918 # no tabs, just close
868 919 event.accept()
869 920 return
870 921 # Do Not loop on the widget count as it change while closing
871 922 title = self.window().windowTitle()
872 923 cancel = QtGui.QMessageBox.Cancel
873 924 okay = QtGui.QMessageBox.Ok
874 925
875 926 if self.confirm_exit:
876 927 if self.tab_widget.count() > 1:
877 928 msg = "Close all tabs, stop all kernels, and Quit?"
878 929 else:
879 930 msg = "Close console, stop kernel, and Quit?"
880 931 info = "Kernels not started here (e.g. notebooks) will be left alone."
881 932 closeall = QtGui.QPushButton("&Yes, quit everything", self)
882 933 closeall.setShortcut('Y')
883 934 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
884 935 title, msg)
885 936 box.setInformativeText(info)
886 937 box.addButton(cancel)
887 938 box.addButton(closeall, QtGui.QMessageBox.YesRole)
888 939 box.setDefaultButton(closeall)
889 940 box.setEscapeButton(cancel)
890 941 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
891 942 box.setIconPixmap(pixmap)
892 943 reply = box.exec_()
893 944 else:
894 945 reply = okay
895 946
896 947 if reply == cancel:
897 948 event.ignore()
898 949 return
899 950 if reply == okay:
900 951 while self.tab_widget.count() >= 1:
901 952 # prevent further confirmations:
902 953 widget = self.active_frontend
903 954 widget._confirm_exit = False
904 955 self.close_tab(widget)
905 956 event.accept()
906 957
@@ -1,552 +1,559 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 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # stdlib imports
22 22 import json
23 23 import os
24 24 import signal
25 25 import sys
26 26 import uuid
27 27
28 28 # System library imports
29 29 from IPython.external.qt import QtGui
30 30
31 31 # Local imports
32 32 from IPython.config.application import boolean_flag, catch_config_error
33 33 from IPython.core.application import BaseIPythonApplication
34 34 from IPython.core.profiledir import ProfileDir
35 35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 39 from IPython.frontend.qt.console import styles
40 40 from IPython.frontend.qt.console.mainwindow import MainWindow
41 41 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 42 from IPython.utils.path import filefind
43 43 from IPython.utils.py3compat import str_to_bytes
44 44 from IPython.utils.traitlets import (
45 45 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 46 )
47 47 from IPython.zmq.ipkernel import (
48 48 flags as ipkernel_flags,
49 49 aliases as ipkernel_aliases,
50 50 IPKernelApp
51 51 )
52 52 from IPython.zmq.session import Session, default_secure
53 53 from IPython.zmq.zmqshell import ZMQInteractiveShell
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Network Constants
57 57 #-----------------------------------------------------------------------------
58 58
59 59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Globals
63 63 #-----------------------------------------------------------------------------
64 64
65 65 _examples = """
66 66 ipython qtconsole # start the qtconsole
67 67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 68 """
69 69
70 70 #-----------------------------------------------------------------------------
71 71 # Aliases and Flags
72 72 #-----------------------------------------------------------------------------
73 73
74 74 flags = dict(ipkernel_flags)
75 75 qt_flags = {
76 76 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
77 77 "Connect to an existing kernel. If no argument specified, guess most recent"),
78 78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 79 "Use a pure Python kernel instead of an IPython kernel."),
80 80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 81 "Disable rich text support."),
82 82 }
83 83 qt_flags.update(boolean_flag(
84 84 'gui-completion', 'ConsoleWidget.gui_completion',
85 85 "use a GUI widget for tab completion",
86 86 "use plaintext output for completion"
87 87 ))
88 88 qt_flags.update(boolean_flag(
89 89 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
90 90 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
91 91 to force a direct exit without any confirmation.
92 92 """,
93 93 """Don't prompt the user when exiting. This will terminate the kernel
94 94 if it is owned by the frontend, and leave it alive if it is external.
95 95 """
96 96 ))
97 97 flags.update(qt_flags)
98 98
99 99 aliases = dict(ipkernel_aliases)
100 100
101 101 qt_aliases = dict(
102 102 hb = 'IPythonQtConsoleApp.hb_port',
103 103 shell = 'IPythonQtConsoleApp.shell_port',
104 104 iopub = 'IPythonQtConsoleApp.iopub_port',
105 105 stdin = 'IPythonQtConsoleApp.stdin_port',
106 106 ip = 'IPythonQtConsoleApp.ip',
107 107 existing = 'IPythonQtConsoleApp.existing',
108 108 f = 'IPythonQtConsoleApp.connection_file',
109 109
110 110 style = 'IPythonWidget.syntax_style',
111 111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 112 colors = 'ZMQInteractiveShell.colors',
113 113
114 114 editor = 'IPythonWidget.editor',
115 115 paging = 'ConsoleWidget.paging',
116 116 ssh = 'IPythonQtConsoleApp.sshserver',
117 117 )
118 118 aliases.update(qt_aliases)
119 119
120 120 #-----------------------------------------------------------------------------
121 121 # Classes
122 122 #-----------------------------------------------------------------------------
123 123
124 124 #-----------------------------------------------------------------------------
125 125 # IPythonQtConsole
126 126 #-----------------------------------------------------------------------------
127 127
128 128
129 129 class IPythonQtConsoleApp(BaseIPythonApplication):
130 130 name = 'ipython-qtconsole'
131 131 default_config_file_name='ipython_config.py'
132 132
133 133 description = """
134 134 The IPython QtConsole.
135 135
136 136 This launches a Console-style application using Qt. It is not a full
137 137 console, in that launched terminal subprocesses will not be able to accept
138 138 input.
139 139
140 140 The QtConsole supports various extra features beyond the Terminal IPython
141 141 shell, such as inline plotting with matplotlib, via:
142 142
143 143 ipython qtconsole --pylab=inline
144 144
145 145 as well as saving your session as HTML, and printing the output.
146 146
147 147 """
148 148 examples = _examples
149 149
150 150 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
151 151 flags = Dict(flags)
152 152 aliases = Dict(aliases)
153 153
154 154 kernel_argv = List(Unicode)
155 155
156 156 # create requested profiles by default, if they don't exist:
157 157 auto_create = CBool(True)
158 158 # connection info:
159 159 ip = Unicode(LOCALHOST, config=True,
160 160 help="""Set the kernel\'s IP address [default localhost].
161 161 If the IP address is something other than localhost, then
162 162 Consoles on other machines will be able to connect
163 163 to the Kernel, so be careful!"""
164 164 )
165 165
166 166 sshserver = Unicode('', config=True,
167 167 help="""The SSH server to use to connect to the kernel.""")
168 168 sshkey = Unicode('', config=True,
169 169 help="""Path to the ssh key to use for logging in to the ssh server.""")
170 170
171 171 hb_port = Integer(0, config=True,
172 172 help="set the heartbeat port [default: random]")
173 173 shell_port = Integer(0, config=True,
174 174 help="set the shell (XREP) port [default: random]")
175 175 iopub_port = Integer(0, config=True,
176 176 help="set the iopub (PUB) port [default: random]")
177 177 stdin_port = Integer(0, config=True,
178 178 help="set the stdin (XREQ) port [default: random]")
179 179 connection_file = Unicode('', config=True,
180 180 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
181 181
182 182 This file will contain the IP, ports, and authentication key needed to connect
183 183 clients to this kernel. By default, this file will be created in the security-dir
184 184 of the current profile, but can be specified by absolute path.
185 185 """)
186 186 def _connection_file_default(self):
187 187 return 'kernel-%i.json' % os.getpid()
188 188
189 189 existing = Unicode('', config=True,
190 190 help="""Connect to an already running kernel""")
191 191
192 192 stylesheet = Unicode('', config=True,
193 193 help="path to a custom CSS stylesheet")
194 194
195 195 pure = CBool(False, config=True,
196 196 help="Use a pure Python kernel instead of an IPython kernel.")
197 197 plain = CBool(False, config=True,
198 198 help="Use a plaintext widget instead of rich text (plain can't print/save).")
199 199
200 200 def _pure_changed(self, name, old, new):
201 201 kind = 'plain' if self.plain else 'rich'
202 202 self.config.ConsoleWidget.kind = kind
203 203 if self.pure:
204 204 self.widget_factory = FrontendWidget
205 205 elif self.plain:
206 206 self.widget_factory = IPythonWidget
207 207 else:
208 208 self.widget_factory = RichIPythonWidget
209 209
210 210 _plain_changed = _pure_changed
211 211
212 212 confirm_exit = CBool(True, config=True,
213 213 help="""
214 214 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
215 215 to force a direct exit without any confirmation.""",
216 216 )
217 217
218 218 # the factory for creating a widget
219 219 widget_factory = Any(RichIPythonWidget)
220 220
221 221 def parse_command_line(self, argv=None):
222 222 super(IPythonQtConsoleApp, self).parse_command_line(argv)
223 223 if argv is None:
224 224 argv = sys.argv[1:]
225 225 self.kernel_argv = list(argv) # copy
226 226 # kernel should inherit default config file from frontend
227 227 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
228 228 # Scrub frontend-specific flags
229 229 swallow_next = False
230 230 was_flag = False
231 231 # copy again, in case some aliases have the same name as a flag
232 232 # argv = list(self.kernel_argv)
233 233 for a in argv:
234 234 if swallow_next:
235 235 swallow_next = False
236 236 # last arg was an alias, remove the next one
237 237 # *unless* the last alias has a no-arg flag version, in which
238 238 # case, don't swallow the next arg if it's also a flag:
239 239 if not (was_flag and a.startswith('-')):
240 240 self.kernel_argv.remove(a)
241 241 continue
242 242 if a.startswith('-'):
243 243 split = a.lstrip('-').split('=')
244 244 alias = split[0]
245 245 if alias in qt_aliases:
246 246 self.kernel_argv.remove(a)
247 247 if len(split) == 1:
248 248 # alias passed with arg via space
249 249 swallow_next = True
250 250 # could have been a flag that matches an alias, e.g. `existing`
251 251 # in which case, we might not swallow the next arg
252 252 was_flag = alias in qt_flags
253 253 elif alias in qt_flags:
254 254 # strip flag, but don't swallow next, as flags don't take args
255 255 self.kernel_argv.remove(a)
256 256
257 257 def init_connection_file(self):
258 258 """find the connection file, and load the info if found.
259 259
260 260 The current working directory and the current profile's security
261 261 directory will be searched for the file if it is not given by
262 262 absolute path.
263 263
264 264 When attempting to connect to an existing kernel and the `--existing`
265 265 argument does not match an existing file, it will be interpreted as a
266 266 fileglob, and the matching file in the current profile's security dir
267 267 with the latest access time will be used.
268 268 """
269 269 if self.existing:
270 270 try:
271 271 cf = find_connection_file(self.existing)
272 272 except Exception:
273 273 self.log.critical("Could not find existing kernel connection file %s", self.existing)
274 274 self.exit(1)
275 275 self.log.info("Connecting to existing kernel: %s" % cf)
276 276 self.connection_file = cf
277 277 # should load_connection_file only be used for existing?
278 278 # as it is now, this allows reusing ports if an existing
279 279 # file is requested
280 280 try:
281 281 self.load_connection_file()
282 282 except Exception:
283 283 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
284 284 self.exit(1)
285 285
286 286 def load_connection_file(self):
287 287 """load ip/port/hmac config from JSON connection file"""
288 288 # this is identical to KernelApp.load_connection_file
289 289 # perhaps it can be centralized somewhere?
290 290 try:
291 291 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
292 292 except IOError:
293 293 self.log.debug("Connection File not found: %s", self.connection_file)
294 294 return
295 295 self.log.debug(u"Loading connection file %s", fname)
296 296 with open(fname) as f:
297 297 s = f.read()
298 298 cfg = json.loads(s)
299 299 if self.ip == LOCALHOST and 'ip' in cfg:
300 300 # not overridden by config or cl_args
301 301 self.ip = cfg['ip']
302 302 for channel in ('hb', 'shell', 'iopub', 'stdin'):
303 303 name = channel + '_port'
304 304 if getattr(self, name) == 0 and name in cfg:
305 305 # not overridden by config or cl_args
306 306 setattr(self, name, cfg[name])
307 307 if 'key' in cfg:
308 308 self.config.Session.key = str_to_bytes(cfg['key'])
309 309
310 310 def init_ssh(self):
311 311 """set up ssh tunnels, if needed."""
312 312 if not self.sshserver and not self.sshkey:
313 313 return
314 314
315 315 if self.sshkey and not self.sshserver:
316 316 # specifying just the key implies that we are connecting directly
317 317 self.sshserver = self.ip
318 318 self.ip = LOCALHOST
319 319
320 320 # build connection dict for tunnels:
321 321 info = dict(ip=self.ip,
322 322 shell_port=self.shell_port,
323 323 iopub_port=self.iopub_port,
324 324 stdin_port=self.stdin_port,
325 325 hb_port=self.hb_port
326 326 )
327 327
328 328 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
329 329
330 330 # tunnels return a new set of ports, which will be on localhost:
331 331 self.ip = LOCALHOST
332 332 try:
333 333 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
334 334 except:
335 335 # even catch KeyboardInterrupt
336 336 self.log.error("Could not setup tunnels", exc_info=True)
337 337 self.exit(1)
338 338
339 339 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
340 340
341 341 cf = self.connection_file
342 342 base,ext = os.path.splitext(cf)
343 343 base = os.path.basename(base)
344 344 self.connection_file = os.path.basename(base)+'-ssh'+ext
345 345 self.log.critical("To connect another client via this tunnel, use:")
346 346 self.log.critical("--existing %s" % self.connection_file)
347 347
348 348 def _new_connection_file(self):
349 349 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
350 350
351 351 def init_kernel_manager(self):
352 352 # Don't let Qt or ZMQ swallow KeyboardInterupts.
353 353 signal.signal(signal.SIGINT, signal.SIG_DFL)
354 354 sec = self.profile_dir.security_dir
355 355 try:
356 356 cf = filefind(self.connection_file, ['.', sec])
357 357 except IOError:
358 358 # file might not exist
359 359 if self.connection_file == os.path.basename(self.connection_file):
360 360 # just shortname, put it in security dir
361 361 cf = os.path.join(sec, self.connection_file)
362 362 else:
363 363 cf = self.connection_file
364 364
365 365 # Create a KernelManager and start a kernel.
366 366 self.kernel_manager = QtKernelManager(
367 367 ip=self.ip,
368 368 shell_port=self.shell_port,
369 369 iopub_port=self.iopub_port,
370 370 stdin_port=self.stdin_port,
371 371 hb_port=self.hb_port,
372 372 connection_file=cf,
373 373 config=self.config,
374 374 )
375 375 # start the kernel
376 376 if not self.existing:
377 377 kwargs = dict(ipython=not self.pure)
378 378 kwargs['extra_arguments'] = self.kernel_argv
379 379 self.kernel_manager.start_kernel(**kwargs)
380 380 elif self.sshserver:
381 381 # ssh, write new connection file
382 382 self.kernel_manager.write_connection_file()
383 383 self.kernel_manager.start_channels()
384 384
385 385 def new_frontend_master(self):
386 386 """ Create and return new frontend attached to new kernel, launched on localhost.
387 387 """
388 388 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
389 389 kernel_manager = QtKernelManager(
390 390 ip=ip,
391 391 connection_file=self._new_connection_file(),
392 392 config=self.config,
393 393 )
394 394 # start the kernel
395 395 kwargs = dict(ipython=not self.pure)
396 396 kwargs['extra_arguments'] = self.kernel_argv
397 397 kernel_manager.start_kernel(**kwargs)
398 398 kernel_manager.start_channels()
399 399 widget = self.widget_factory(config=self.config,
400 400 local_kernel=True)
401 401 widget.kernel_manager = kernel_manager
402 402 widget._existing = False
403 403 widget._may_close = True
404 404 widget._confirm_exit = self.confirm_exit
405 405 return widget
406 406
407 407 def new_frontend_slave(self, current_widget):
408 408 """Create and return a new frontend attached to an existing kernel.
409 409
410 410 Parameters
411 411 ----------
412 412 current_widget : IPythonWidget
413 413 The IPythonWidget whose kernel this frontend is to share
414 414 """
415 415 kernel_manager = QtKernelManager(
416 416 connection_file=current_widget.kernel_manager.connection_file,
417 417 config = self.config,
418 418 )
419 419 kernel_manager.load_connection_file()
420 420 kernel_manager.start_channels()
421 421 widget = self.widget_factory(config=self.config,
422 422 local_kernel=False)
423 423 widget._existing = True
424 424 widget._may_close = False
425 425 widget._confirm_exit = False
426 426 widget.kernel_manager = kernel_manager
427 427 return widget
428 428
429 429 def init_qt_elements(self):
430 430 # Create the widget.
431 431 self.app = QtGui.QApplication([])
432 432
433 433 base_path = os.path.abspath(os.path.dirname(__file__))
434 434 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
435 435 self.app.icon = QtGui.QIcon(icon_path)
436 436 QtGui.QApplication.setWindowIcon(self.app.icon)
437 437
438 438 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
439 439 self.widget = self.widget_factory(config=self.config,
440 440 local_kernel=local_kernel)
441 441 self.widget._existing = self.existing
442 442 self.widget._may_close = not self.existing
443 443 self.widget._confirm_exit = self.confirm_exit
444 444
445 445 self.widget.kernel_manager = self.kernel_manager
446 446 self.window = MainWindow(self.app,
447 447 confirm_exit=self.confirm_exit,
448 448 new_frontend_factory=self.new_frontend_master,
449 449 slave_frontend_factory=self.new_frontend_slave,
450 450 )
451 451 self.window.log = self.log
452 452 self.window.add_tab_with_frontend(self.widget)
453 453 self.window.init_menu_bar()
454 454
455 # we need to populate the 'Magic Menu' once the kernel has answer at least once
456 self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger)
455 # we need to populate the 'Magic Menu' once the kernel has answer at
456 # least once
457
458 ########################################################################
459 ## TEMPORARILY DISABLED - see #1057 for details, uncomment the next
460 ## line when a proper fix is found:
461 ## self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger)
462 ## END TEMPORARY FIX
463 ########################################################################
457 464
458 465 self.window.setWindowTitle('Python' if self.pure else 'IPython')
459 466
460 467 def init_colors(self):
461 468 """Configure the coloring of the widget"""
462 469 # Note: This will be dramatically simplified when colors
463 470 # are removed from the backend.
464 471
465 472 if self.pure:
466 473 # only IPythonWidget supports styling
467 474 return
468 475
469 476 # parse the colors arg down to current known labels
470 477 try:
471 478 colors = self.config.ZMQInteractiveShell.colors
472 479 except AttributeError:
473 480 colors = None
474 481 try:
475 482 style = self.config.IPythonWidget.syntax_style
476 483 except AttributeError:
477 484 style = None
478 485
479 486 # find the value for colors:
480 487 if colors:
481 488 colors=colors.lower()
482 489 if colors in ('lightbg', 'light'):
483 490 colors='lightbg'
484 491 elif colors in ('dark', 'linux'):
485 492 colors='linux'
486 493 else:
487 494 colors='nocolor'
488 495 elif style:
489 496 if style=='bw':
490 497 colors='nocolor'
491 498 elif styles.dark_style(style):
492 499 colors='linux'
493 500 else:
494 501 colors='lightbg'
495 502 else:
496 503 colors=None
497 504
498 505 # Configure the style.
499 506 widget = self.widget
500 507 if style:
501 508 widget.style_sheet = styles.sheet_from_template(style, colors)
502 509 widget.syntax_style = style
503 510 widget._syntax_style_changed()
504 511 widget._style_sheet_changed()
505 512 elif colors:
506 513 # use a default style
507 514 widget.set_default_style(colors=colors)
508 515 else:
509 516 # this is redundant for now, but allows the widget's
510 517 # defaults to change
511 518 widget.set_default_style()
512 519
513 520 if self.stylesheet:
514 521 # we got an expicit stylesheet
515 522 if os.path.isfile(self.stylesheet):
516 523 with open(self.stylesheet) as f:
517 524 sheet = f.read()
518 525 widget.style_sheet = sheet
519 526 widget._style_sheet_changed()
520 527 else:
521 528 raise IOError("Stylesheet %r not found."%self.stylesheet)
522 529
523 530 @catch_config_error
524 531 def initialize(self, argv=None):
525 532 super(IPythonQtConsoleApp, self).initialize(argv)
526 533 self.init_connection_file()
527 534 default_secure(self.config)
528 535 self.init_ssh()
529 536 self.init_kernel_manager()
530 537 self.init_qt_elements()
531 538 self.init_colors()
532 539
533 540 def start(self):
534 541
535 542 # draw the window
536 543 self.window.show()
537 544
538 545 # Start the application main loop.
539 546 self.app.exec_()
540 547
541 548 #-----------------------------------------------------------------------------
542 549 # Main entry point
543 550 #-----------------------------------------------------------------------------
544 551
545 552 def main():
546 553 app = IPythonQtConsoleApp()
547 554 app.initialize()
548 555 app.start()
549 556
550 557
551 558 if __name__ == '__main__':
552 559 main()
General Comments 0
You need to be logged in to leave comments. Login now