##// END OF EJS Templates
Introduce a doclkable widget that will substitute "all magics" menu....
Dimitry Kloper -
Show More
@@ -1,995 +1,1151 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 * Paul Ivanov
15 15
16 16 """
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # stdlib imports
23 23 import json
24 24 import re
25 25 import sys
26 26 import webbrowser
27 27 from threading import Thread
28 28
29 29 # System library imports
30 30 from IPython.external.qt import QtGui,QtCore
31 31
32 32 from IPython.core.magic import magic_escapes
33 33
34 34 def background(f):
35 35 """call a function in a simple thread, to prevent blocking"""
36 36 t = Thread(target=f)
37 37 t.start()
38 38 return t
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Classes
42 42 #-----------------------------------------------------------------------------
43 43
44 44 class MainWindow(QtGui.QMainWindow):
45 45
46 46 #---------------------------------------------------------------------------
47 47 # 'object' interface
48 48 #---------------------------------------------------------------------------
49 49
50 50 _magic_menu_dict = {}
51 51
52 52 def __init__(self, app,
53 53 confirm_exit=True,
54 54 new_frontend_factory=None, slave_frontend_factory=None,
55 55 ):
56 56 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
57 57
58 58 Parameters
59 59 ----------
60 60
61 61 app : reference to QApplication parent
62 62 confirm_exit : bool, optional
63 63 Whether we should prompt on close of tabs
64 64 new_frontend_factory : callable
65 65 A callable that returns a new IPythonWidget instance, attached to
66 66 its own running kernel.
67 67 slave_frontend_factory : callable
68 68 A callable that takes an existing IPythonWidget, and returns a new
69 69 IPythonWidget instance, attached to the same kernel.
70 70 """
71 71
72 72 super(MainWindow, self).__init__()
73 73 self._kernel_counter = 0
74 74 self._app = app
75 75 self.confirm_exit = confirm_exit
76 76 self.new_frontend_factory = new_frontend_factory
77 77 self.slave_frontend_factory = slave_frontend_factory
78 78
79 79 self.tab_widget = QtGui.QTabWidget(self)
80 80 self.tab_widget.setDocumentMode(True)
81 81 self.tab_widget.setTabsClosable(True)
82 82 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
83 83
84 84 self.setCentralWidget(self.tab_widget)
85 85 # hide tab bar at first, since we have no tabs:
86 86 self.tab_widget.tabBar().setVisible(False)
87 87 # prevent focus in tab bar
88 88 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
89 89
90 90 def update_tab_bar_visibility(self):
91 91 """ update visibility of the tabBar depending of the number of tab
92 92
93 93 0 or 1 tab, tabBar hidden
94 94 2+ tabs, tabBar visible
95 95
96 96 send a self.close if number of tab ==0
97 97
98 98 need to be called explicitly, or be connected to tabInserted/tabRemoved
99 99 """
100 100 if self.tab_widget.count() <= 1:
101 101 self.tab_widget.tabBar().setVisible(False)
102 102 else:
103 103 self.tab_widget.tabBar().setVisible(True)
104 104 if self.tab_widget.count()==0 :
105 105 self.close()
106 106
107 107 @property
108 108 def next_kernel_id(self):
109 109 """constantly increasing counter for kernel IDs"""
110 110 c = self._kernel_counter
111 111 self._kernel_counter += 1
112 112 return c
113 113
114 114 @property
115 115 def active_frontend(self):
116 116 return self.tab_widget.currentWidget()
117 117
118 118 def create_tab_with_new_frontend(self):
119 119 """create a new frontend and attach it to a new tab"""
120 120 widget = self.new_frontend_factory()
121 121 self.add_tab_with_frontend(widget)
122 122
123 123 def create_tab_with_current_kernel(self):
124 124 """create a new frontend attached to the same kernel as the current tab"""
125 125 current_widget = self.tab_widget.currentWidget()
126 126 current_widget_index = self.tab_widget.indexOf(current_widget)
127 127 current_widget_name = self.tab_widget.tabText(current_widget_index)
128 128 widget = self.slave_frontend_factory(current_widget)
129 129 if 'slave' in current_widget_name:
130 130 # don't keep stacking slaves
131 131 name = current_widget_name
132 132 else:
133 133 name = '(%s) slave' % current_widget_name
134 134 self.add_tab_with_frontend(widget,name=name)
135 135
136 136 def close_tab(self,current_tab):
137 137 """ Called when you need to try to close a tab.
138 138
139 139 It takes the number of the tab to be closed as argument, or a reference
140 140 to the widget inside this tab
141 141 """
142 142
143 143 # let's be sure "tab" and "closing widget" are respectively the index
144 144 # of the tab to close and a reference to the frontend to close
145 145 if type(current_tab) is not int :
146 146 current_tab = self.tab_widget.indexOf(current_tab)
147 147 closing_widget=self.tab_widget.widget(current_tab)
148 148
149 149
150 150 # when trying to be closed, widget might re-send a request to be
151 151 # closed again, but will be deleted when event will be processed. So
152 152 # need to check that widget still exists and skip if not. One example
153 153 # of this is when 'exit' is sent in a slave tab. 'exit' will be
154 154 # re-sent by this function on the master widget, which ask all slave
155 155 # widgets to exit
156 156 if closing_widget==None:
157 157 return
158 158
159 159 #get a list of all slave widgets on the same kernel.
160 160 slave_tabs = self.find_slave_widgets(closing_widget)
161 161
162 162 keepkernel = None #Use the prompt by default
163 163 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
164 164 keepkernel = closing_widget._keep_kernel_on_exit
165 165 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
166 166 # we set local slave tabs._hidden to True to avoid prompting for kernel
167 167 # restart when they get the signal. and then "forward" the 'exit'
168 168 # to the main window
169 169 if keepkernel is not None:
170 170 for tab in slave_tabs:
171 171 tab._hidden = True
172 172 if closing_widget in slave_tabs:
173 173 try :
174 174 self.find_master_tab(closing_widget).execute('exit')
175 175 except AttributeError:
176 176 self.log.info("Master already closed or not local, closing only current tab")
177 177 self.tab_widget.removeTab(current_tab)
178 178 self.update_tab_bar_visibility()
179 179 return
180 180
181 181 kernel_client = closing_widget.kernel_client
182 182 kernel_manager = closing_widget.kernel_manager
183 183
184 184 if keepkernel is None and not closing_widget._confirm_exit:
185 185 # don't prompt, just terminate the kernel if we own it
186 186 # or leave it alone if we don't
187 187 keepkernel = closing_widget._existing
188 188 if keepkernel is None: #show prompt
189 189 if kernel_client and kernel_client.channels_running:
190 190 title = self.window().windowTitle()
191 191 cancel = QtGui.QMessageBox.Cancel
192 192 okay = QtGui.QMessageBox.Ok
193 193 if closing_widget._may_close:
194 194 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
195 195 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
196 196 justthis = QtGui.QPushButton("&No, just this Tab", self)
197 197 justthis.setShortcut('N')
198 198 closeall = QtGui.QPushButton("&Yes, close all", self)
199 199 closeall.setShortcut('Y')
200 200 # allow ctrl-d ctrl-d exit, like in terminal
201 201 closeall.setShortcut('Ctrl+D')
202 202 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
203 203 title, msg)
204 204 box.setInformativeText(info)
205 205 box.addButton(cancel)
206 206 box.addButton(justthis, QtGui.QMessageBox.NoRole)
207 207 box.addButton(closeall, QtGui.QMessageBox.YesRole)
208 208 box.setDefaultButton(closeall)
209 209 box.setEscapeButton(cancel)
210 210 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
211 211 box.setIconPixmap(pixmap)
212 212 reply = box.exec_()
213 213 if reply == 1: # close All
214 214 for slave in slave_tabs:
215 215 background(slave.kernel_client.stop_channels)
216 216 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
217 217 closing_widget.execute("exit")
218 218 self.tab_widget.removeTab(current_tab)
219 219 background(kernel_client.stop_channels)
220 220 elif reply == 0: # close Console
221 221 if not closing_widget._existing:
222 222 # Have kernel: don't quit, just close the tab
223 223 closing_widget.execute("exit True")
224 224 self.tab_widget.removeTab(current_tab)
225 225 background(kernel_client.stop_channels)
226 226 else:
227 227 reply = QtGui.QMessageBox.question(self, title,
228 228 "Are you sure you want to close this Console?"+
229 229 "\nThe Kernel and other Consoles will remain active.",
230 230 okay|cancel,
231 231 defaultButton=okay
232 232 )
233 233 if reply == okay:
234 234 self.tab_widget.removeTab(current_tab)
235 235 elif keepkernel: #close console but leave kernel running (no prompt)
236 236 self.tab_widget.removeTab(current_tab)
237 237 background(kernel_client.stop_channels)
238 238 else: #close console and kernel (no prompt)
239 239 self.tab_widget.removeTab(current_tab)
240 240 if kernel_client and kernel_client.channels_running:
241 241 for slave in slave_tabs:
242 242 background(slave.kernel_client.stop_channels)
243 243 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
244 244 if kernel_manager:
245 245 kernel_manager.shutdown_kernel()
246 246 background(kernel_client.stop_channels)
247 247
248 248 self.update_tab_bar_visibility()
249 249
250 250 def add_tab_with_frontend(self,frontend,name=None):
251 251 """ insert a tab with a given frontend in the tab bar, and give it a name
252 252
253 253 """
254 254 if not name:
255 255 name = 'kernel %i' % self.next_kernel_id
256 256 self.tab_widget.addTab(frontend,name)
257 257 self.update_tab_bar_visibility()
258 258 self.make_frontend_visible(frontend)
259 259 frontend.exit_requested.connect(self.close_tab)
260 260
261 261 def next_tab(self):
262 262 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
263 263
264 264 def prev_tab(self):
265 265 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
266 266
267 267 def make_frontend_visible(self,frontend):
268 268 widget_index=self.tab_widget.indexOf(frontend)
269 269 if widget_index > 0 :
270 270 self.tab_widget.setCurrentIndex(widget_index)
271 271
272 272 def find_master_tab(self,tab,as_list=False):
273 273 """
274 274 Try to return the frontend that owns the kernel attached to the given widget/tab.
275 275
276 276 Only finds frontend owned by the current application. Selection
277 277 based on port of the kernel might be inaccurate if several kernel
278 278 on different ip use same port number.
279 279
280 280 This function does the conversion tabNumber/widget if needed.
281 281 Might return None if no master widget (non local kernel)
282 282 Will crash IPython if more than 1 masterWidget
283 283
284 284 When asList set to True, always return a list of widget(s) owning
285 285 the kernel. The list might be empty or containing several Widget.
286 286 """
287 287
288 288 #convert from/to int/richIpythonWidget if needed
289 289 if isinstance(tab, int):
290 290 tab = self.tab_widget.widget(tab)
291 291 km=tab.kernel_client
292 292
293 293 #build list of all widgets
294 294 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
295 295
296 296 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
297 297 # And should have a _may_close attribute
298 298 filtered_widget_list = [ widget for widget in widget_list if
299 299 widget.kernel_client.connection_file == km.connection_file and
300 300 hasattr(widget,'_may_close') ]
301 301 # the master widget is the one that may close the kernel
302 302 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
303 303 if as_list:
304 304 return master_widget
305 305 assert(len(master_widget)<=1 )
306 306 if len(master_widget)==0:
307 307 return None
308 308
309 309 return master_widget[0]
310 310
311 311 def find_slave_widgets(self,tab):
312 312 """return all the frontends that do not own the kernel attached to the given widget/tab.
313 313
314 314 Only find frontends owned by the current application. Selection
315 315 based on connection file of the kernel.
316 316
317 317 This function does the conversion tabNumber/widget if needed.
318 318 """
319 319 #convert from/to int/richIpythonWidget if needed
320 320 if isinstance(tab, int):
321 321 tab = self.tab_widget.widget(tab)
322 322 km=tab.kernel_client
323 323
324 324 #build list of all widgets
325 325 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
326 326
327 327 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
328 328 filtered_widget_list = ( widget for widget in widget_list if
329 329 widget.kernel_client.connection_file == km.connection_file)
330 330 # Get a list of all widget owning the same kernel and removed it from
331 331 # the previous cadidate. (better using sets ?)
332 332 master_widget_list = self.find_master_tab(tab, as_list=True)
333 333 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
334 334
335 335 return slave_list
336 336
337 337 # Populate the menu bar with common actions and shortcuts
338 338 def add_menu_action(self, menu, action, defer_shortcut=False):
339 339 """Add action to menu as well as self
340 340
341 341 So that when the menu bar is invisible, its actions are still available.
342 342
343 343 If defer_shortcut is True, set the shortcut context to widget-only,
344 344 where it will avoid conflict with shortcuts already bound to the
345 345 widgets themselves.
346 346 """
347 347 menu.addAction(action)
348 348 self.addAction(action)
349 349
350 350 if defer_shortcut:
351 351 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
352 352
353 353 def init_menu_bar(self):
354 354 #create menu in the order they should appear in the menu bar
355 355 self.init_file_menu()
356 356 self.init_edit_menu()
357 357 self.init_view_menu()
358 358 self.init_kernel_menu()
359 359 self.init_magic_menu()
360 360 self.init_window_menu()
361 361 self.init_help_menu()
362 362
363 363 def init_file_menu(self):
364 364 self.file_menu = self.menuBar().addMenu("&File")
365 365
366 366 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
367 367 self,
368 368 shortcut="Ctrl+T",
369 369 triggered=self.create_tab_with_new_frontend)
370 370 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
371 371
372 372 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
373 373 self,
374 374 shortcut="Ctrl+Shift+T",
375 375 triggered=self.create_tab_with_current_kernel)
376 376 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
377 377
378 378 self.file_menu.addSeparator()
379 379
380 380 self.close_action=QtGui.QAction("&Close Tab",
381 381 self,
382 382 shortcut=QtGui.QKeySequence.Close,
383 383 triggered=self.close_active_frontend
384 384 )
385 385 self.add_menu_action(self.file_menu, self.close_action)
386 386
387 387 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
388 388 self,
389 389 shortcut=QtGui.QKeySequence.Save,
390 390 triggered=self.export_action_active_frontend
391 391 )
392 392 self.add_menu_action(self.file_menu, self.export_action, True)
393 393
394 394 self.file_menu.addSeparator()
395 395
396 396 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
397 397 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
398 398 # Only override the default if there is a collision.
399 399 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
400 400 printkey = "Ctrl+Shift+P"
401 401 self.print_action = QtGui.QAction("&Print",
402 402 self,
403 403 shortcut=printkey,
404 404 triggered=self.print_action_active_frontend)
405 405 self.add_menu_action(self.file_menu, self.print_action, True)
406 406
407 407 if sys.platform != 'darwin':
408 408 # OSX always has Quit in the Application menu, only add it
409 409 # to the File menu elsewhere.
410 410
411 411 self.file_menu.addSeparator()
412 412
413 413 self.quit_action = QtGui.QAction("&Quit",
414 414 self,
415 415 shortcut=QtGui.QKeySequence.Quit,
416 416 triggered=self.close,
417 417 )
418 418 self.add_menu_action(self.file_menu, self.quit_action)
419 419
420 420
421 421 def init_edit_menu(self):
422 422 self.edit_menu = self.menuBar().addMenu("&Edit")
423 423
424 424 self.undo_action = QtGui.QAction("&Undo",
425 425 self,
426 426 shortcut=QtGui.QKeySequence.Undo,
427 427 statusTip="Undo last action if possible",
428 428 triggered=self.undo_active_frontend
429 429 )
430 430 self.add_menu_action(self.edit_menu, self.undo_action)
431 431
432 432 self.redo_action = QtGui.QAction("&Redo",
433 433 self,
434 434 shortcut=QtGui.QKeySequence.Redo,
435 435 statusTip="Redo last action if possible",
436 436 triggered=self.redo_active_frontend)
437 437 self.add_menu_action(self.edit_menu, self.redo_action)
438 438
439 439 self.edit_menu.addSeparator()
440 440
441 441 self.cut_action = QtGui.QAction("&Cut",
442 442 self,
443 443 shortcut=QtGui.QKeySequence.Cut,
444 444 triggered=self.cut_active_frontend
445 445 )
446 446 self.add_menu_action(self.edit_menu, self.cut_action, True)
447 447
448 448 self.copy_action = QtGui.QAction("&Copy",
449 449 self,
450 450 shortcut=QtGui.QKeySequence.Copy,
451 451 triggered=self.copy_active_frontend
452 452 )
453 453 self.add_menu_action(self.edit_menu, self.copy_action, True)
454 454
455 455 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
456 456 self,
457 457 shortcut="Ctrl+Shift+C",
458 458 triggered=self.copy_raw_active_frontend
459 459 )
460 460 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
461 461
462 462 self.paste_action = QtGui.QAction("&Paste",
463 463 self,
464 464 shortcut=QtGui.QKeySequence.Paste,
465 465 triggered=self.paste_active_frontend
466 466 )
467 467 self.add_menu_action(self.edit_menu, self.paste_action, True)
468 468
469 469 self.edit_menu.addSeparator()
470 470
471 471 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
472 472 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
473 473 # Only override the default if there is a collision.
474 474 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
475 475 selectall = "Ctrl+Shift+A"
476 476 self.select_all_action = QtGui.QAction("Select &All",
477 477 self,
478 478 shortcut=selectall,
479 479 triggered=self.select_all_active_frontend
480 480 )
481 481 self.add_menu_action(self.edit_menu, self.select_all_action, True)
482 482
483 483
484 484 def init_view_menu(self):
485 485 self.view_menu = self.menuBar().addMenu("&View")
486 486
487 487 if sys.platform != 'darwin':
488 488 # disable on OSX, where there is always a menu bar
489 489 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
490 490 self,
491 491 shortcut="Ctrl+Shift+M",
492 492 statusTip="Toggle visibility of menubar",
493 493 triggered=self.toggle_menu_bar)
494 494 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
495 495
496 496 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
497 497 self.full_screen_act = QtGui.QAction("&Full Screen",
498 498 self,
499 499 shortcut=fs_key,
500 500 statusTip="Toggle between Fullscreen and Normal Size",
501 501 triggered=self.toggleFullScreen)
502 502 self.add_menu_action(self.view_menu, self.full_screen_act)
503 503
504 504 self.view_menu.addSeparator()
505 505
506 506 self.increase_font_size = QtGui.QAction("Zoom &In",
507 507 self,
508 508 shortcut=QtGui.QKeySequence.ZoomIn,
509 509 triggered=self.increase_font_size_active_frontend
510 510 )
511 511 self.add_menu_action(self.view_menu, self.increase_font_size, True)
512 512
513 513 self.decrease_font_size = QtGui.QAction("Zoom &Out",
514 514 self,
515 515 shortcut=QtGui.QKeySequence.ZoomOut,
516 516 triggered=self.decrease_font_size_active_frontend
517 517 )
518 518 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
519 519
520 520 self.reset_font_size = QtGui.QAction("Zoom &Reset",
521 521 self,
522 522 shortcut="Ctrl+0",
523 523 triggered=self.reset_font_size_active_frontend
524 524 )
525 525 self.add_menu_action(self.view_menu, self.reset_font_size, True)
526 526
527 527 self.view_menu.addSeparator()
528 528
529 529 self.clear_action = QtGui.QAction("&Clear Screen",
530 530 self,
531 531 shortcut='Ctrl+L',
532 532 statusTip="Clear the console",
533 533 triggered=self.clear_magic_active_frontend)
534 534 self.add_menu_action(self.view_menu, self.clear_action)
535 535
536 536 self.pager_menu = self.view_menu.addMenu("&Pager")
537 537
538 538 hsplit_action = QtGui.QAction(".. &Horizontal Split",
539 539 self,
540 540 triggered=lambda: self.set_paging_active_frontend('hsplit'))
541 541
542 542 vsplit_action = QtGui.QAction(" : &Vertical Split",
543 543 self,
544 544 triggered=lambda: self.set_paging_active_frontend('vsplit'))
545 545
546 546 inside_action = QtGui.QAction(" &Inside Pager",
547 547 self,
548 548 triggered=lambda: self.set_paging_active_frontend('inside'))
549 549
550 550 self.pager_menu.addAction(hsplit_action)
551 551 self.pager_menu.addAction(vsplit_action)
552 552 self.pager_menu.addAction(inside_action)
553 553
554 554 def init_kernel_menu(self):
555 555 self.kernel_menu = self.menuBar().addMenu("&Kernel")
556 556 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
557 557 # keep the signal shortcuts to ctrl, rather than
558 558 # platform-default like we do elsewhere.
559 559
560 560 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
561 561
562 562 self.interrupt_kernel_action = QtGui.QAction("&Interrupt current Kernel",
563 563 self,
564 564 triggered=self.interrupt_kernel_active_frontend,
565 565 shortcut=ctrl+"+C",
566 566 )
567 567 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
568 568
569 569 self.restart_kernel_action = QtGui.QAction("&Restart current Kernel",
570 570 self,
571 571 triggered=self.restart_kernel_active_frontend,
572 572 shortcut=ctrl+"+.",
573 573 )
574 574 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
575 575
576 576 self.kernel_menu.addSeparator()
577 577
578 578 self.confirm_restart_kernel_action = QtGui.QAction("&Confirm kernel restart",
579 579 self,
580 580 checkable=True,
581 581 checked=self.active_frontend.confirm_restart,
582 582 triggered=self.toggle_confirm_restart_active_frontend
583 583 )
584 584
585 585 self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action)
586 586 self.tab_widget.currentChanged.connect(self.update_restart_checkbox)
587 587
588 588 def _make_dynamic_magic(self,magic):
589 589 """Return a function `fun` that will execute `magic` on active frontend.
590 590
591 591 Parameters
592 592 ----------
593 593 magic : string
594 594 string that will be executed as is when the returned function is called
595 595
596 596 Returns
597 597 -------
598 598 fun : function
599 599 function with no parameters, when called will execute `magic` on the
600 600 current active frontend at call time
601 601
602 602 See Also
603 603 --------
604 604 populate_all_magic_menu : generate the "All Magics..." menu
605 605
606 606 Notes
607 607 -----
608 608 `fun` executes `magic` in active frontend at the moment it is triggered,
609 609 not the active frontend at the moment it was created.
610 610
611 611 This function is mostly used to create the "All Magics..." Menu at run time.
612 612 """
613 613 # need two level nested function to be sure to pass magic
614 614 # to active frontend **at run time**.
615 615 def inner_dynamic_magic():
616 616 self.active_frontend.execute(magic)
617 617 inner_dynamic_magic.__name__ = "dynamics_magic_s"
618 618 return inner_dynamic_magic
619 619
620 620 def populate_all_magic_menu(self, display_data=None):
621 621 """Clean "All Magics..." menu and repopulate it with `display_data`
622 622
623 623 Parameters
624 624 ----------
625 625 display_data : dict,
626 626 dict of display_data for the magics dict of a MagicsManager.
627 627 Expects json data, as the result of %lsmagic
628 628
629 629 """
630 630 for k,v in self._magic_menu_dict.items():
631 631 v.clear()
632 632 self.all_magic_menu.clear()
633 633
634 634 if not display_data:
635 635 return
636 636
637 637 if display_data['status'] != 'ok':
638 638 self.log.warn("%%lsmagic user-expression failed: %s" % display_data)
639 639 return
640 640
641 641 mdict = json.loads(display_data['data'].get('application/json', {}))
642 642
643 643 for mtype in sorted(mdict):
644 644 subdict = mdict[mtype]
645 645 prefix = magic_escapes[mtype]
646 646 for name in sorted(subdict):
647 647 mclass = subdict[name]
648 648 magic_menu = self._get_magic_menu(mclass)
649 649 pmagic = prefix + name
650 650
651 651 # Adding seperate QActions is needed for some window managers
652 652 xaction = QtGui.QAction(pmagic,
653 653 self,
654 654 triggered=self._make_dynamic_magic(pmagic)
655 655 )
656 656 magic_menu.addAction(xaction)
657 657 self.all_magic_menu.addAction(xaction)
658 658
659 659 def update_all_magic_menu(self):
660 660 """ Update the list of magics in the "All Magics..." Menu
661 661
662 662 Request the kernel with the list of available magics and populate the
663 663 menu with the list received back
664 664
665 665 """
666 666 self.active_frontend._silent_exec_callback('get_ipython().magic("lsmagic")',
667 667 self.populate_all_magic_menu)
668 668
669 669 def _get_magic_menu(self,menuidentifier, menulabel=None):
670 670 """return a submagic menu by name, and create it if needed
671 671
672 672 Parameters
673 673 ----------
674 674
675 675 menulabel : str
676 676 Label for the menu
677 677
678 678 Will infere the menu name from the identifier at creation if menulabel not given.
679 679 To do so you have too give menuidentifier as a CamelCassedString
680 680 """
681 681 menu = self._magic_menu_dict.get(menuidentifier,None)
682 682 if not menu :
683 683 if not menulabel:
684 684 menulabel = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>",menuidentifier)
685 685 menu = QtGui.QMenu(menulabel,self.magic_menu)
686 686 self._magic_menu_dict[menuidentifier]=menu
687 687 self.magic_menu.insertMenu(self.magic_menu_separator,menu)
688 688 return menu
689 689
690 690
691 691
692 692 def init_magic_menu(self):
693 693 self.magic_menu = self.menuBar().addMenu("&Magic")
694 694 self.magic_menu_separator = self.magic_menu.addSeparator()
695 695
696 696 self.all_magic_menu = self._get_magic_menu("AllMagics", menulabel="&All Magics...")
697 697
698 698 # This action should usually not appear as it will be cleared when menu
699 699 # is updated at first kernel response. Though, it is necessary when
700 700 # connecting through X-forwarding, as in this case, the menu is not
701 701 # auto updated, SO DO NOT DELETE.
702 702 self.pop = QtGui.QAction("&Update All Magic Menu ",
703 703 self, triggered=self.update_all_magic_menu)
704 704 self.add_menu_action(self.all_magic_menu, self.pop)
705 705 # we need to populate the 'Magic Menu' once the kernel has answer at
706 706 # least once let's do it immediately, but it's assured to works
707 707 self.pop.trigger()
708 708
709 709 self.reset_action = QtGui.QAction("&Reset",
710 710 self,
711 711 statusTip="Clear all variables from workspace",
712 712 triggered=self.reset_magic_active_frontend)
713 713 self.add_menu_action(self.magic_menu, self.reset_action)
714 714
715 715 self.history_action = QtGui.QAction("&History",
716 716 self,
717 717 statusTip="show command history",
718 718 triggered=self.history_magic_active_frontend)
719 719 self.add_menu_action(self.magic_menu, self.history_action)
720 720
721 721 self.save_action = QtGui.QAction("E&xport History ",
722 722 self,
723 723 statusTip="Export History as Python File",
724 724 triggered=self.save_magic_active_frontend)
725 725 self.add_menu_action(self.magic_menu, self.save_action)
726 726
727 727 self.who_action = QtGui.QAction("&Who",
728 728 self,
729 729 statusTip="List interactive variables",
730 730 triggered=self.who_magic_active_frontend)
731 731 self.add_menu_action(self.magic_menu, self.who_action)
732 732
733 733 self.who_ls_action = QtGui.QAction("Wh&o ls",
734 734 self,
735 735 statusTip="Return a list of interactive variables",
736 736 triggered=self.who_ls_magic_active_frontend)
737 737 self.add_menu_action(self.magic_menu, self.who_ls_action)
738 738
739 739 self.whos_action = QtGui.QAction("Who&s",
740 740 self,
741 741 statusTip="List interactive variables with details",
742 742 triggered=self.whos_magic_active_frontend)
743 743 self.add_menu_action(self.magic_menu, self.whos_action)
744 744
745 745 def init_window_menu(self):
746 746 self.window_menu = self.menuBar().addMenu("&Window")
747 747 if sys.platform == 'darwin':
748 748 # add min/maximize actions to OSX, which lacks default bindings.
749 749 self.minimizeAct = QtGui.QAction("Mini&mize",
750 750 self,
751 751 shortcut="Ctrl+m",
752 752 statusTip="Minimize the window/Restore Normal Size",
753 753 triggered=self.toggleMinimized)
754 754 # maximize is called 'Zoom' on OSX for some reason
755 755 self.maximizeAct = QtGui.QAction("&Zoom",
756 756 self,
757 757 shortcut="Ctrl+Shift+M",
758 758 statusTip="Maximize the window/Restore Normal Size",
759 759 triggered=self.toggleMaximized)
760 760
761 761 self.add_menu_action(self.window_menu, self.minimizeAct)
762 762 self.add_menu_action(self.window_menu, self.maximizeAct)
763 763 self.window_menu.addSeparator()
764 764
765 765 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
766 766 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
767 767 self,
768 768 shortcut=prev_key,
769 769 statusTip="Select previous tab",
770 770 triggered=self.prev_tab)
771 771 self.add_menu_action(self.window_menu, self.prev_tab_act)
772 772
773 773 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
774 774 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
775 775 self,
776 776 shortcut=next_key,
777 777 statusTip="Select next tab",
778 778 triggered=self.next_tab)
779 779 self.add_menu_action(self.window_menu, self.next_tab_act)
780 780
781 781 def init_help_menu(self):
782 782 # please keep the Help menu in Mac Os even if empty. It will
783 783 # automatically contain a search field to search inside menus and
784 784 # please keep it spelled in English, as long as Qt Doesn't support
785 785 # a QAction.MenuRole like HelpMenuRole otherwise it will lose
786 786 # this search field functionality
787 787
788 788 self.help_menu = self.menuBar().addMenu("&Help")
789 789
790 790
791 791 # Help Menu
792 792
793 793 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
794 794 self,
795 795 triggered=self.intro_active_frontend
796 796 )
797 797 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
798 798
799 799 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
800 800 self,
801 801 triggered=self.quickref_active_frontend
802 802 )
803 803 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
804 804
805 805 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
806 806 self,
807 807 triggered=self.guiref_active_frontend
808 808 )
809 809 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
810 810
811 811 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
812 812 self,
813 813 triggered=self._open_online_help)
814 814 self.add_menu_action(self.help_menu, self.onlineHelpAct)
815 815
816 def init_magic_helper(self):
817 self.magic_helper_data = None
818 self.magic_helper = QtGui.QDockWidget("Magics", self)
819 self.magic_helper.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
820 QtCore.Qt.RightDockWidgetArea)
821 self.magic_helper.setVisible(False)
822
823 class MinListWidget(QtGui.QListWidget):
824 def sizeHint(self):
825 s = QtCore.QSize()
826 s.setHeight(super(MinListWidget,self).sizeHint().height())
827 s.setWidth(self.sizeHintForColumn(0))
828 return s
829
830 self.magic_helper_frame = QtGui.QFrame()
831 self.magic_helper_searchl = QtGui.QLabel("Search:")
832 self.magic_helper_search = QtGui.QLineEdit()
833 self.magic_helper_class = QtGui.QComboBox()
834 self.magic_helper_list = MinListWidget()
835 self.magic_helper_paste = QtGui.QPushButton("Paste")
836 self.magic_helper_run = QtGui.QPushButton("Run")
837
838 main_layout = QtGui.QVBoxLayout()
839 search_layout = QtGui.QHBoxLayout()
840 search_layout.addWidget(self.magic_helper_searchl)
841 search_layout.addWidget(self.magic_helper_search, 10)
842 main_layout.addLayout(search_layout)
843 main_layout.addWidget(self.magic_helper_class)
844 main_layout.addWidget(self.magic_helper_list, 10)
845 action_layout = QtGui.QHBoxLayout()
846 action_layout.addWidget(self.magic_helper_paste)
847 action_layout.addWidget(self.magic_helper_run)
848 main_layout.addLayout(action_layout)
849
850 self.magic_helper_frame.setLayout(main_layout)
851 self.magic_helper.setWidget(self.magic_helper_frame)
852
853 self.magic_helper.visibilityChanged[bool].connect(
854 self.update_magic_helper
855 )
856 self.magic_helper_class.activated[int].connect(
857 self.magic_helper_class_selected
858 )
859 self.magic_helper_search.textChanged[str].connect(
860 self.magic_helper_search_changed
861 )
862 self.magic_helper_list.itemDoubleClicked[QtGui.QListWidgetItem].connect(
863 self.magic_helper_paste_requested
864 )
865 self.magic_helper_paste.clicked[bool].connect(
866 self.magic_helper_paste_requested
867 )
868 self.magic_helper_run.clicked[bool].connect(
869 self.magic_helper_run_requested
870 )
871
872 self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.magic_helper)
873 self.add_menu_action(self.magic_menu,
874 self.magic_helper.toggleViewAction())
875
876 def update_magic_helper(self, visible):
877 if not visible or self.magic_helper_data != None:
878 return
879 self.magic_helper_data = {}
880 self.magic_helper_class.clear()
881 self.magic_helper_class.addItem("Populating...")
882 self.active_frontend._silent_exec_callback(
883 'get_ipython().magic("lsmagic")',
884 self.populate_magic_helper
885 )
886
887 def populate_magic_helper(self, data):
888 if not data:
889 return
890
891 if data['status'] != 'ok':
892 self.log.warn("%%lsmagic user-expression failed: {}".format(data))
893 return
894
895 self.magic_helper_class.clear()
896 self.magic_helper_list.clear()
897
898 self.magic_helper_data = json.loads(
899 data['data'].get('application/json', {})
900 )
901
902 self.magic_helper_class.addItem('All Magics', 'any')
903 classes = set()
904
905 for mtype in sorted(self.magic_helper_data):
906 subdict = self.magic_helper_data[mtype]
907 for name in sorted(subdict):
908 classes.add(subdict[name])
909
910 for cls in sorted(classes):
911 label = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>", cls)
912 self.magic_helper_class.addItem(label, cls)
913
914 self.filter_magic_helper('.', 'any')
915
916 def magic_helper_class_selected(self, index):
917 item = self.magic_helper_class.itemData(index)
918 regex = self.magic_helper_search.text()
919 self.filter_magic_helper(regex = regex, cls = item)
920
921 def magic_helper_search_changed(self, search_string):
922 item = self.magic_helper_class.itemData(
923 self.magic_helper_class.currentIndex()
924 )
925 self.filter_magic_helper(regex = search_string, cls = item)
926
927 def _magic_helper_get_current(self, item = None):
928 text = None
929 if not isinstance(item, QtGui.QListWidgetItem):
930 item = self.magic_helper_list.currentItem()
931 text = item.text()
932 return text
933
934 def _set_active_frontend_focus(self):
935 # this is a hack, self.active_frontend._control seems to be
936 # a private member. Unfortunately this is the only method
937 # to set focus reliably
938 QtCore.QTimer.singleShot(200, self.active_frontend._control.setFocus)
939
940 def magic_helper_paste_requested(self, item = None):
941 text = self._magic_helper_get_current(item)
942 if text != None:
943 self.active_frontend.input_buffer = text
944 self._set_active_frontend_focus()
945
946 def magic_helper_run_requested(self, item = None):
947 text = self._magic_helper_get_current(item)
948 if text != None:
949 self.active_frontend.execute(text)
950 self._set_active_frontend_focus()
951
952 def filter_magic_helper(self, regex, cls):
953 if regex == "" or regex == None:
954 regex = '.'
955 if cls == None:
956 cls = 'any'
957
958 self.magic_helper_list.clear()
959 for mtype in sorted(self.magic_helper_data):
960 subdict = self.magic_helper_data[mtype]
961 prefix = magic_escapes[mtype]
962
963 for name in sorted(subdict):
964 mclass = subdict[name]
965 pmagic = prefix + name
966
967 if (re.match(regex, name) or re.match(regex, pmagic)) and \
968 (cls == 'any' or cls == mclass):
969 self.magic_helper_list.addItem(pmagic)
970
971
816 972 # minimize/maximize/fullscreen actions:
817 973
818 974 def toggle_menu_bar(self):
819 975 menu_bar = self.menuBar()
820 976 if menu_bar.isVisible():
821 977 menu_bar.setVisible(False)
822 978 else:
823 979 menu_bar.setVisible(True)
824 980
825 981 def toggleMinimized(self):
826 982 if not self.isMinimized():
827 983 self.showMinimized()
828 984 else:
829 985 self.showNormal()
830 986
831 987 def _open_online_help(self):
832 988 filename="http://ipython.org/ipython-doc/stable/index.html"
833 989 webbrowser.open(filename, new=1, autoraise=True)
834 990
835 991 def toggleMaximized(self):
836 992 if not self.isMaximized():
837 993 self.showMaximized()
838 994 else:
839 995 self.showNormal()
840 996
841 997 # Min/Max imizing while in full screen give a bug
842 998 # when going out of full screen, at least on OSX
843 999 def toggleFullScreen(self):
844 1000 if not self.isFullScreen():
845 1001 self.showFullScreen()
846 1002 if sys.platform == 'darwin':
847 1003 self.maximizeAct.setEnabled(False)
848 1004 self.minimizeAct.setEnabled(False)
849 1005 else:
850 1006 self.showNormal()
851 1007 if sys.platform == 'darwin':
852 1008 self.maximizeAct.setEnabled(True)
853 1009 self.minimizeAct.setEnabled(True)
854 1010
855 1011 def set_paging_active_frontend(self, paging):
856 1012 self.active_frontend._set_paging(paging)
857 1013
858 1014 def close_active_frontend(self):
859 1015 self.close_tab(self.active_frontend)
860 1016
861 1017 def restart_kernel_active_frontend(self):
862 1018 self.active_frontend.request_restart_kernel()
863 1019
864 1020 def interrupt_kernel_active_frontend(self):
865 1021 self.active_frontend.request_interrupt_kernel()
866 1022
867 1023 def toggle_confirm_restart_active_frontend(self):
868 1024 widget = self.active_frontend
869 1025 widget.confirm_restart = not widget.confirm_restart
870 1026 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
871 1027
872 1028 def update_restart_checkbox(self):
873 1029 if self.active_frontend is None:
874 1030 return
875 1031 widget = self.active_frontend
876 1032 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
877 1033
878 1034 def cut_active_frontend(self):
879 1035 widget = self.active_frontend
880 1036 if widget.can_cut():
881 1037 widget.cut()
882 1038
883 1039 def copy_active_frontend(self):
884 1040 widget = self.active_frontend
885 1041 widget.copy()
886 1042
887 1043 def copy_raw_active_frontend(self):
888 1044 self.active_frontend._copy_raw_action.trigger()
889 1045
890 1046 def paste_active_frontend(self):
891 1047 widget = self.active_frontend
892 1048 if widget.can_paste():
893 1049 widget.paste()
894 1050
895 1051 def undo_active_frontend(self):
896 1052 self.active_frontend.undo()
897 1053
898 1054 def redo_active_frontend(self):
899 1055 self.active_frontend.redo()
900 1056
901 1057 def reset_magic_active_frontend(self):
902 1058 self.active_frontend.execute("%reset")
903 1059
904 1060 def history_magic_active_frontend(self):
905 1061 self.active_frontend.execute("%history")
906 1062
907 1063 def save_magic_active_frontend(self):
908 1064 self.active_frontend.save_magic()
909 1065
910 1066 def clear_magic_active_frontend(self):
911 1067 self.active_frontend.execute("%clear")
912 1068
913 1069 def who_magic_active_frontend(self):
914 1070 self.active_frontend.execute("%who")
915 1071
916 1072 def who_ls_magic_active_frontend(self):
917 1073 self.active_frontend.execute("%who_ls")
918 1074
919 1075 def whos_magic_active_frontend(self):
920 1076 self.active_frontend.execute("%whos")
921 1077
922 1078 def print_action_active_frontend(self):
923 1079 self.active_frontend.print_action.trigger()
924 1080
925 1081 def export_action_active_frontend(self):
926 1082 self.active_frontend.export_action.trigger()
927 1083
928 1084 def select_all_active_frontend(self):
929 1085 self.active_frontend.select_all_action.trigger()
930 1086
931 1087 def increase_font_size_active_frontend(self):
932 1088 self.active_frontend.increase_font_size.trigger()
933 1089
934 1090 def decrease_font_size_active_frontend(self):
935 1091 self.active_frontend.decrease_font_size.trigger()
936 1092
937 1093 def reset_font_size_active_frontend(self):
938 1094 self.active_frontend.reset_font_size.trigger()
939 1095
940 1096 def guiref_active_frontend(self):
941 1097 self.active_frontend.execute("%guiref")
942 1098
943 1099 def intro_active_frontend(self):
944 1100 self.active_frontend.execute("?")
945 1101
946 1102 def quickref_active_frontend(self):
947 1103 self.active_frontend.execute("%quickref")
948 1104 #---------------------------------------------------------------------------
949 1105 # QWidget interface
950 1106 #---------------------------------------------------------------------------
951 1107
952 1108 def closeEvent(self, event):
953 1109 """ Forward the close event to every tabs contained by the windows
954 1110 """
955 1111 if self.tab_widget.count() == 0:
956 1112 # no tabs, just close
957 1113 event.accept()
958 1114 return
959 1115 # Do Not loop on the widget count as it change while closing
960 1116 title = self.window().windowTitle()
961 1117 cancel = QtGui.QMessageBox.Cancel
962 1118 okay = QtGui.QMessageBox.Ok
963 1119
964 1120 if self.confirm_exit:
965 1121 if self.tab_widget.count() > 1:
966 1122 msg = "Close all tabs, stop all kernels, and Quit?"
967 1123 else:
968 1124 msg = "Close console, stop kernel, and Quit?"
969 1125 info = "Kernels not started here (e.g. notebooks) will be left alone."
970 1126 closeall = QtGui.QPushButton("&Quit", self)
971 1127 closeall.setShortcut('Q')
972 1128 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
973 1129 title, msg)
974 1130 box.setInformativeText(info)
975 1131 box.addButton(cancel)
976 1132 box.addButton(closeall, QtGui.QMessageBox.YesRole)
977 1133 box.setDefaultButton(closeall)
978 1134 box.setEscapeButton(cancel)
979 1135 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
980 1136 box.setIconPixmap(pixmap)
981 1137 reply = box.exec_()
982 1138 else:
983 1139 reply = okay
984 1140
985 1141 if reply == cancel:
986 1142 event.ignore()
987 1143 return
988 1144 if reply == okay:
989 1145 while self.tab_widget.count() >= 1:
990 1146 # prevent further confirmations:
991 1147 widget = self.active_frontend
992 1148 widget._confirm_exit = False
993 1149 self.close_tab(widget)
994 1150 event.accept()
995 1151
@@ -1,378 +1,379 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 * Paul Ivanov
15 15
16 16 """
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # stdlib imports
23 23 import os
24 24 import signal
25 25 import sys
26 26
27 27 # If run on Windows, install an exception hook which pops up a
28 28 # message box. Pythonw.exe hides the console, so without this
29 29 # the application silently fails to load.
30 30 #
31 31 # We always install this handler, because the expectation is for
32 32 # qtconsole to bring up a GUI even if called from the console.
33 33 # The old handler is called, so the exception is printed as well.
34 34 # If desired, check for pythonw with an additional condition
35 35 # (sys.executable.lower().find('pythonw.exe') >= 0).
36 36 if os.name == 'nt':
37 37 old_excepthook = sys.excepthook
38 38
39 39 # Exclude this from our autogenerated API docs.
40 40 undoc = lambda func: func
41 41
42 42 @undoc
43 43 def gui_excepthook(exctype, value, tb):
44 44 try:
45 45 import ctypes, traceback
46 46 MB_ICONERROR = 0x00000010
47 47 title = u'Error starting IPython QtConsole'
48 48 msg = u''.join(traceback.format_exception(exctype, value, tb))
49 49 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
50 50 finally:
51 51 # Also call the old exception hook to let it do
52 52 # its thing too.
53 53 old_excepthook(exctype, value, tb)
54 54
55 55 sys.excepthook = gui_excepthook
56 56
57 57 # System library imports
58 58 from IPython.external.qt import QtCore, QtGui
59 59
60 60 # Local imports
61 61 from IPython.config.application import catch_config_error
62 62 from IPython.core.application import BaseIPythonApplication
63 63 from IPython.qt.console.ipython_widget import IPythonWidget
64 64 from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
65 65 from IPython.qt.console import styles
66 66 from IPython.qt.console.mainwindow import MainWindow
67 67 from IPython.qt.client import QtKernelClient
68 68 from IPython.qt.manager import QtKernelManager
69 69 from IPython.utils.traitlets import (
70 70 Dict, Unicode, CBool, Any
71 71 )
72 72
73 73 from IPython.consoleapp import (
74 74 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
75 75 )
76 76
77 77 #-----------------------------------------------------------------------------
78 78 # Network Constants
79 79 #-----------------------------------------------------------------------------
80 80
81 81 from IPython.utils.localinterfaces import is_local_ip
82 82
83 83 #-----------------------------------------------------------------------------
84 84 # Globals
85 85 #-----------------------------------------------------------------------------
86 86
87 87 _examples = """
88 88 ipython qtconsole # start the qtconsole
89 89 ipython qtconsole --matplotlib=inline # start with matplotlib inline plotting mode
90 90 """
91 91
92 92 #-----------------------------------------------------------------------------
93 93 # Aliases and Flags
94 94 #-----------------------------------------------------------------------------
95 95
96 96 # start with copy of flags
97 97 flags = dict(flags)
98 98 qt_flags = {
99 99 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
100 100 "Disable rich text support."),
101 101 }
102 102
103 103 # and app_flags from the Console Mixin
104 104 qt_flags.update(app_flags)
105 105 # add frontend flags to the full set
106 106 flags.update(qt_flags)
107 107
108 108 # start with copy of front&backend aliases list
109 109 aliases = dict(aliases)
110 110 qt_aliases = dict(
111 111 style = 'IPythonWidget.syntax_style',
112 112 stylesheet = 'IPythonQtConsoleApp.stylesheet',
113 113 colors = 'ZMQInteractiveShell.colors',
114 114
115 115 editor = 'IPythonWidget.editor',
116 116 paging = 'ConsoleWidget.paging',
117 117 )
118 118 # and app_aliases from the Console Mixin
119 119 qt_aliases.update(app_aliases)
120 120 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
121 121 # add frontend aliases to the full set
122 122 aliases.update(qt_aliases)
123 123
124 124 # get flags&aliases into sets, and remove a couple that
125 125 # shouldn't be scrubbed from backend flags:
126 126 qt_aliases = set(qt_aliases.keys())
127 127 qt_aliases.remove('colors')
128 128 qt_flags = set(qt_flags.keys())
129 129
130 130 #-----------------------------------------------------------------------------
131 131 # Classes
132 132 #-----------------------------------------------------------------------------
133 133
134 134 #-----------------------------------------------------------------------------
135 135 # IPythonQtConsole
136 136 #-----------------------------------------------------------------------------
137 137
138 138
139 139 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
140 140 name = 'ipython-qtconsole'
141 141
142 142 description = """
143 143 The IPython QtConsole.
144 144
145 145 This launches a Console-style application using Qt. It is not a full
146 146 console, in that launched terminal subprocesses will not be able to accept
147 147 input.
148 148
149 149 The QtConsole supports various extra features beyond the Terminal IPython
150 150 shell, such as inline plotting with matplotlib, via:
151 151
152 152 ipython qtconsole --matplotlib=inline
153 153
154 154 as well as saving your session as HTML, and printing the output.
155 155
156 156 """
157 157 examples = _examples
158 158
159 159 classes = [IPythonWidget] + IPythonConsoleApp.classes
160 160 flags = Dict(flags)
161 161 aliases = Dict(aliases)
162 162 frontend_flags = Any(qt_flags)
163 163 frontend_aliases = Any(qt_aliases)
164 164 kernel_client_class = QtKernelClient
165 165 kernel_manager_class = QtKernelManager
166 166
167 167 stylesheet = Unicode('', config=True,
168 168 help="path to a custom CSS stylesheet")
169 169
170 170 hide_menubar = CBool(False, config=True,
171 171 help="Start the console window with the menu bar hidden.")
172 172
173 173 maximize = CBool(False, config=True,
174 174 help="Start the console window maximized.")
175 175
176 176 plain = CBool(False, config=True,
177 177 help="Use a plaintext widget instead of rich text (plain can't print/save).")
178 178
179 179 def _plain_changed(self, name, old, new):
180 180 kind = 'plain' if new else 'rich'
181 181 self.config.ConsoleWidget.kind = kind
182 182 if new:
183 183 self.widget_factory = IPythonWidget
184 184 else:
185 185 self.widget_factory = RichIPythonWidget
186 186
187 187 # the factory for creating a widget
188 188 widget_factory = Any(RichIPythonWidget)
189 189
190 190 def parse_command_line(self, argv=None):
191 191 super(IPythonQtConsoleApp, self).parse_command_line(argv)
192 192 self.build_kernel_argv(argv)
193 193
194 194
195 195 def new_frontend_master(self):
196 196 """ Create and return new frontend attached to new kernel, launched on localhost.
197 197 """
198 198 kernel_manager = self.kernel_manager_class(
199 199 connection_file=self._new_connection_file(),
200 200 parent=self,
201 201 autorestart=True,
202 202 )
203 203 # start the kernel
204 204 kwargs = dict()
205 205 kwargs['extra_arguments'] = self.kernel_argv
206 206 kernel_manager.start_kernel(**kwargs)
207 207 kernel_manager.client_factory = self.kernel_client_class
208 208 kernel_client = kernel_manager.client()
209 209 kernel_client.start_channels(shell=True, iopub=True)
210 210 widget = self.widget_factory(config=self.config,
211 211 local_kernel=True)
212 212 self.init_colors(widget)
213 213 widget.kernel_manager = kernel_manager
214 214 widget.kernel_client = kernel_client
215 215 widget._existing = False
216 216 widget._may_close = True
217 217 widget._confirm_exit = self.confirm_exit
218 218 return widget
219 219
220 220 def new_frontend_slave(self, current_widget):
221 221 """Create and return a new frontend attached to an existing kernel.
222 222
223 223 Parameters
224 224 ----------
225 225 current_widget : IPythonWidget
226 226 The IPythonWidget whose kernel this frontend is to share
227 227 """
228 228 kernel_client = self.kernel_client_class(
229 229 connection_file=current_widget.kernel_client.connection_file,
230 230 config = self.config,
231 231 )
232 232 kernel_client.load_connection_file()
233 233 kernel_client.start_channels()
234 234 widget = self.widget_factory(config=self.config,
235 235 local_kernel=False)
236 236 self.init_colors(widget)
237 237 widget._existing = True
238 238 widget._may_close = False
239 239 widget._confirm_exit = False
240 240 widget.kernel_client = kernel_client
241 241 widget.kernel_manager = current_widget.kernel_manager
242 242 return widget
243 243
244 244 def init_qt_app(self):
245 245 # separate from qt_elements, because it must run first
246 246 self.app = QtGui.QApplication([])
247 247
248 248 def init_qt_elements(self):
249 249 # Create the widget.
250 250
251 251 base_path = os.path.abspath(os.path.dirname(__file__))
252 252 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
253 253 self.app.icon = QtGui.QIcon(icon_path)
254 254 QtGui.QApplication.setWindowIcon(self.app.icon)
255 255
256 256 ip = self.ip
257 257 local_kernel = (not self.existing) or is_local_ip(ip)
258 258 self.widget = self.widget_factory(config=self.config,
259 259 local_kernel=local_kernel)
260 260 self.init_colors(self.widget)
261 261 self.widget._existing = self.existing
262 262 self.widget._may_close = not self.existing
263 263 self.widget._confirm_exit = self.confirm_exit
264 264
265 265 self.widget.kernel_manager = self.kernel_manager
266 266 self.widget.kernel_client = self.kernel_client
267 267 self.window = MainWindow(self.app,
268 268 confirm_exit=self.confirm_exit,
269 269 new_frontend_factory=self.new_frontend_master,
270 270 slave_frontend_factory=self.new_frontend_slave,
271 271 )
272 272 self.window.log = self.log
273 273 self.window.add_tab_with_frontend(self.widget)
274 274 self.window.init_menu_bar()
275 self.window.init_magic_helper()
275 276
276 277 # Ignore on OSX, where there is always a menu bar
277 278 if sys.platform != 'darwin' and self.hide_menubar:
278 279 self.window.menuBar().setVisible(False)
279 280
280 281 self.window.setWindowTitle('IPython')
281 282
282 283 def init_colors(self, widget):
283 284 """Configure the coloring of the widget"""
284 285 # Note: This will be dramatically simplified when colors
285 286 # are removed from the backend.
286 287
287 288 # parse the colors arg down to current known labels
288 289 cfg = self.config
289 290 colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
290 291 style = cfg.IPythonWidget.syntax_style if 'IPythonWidget.syntax_style' in cfg else None
291 292 sheet = cfg.IPythonWidget.style_sheet if 'IPythonWidget.style_sheet' in cfg else None
292 293
293 294 # find the value for colors:
294 295 if colors:
295 296 colors=colors.lower()
296 297 if colors in ('lightbg', 'light'):
297 298 colors='lightbg'
298 299 elif colors in ('dark', 'linux'):
299 300 colors='linux'
300 301 else:
301 302 colors='nocolor'
302 303 elif style:
303 304 if style=='bw':
304 305 colors='nocolor'
305 306 elif styles.dark_style(style):
306 307 colors='linux'
307 308 else:
308 309 colors='lightbg'
309 310 else:
310 311 colors=None
311 312
312 313 # Configure the style
313 314 if style:
314 315 widget.style_sheet = styles.sheet_from_template(style, colors)
315 316 widget.syntax_style = style
316 317 widget._syntax_style_changed()
317 318 widget._style_sheet_changed()
318 319 elif colors:
319 320 # use a default dark/light/bw style
320 321 widget.set_default_style(colors=colors)
321 322
322 323 if self.stylesheet:
323 324 # we got an explicit stylesheet
324 325 if os.path.isfile(self.stylesheet):
325 326 with open(self.stylesheet) as f:
326 327 sheet = f.read()
327 328 else:
328 329 raise IOError("Stylesheet %r not found." % self.stylesheet)
329 330 if sheet:
330 331 widget.style_sheet = sheet
331 332 widget._style_sheet_changed()
332 333
333 334
334 335 def init_signal(self):
335 336 """allow clean shutdown on sigint"""
336 337 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
337 338 # need a timer, so that QApplication doesn't block until a real
338 339 # Qt event fires (can require mouse movement)
339 340 # timer trick from http://stackoverflow.com/q/4938723/938949
340 341 timer = QtCore.QTimer()
341 342 # Let the interpreter run each 200 ms:
342 343 timer.timeout.connect(lambda: None)
343 344 timer.start(200)
344 345 # hold onto ref, so the timer doesn't get cleaned up
345 346 self._sigint_timer = timer
346 347
347 348 @catch_config_error
348 349 def initialize(self, argv=None):
349 350 self.init_qt_app()
350 351 super(IPythonQtConsoleApp, self).initialize(argv)
351 352 IPythonConsoleApp.initialize(self,argv)
352 353 self.init_qt_elements()
353 354 self.init_signal()
354 355
355 356 def start(self):
356 357
357 358 # draw the window
358 359 if self.maximize:
359 360 self.window.showMaximized()
360 361 else:
361 362 self.window.show()
362 363 self.window.raise_()
363 364
364 365 # Start the application main loop.
365 366 self.app.exec_()
366 367
367 368 #-----------------------------------------------------------------------------
368 369 # Main entry point
369 370 #-----------------------------------------------------------------------------
370 371
371 372 def main():
372 373 app = IPythonQtConsoleApp()
373 374 app.initialize()
374 375 app.start()
375 376
376 377
377 378 if __name__ == '__main__':
378 379 main()
General Comments 0
You need to be logged in to leave comments. Login now