mainwindow.py
969 lines
| 36.9 KiB
| text/x-python
|
PythonLexer
MinRK
|
r5136 | """The Qt MainWindow for the QtConsole | ||
This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for | ||||
common actions. | ||||
Authors: | ||||
* Evan Patterson | ||||
* Min RK | ||||
* Erik Tollerud | ||||
* Fernando Perez | ||||
* Bussonnier Matthias | ||||
* Thomas Kluyver | ||||
Paul Ivanov
|
r7593 | * Paul Ivanov | ||
MinRK
|
r5136 | |||
""" | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
# stdlib imports | ||||
import sys | ||||
Matthias BUSSONNIER
|
r5396 | import re | ||
MinRK
|
r5136 | import webbrowser | ||
Matthias BUSSONNIER
|
r7270 | import ast | ||
MinRK
|
r5142 | from threading import Thread | ||
MinRK
|
r5136 | |||
# System library imports | ||||
from IPython.external.qt import QtGui,QtCore | ||||
MinRK
|
r5142 | def background(f): | ||
"""call a function in a simple thread, to prevent blocking""" | ||||
t = Thread(target=f) | ||||
t.start() | ||||
return t | ||||
MinRK
|
r5136 | #----------------------------------------------------------------------------- | ||
# Classes | ||||
#----------------------------------------------------------------------------- | ||||
class MainWindow(QtGui.QMainWindow): | ||||
#--------------------------------------------------------------------------- | ||||
# 'object' interface | ||||
#--------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r7270 | _magic_menu_dict = {} | ||
MinRK
|
r5138 | def __init__(self, app, | ||
MinRK
|
r5136 | confirm_exit=True, | ||
new_frontend_factory=None, slave_frontend_factory=None, | ||||
): | ||||
""" Create a tabbed MainWindow for managing IPython FrontendWidgets | ||||
Parameters | ||||
---------- | ||||
app : reference to QApplication parent | ||||
confirm_exit : bool, optional | ||||
Whether we should prompt on close of tabs | ||||
new_frontend_factory : callable | ||||
A callable that returns a new IPythonWidget instance, attached to | ||||
its own running kernel. | ||||
slave_frontend_factory : callable | ||||
A callable that takes an existing IPythonWidget, and returns a new | ||||
IPythonWidget instance, attached to the same kernel. | ||||
""" | ||||
super(MainWindow, self).__init__() | ||||
MinRK
|
r5144 | self._kernel_counter = 0 | ||
MinRK
|
r5136 | self._app = app | ||
MinRK
|
r5138 | self.confirm_exit = confirm_exit | ||
MinRK
|
r5136 | self.new_frontend_factory = new_frontend_factory | ||
self.slave_frontend_factory = slave_frontend_factory | ||||
self.tab_widget = QtGui.QTabWidget(self) | ||||
self.tab_widget.setDocumentMode(True) | ||||
self.tab_widget.setTabsClosable(True) | ||||
self.tab_widget.tabCloseRequested[int].connect(self.close_tab) | ||||
self.setCentralWidget(self.tab_widget) | ||||
MinRK
|
r5141 | # hide tab bar at first, since we have no tabs: | ||
MinRK
|
r5138 | self.tab_widget.tabBar().setVisible(False) | ||
MinRK
|
r5141 | # prevent focus in tab bar | ||
self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus) | ||||
MinRK
|
r5136 | |||
def update_tab_bar_visibility(self): | ||||
""" update visibility of the tabBar depending of the number of tab | ||||
0 or 1 tab, tabBar hidden | ||||
2+ tabs, tabBar visible | ||||
send a self.close if number of tab ==0 | ||||
Paul Ivanov
|
r7589 | need to be called explicitly, or be connected to tabInserted/tabRemoved | ||
MinRK
|
r5136 | """ | ||
if self.tab_widget.count() <= 1: | ||||
self.tab_widget.tabBar().setVisible(False) | ||||
else: | ||||
self.tab_widget.tabBar().setVisible(True) | ||||
if self.tab_widget.count()==0 : | ||||
self.close() | ||||
@property | ||||
MinRK
|
r5144 | def next_kernel_id(self): | ||
"""constantly increasing counter for kernel IDs""" | ||||
c = self._kernel_counter | ||||
self._kernel_counter += 1 | ||||
return c | ||||
@property | ||||
MinRK
|
r5136 | def active_frontend(self): | ||
return self.tab_widget.currentWidget() | ||||
def create_tab_with_new_frontend(self): | ||||
"""create a new frontend and attach it to a new tab""" | ||||
widget = self.new_frontend_factory() | ||||
self.add_tab_with_frontend(widget) | ||||
def create_tab_with_current_kernel(self): | ||||
"""create a new frontend attached to the same kernel as the current tab""" | ||||
current_widget = self.tab_widget.currentWidget() | ||||
current_widget_index = self.tab_widget.indexOf(current_widget) | ||||
current_widget_name = self.tab_widget.tabText(current_widget_index) | ||||
widget = self.slave_frontend_factory(current_widget) | ||||
if 'slave' in current_widget_name: | ||||
# don't keep stacking slaves | ||||
name = current_widget_name | ||||
else: | ||||
MinRK
|
r5144 | name = '(%s) slave' % current_widget_name | ||
MinRK
|
r5136 | self.add_tab_with_frontend(widget,name=name) | ||
def close_tab(self,current_tab): | ||||
""" Called when you need to try to close a tab. | ||||
Paul Ivanov
|
r7589 | It takes the number of the tab to be closed as argument, or a reference | ||
to the widget inside this tab | ||||
MinRK
|
r5136 | """ | ||
Paul Ivanov
|
r7589 | # let's be sure "tab" and "closing widget" are respectively the index | ||
# of the tab to close and a reference to the frontend to close | ||||
MinRK
|
r5136 | if type(current_tab) is not int : | ||
current_tab = self.tab_widget.indexOf(current_tab) | ||||
closing_widget=self.tab_widget.widget(current_tab) | ||||
Paul Ivanov
|
r7589 | # when trying to be closed, widget might re-send a request to be | ||
# closed again, but will be deleted when event will be processed. So | ||||
# need to check that widget still exists and skip if not. One example | ||||
# of this is when 'exit' is sent in a slave tab. 'exit' will be | ||||
# re-sent by this function on the master widget, which ask all slave | ||||
# widgets to exit | ||||
MinRK
|
r5136 | if closing_widget==None: | ||
return | ||||
MinRK
|
r5138 | #get a list of all slave widgets on the same kernel. | ||
slave_tabs = self.find_slave_widgets(closing_widget) | ||||
MinRK
|
r5136 | |||
keepkernel = None #Use the prompt by default | ||||
if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic | ||||
keepkernel = closing_widget._keep_kernel_on_exit | ||||
MinRK
|
r5138 | # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None) | ||
# we set local slave tabs._hidden to True to avoid prompting for kernel | ||||
# restart when they get the signal. and then "forward" the 'exit' | ||||
# to the main window | ||||
MinRK
|
r5136 | if keepkernel is not None: | ||
for tab in slave_tabs: | ||||
tab._hidden = True | ||||
MinRK
|
r5138 | if closing_widget in slave_tabs: | ||
MinRK
|
r5136 | try : | ||
self.find_master_tab(closing_widget).execute('exit') | ||||
except AttributeError: | ||||
self.log.info("Master already closed or not local, closing only current tab") | ||||
self.tab_widget.removeTab(current_tab) | ||||
MinRK
|
r5138 | self.update_tab_bar_visibility() | ||
MinRK
|
r5136 | return | ||
kernel_manager = closing_widget.kernel_manager | ||||
if keepkernel is None and not closing_widget._confirm_exit: | ||||
# don't prompt, just terminate the kernel if we own it | ||||
# or leave it alone if we don't | ||||
MinRK
|
r5138 | keepkernel = closing_widget._existing | ||
MinRK
|
r5136 | if keepkernel is None: #show prompt | ||
if kernel_manager and kernel_manager.channels_running: | ||||
title = self.window().windowTitle() | ||||
cancel = QtGui.QMessageBox.Cancel | ||||
okay = QtGui.QMessageBox.Ok | ||||
if closing_widget._may_close: | ||||
msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"' | ||||
MinRK
|
r5138 | info = "Would you like to quit the Kernel and close all attached Consoles as well?" | ||
justthis = QtGui.QPushButton("&No, just this Tab", self) | ||||
MinRK
|
r5136 | justthis.setShortcut('N') | ||
MinRK
|
r5138 | closeall = QtGui.QPushButton("&Yes, close all", self) | ||
MinRK
|
r5136 | closeall.setShortcut('Y') | ||
Paul Ivanov
|
r5964 | # allow ctrl-d ctrl-d exit, like in terminal | ||
closeall.setShortcut('Ctrl+D') | ||||
MinRK
|
r5136 | box = QtGui.QMessageBox(QtGui.QMessageBox.Question, | ||
title, msg) | ||||
box.setInformativeText(info) | ||||
box.addButton(cancel) | ||||
box.addButton(justthis, QtGui.QMessageBox.NoRole) | ||||
box.addButton(closeall, QtGui.QMessageBox.YesRole) | ||||
box.setDefaultButton(closeall) | ||||
box.setEscapeButton(cancel) | ||||
pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64))) | ||||
box.setIconPixmap(pixmap) | ||||
reply = box.exec_() | ||||
if reply == 1: # close All | ||||
for slave in slave_tabs: | ||||
MinRK
|
r5142 | background(slave.kernel_manager.stop_channels) | ||
MinRK
|
r5136 | self.tab_widget.removeTab(self.tab_widget.indexOf(slave)) | ||
closing_widget.execute("exit") | ||||
self.tab_widget.removeTab(current_tab) | ||||
MinRK
|
r5142 | background(kernel_manager.stop_channels) | ||
MinRK
|
r5136 | elif reply == 0: # close Console | ||
if not closing_widget._existing: | ||||
MinRK
|
r5138 | # Have kernel: don't quit, just close the tab | ||
MinRK
|
r5136 | closing_widget.execute("exit True") | ||
MinRK
|
r5138 | self.tab_widget.removeTab(current_tab) | ||
MinRK
|
r5142 | background(kernel_manager.stop_channels) | ||
MinRK
|
r5136 | else: | ||
reply = QtGui.QMessageBox.question(self, title, | ||||
"Are you sure you want to close this Console?"+ | ||||
"\nThe Kernel and other Consoles will remain active.", | ||||
okay|cancel, | ||||
defaultButton=okay | ||||
) | ||||
if reply == okay: | ||||
self.tab_widget.removeTab(current_tab) | ||||
elif keepkernel: #close console but leave kernel running (no prompt) | ||||
MinRK
|
r5138 | self.tab_widget.removeTab(current_tab) | ||
MinRK
|
r5142 | background(kernel_manager.stop_channels) | ||
MinRK
|
r5136 | else: #close console and kernel (no prompt) | ||
MinRK
|
r5138 | self.tab_widget.removeTab(current_tab) | ||
MinRK
|
r5136 | if kernel_manager and kernel_manager.channels_running: | ||
for slave in slave_tabs: | ||||
MinRK
|
r5142 | background(slave.kernel_manager.stop_channels) | ||
MinRK
|
r5136 | self.tab_widget.removeTab(self.tab_widget.indexOf(slave)) | ||
MinRK
|
r5142 | kernel_manager.shutdown_kernel() | ||
background(kernel_manager.stop_channels) | ||||
MinRK
|
r5138 | |||
MinRK
|
r5136 | self.update_tab_bar_visibility() | ||
def add_tab_with_frontend(self,frontend,name=None): | ||||
""" insert a tab with a given frontend in the tab bar, and give it a name | ||||
""" | ||||
if not name: | ||||
MinRK
|
r5144 | name = 'kernel %i' % self.next_kernel_id | ||
MinRK
|
r5136 | self.tab_widget.addTab(frontend,name) | ||
self.update_tab_bar_visibility() | ||||
self.make_frontend_visible(frontend) | ||||
frontend.exit_requested.connect(self.close_tab) | ||||
def next_tab(self): | ||||
self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1)) | ||||
def prev_tab(self): | ||||
self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1)) | ||||
def make_frontend_visible(self,frontend): | ||||
widget_index=self.tab_widget.indexOf(frontend) | ||||
if widget_index > 0 : | ||||
self.tab_widget.setCurrentIndex(widget_index) | ||||
def find_master_tab(self,tab,as_list=False): | ||||
""" | ||||
Paul Ivanov
|
r7589 | Try to return the frontend that owns the kernel attached to the given widget/tab. | ||
MinRK
|
r5136 | |||
Paul Ivanov
|
r7589 | Only finds frontend owned by the current application. Selection | ||
based on port of the kernel might be inaccurate if several kernel | ||||
MinRK
|
r5136 | on different ip use same port number. | ||
Paul Ivanov
|
r7589 | This function does the conversion tabNumber/widget if needed. | ||
MinRK
|
r5136 | Might return None if no master widget (non local kernel) | ||
Will crash IPython if more than 1 masterWidget | ||||
When asList set to True, always return a list of widget(s) owning | ||||
the kernel. The list might be empty or containing several Widget. | ||||
""" | ||||
#convert from/to int/richIpythonWidget if needed | ||||
MinRK
|
r5138 | if isinstance(tab, int): | ||
MinRK
|
r5136 | tab = self.tab_widget.widget(tab) | ||
MinRK
|
r5138 | km=tab.kernel_manager | ||
MinRK
|
r5136 | |||
#build list of all widgets | ||||
widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())] | ||||
# widget that are candidate to be the owner of the kernel does have all the same port of the curent widget | ||||
# And should have a _may_close attribute | ||||
MinRK
|
r5138 | filtered_widget_list = [ widget for widget in widget_list if | ||
MinRK
|
r5136 | widget.kernel_manager.connection_file == km.connection_file and | ||
hasattr(widget,'_may_close') ] | ||||
# the master widget is the one that may close the kernel | ||||
MinRK
|
r5138 | master_widget= [ widget for widget in filtered_widget_list if widget._may_close] | ||
MinRK
|
r5136 | if as_list: | ||
return master_widget | ||||
assert(len(master_widget)<=1 ) | ||||
if len(master_widget)==0: | ||||
return None | ||||
return master_widget[0] | ||||
MinRK
|
r5138 | def find_slave_widgets(self,tab): | ||
"""return all the frontends that do not own the kernel attached to the given widget/tab. | ||||
MinRK
|
r5136 | |||
MinRK
|
r5138 | Only find frontends owned by the current application. Selection | ||
based on connection file of the kernel. | ||||
MinRK
|
r5136 | |||
MinRK
|
r5138 | This function does the conversion tabNumber/widget if needed. | ||
MinRK
|
r5136 | """ | ||
#convert from/to int/richIpythonWidget if needed | ||||
MinRK
|
r5138 | if isinstance(tab, int): | ||
MinRK
|
r5136 | tab = self.tab_widget.widget(tab) | ||
MinRK
|
r5138 | km=tab.kernel_manager | ||
MinRK
|
r5136 | |||
#build list of all widgets | ||||
widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())] | ||||
# widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget | ||||
filtered_widget_list = ( widget for widget in widget_list if | ||||
widget.kernel_manager.connection_file == km.connection_file) | ||||
# Get a list of all widget owning the same kernel and removed it from | ||||
# the previous cadidate. (better using sets ?) | ||||
MinRK
|
r5138 | master_widget_list = self.find_master_tab(tab, as_list=True) | ||
MinRK
|
r5136 | slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list] | ||
return slave_list | ||||
# Populate the menu bar with common actions and shortcuts | ||||
MinRK
|
r5140 | def add_menu_action(self, menu, action, defer_shortcut=False): | ||
MinRK
|
r5136 | """Add action to menu as well as self | ||
So that when the menu bar is invisible, its actions are still available. | ||||
MinRK
|
r5140 | |||
If defer_shortcut is True, set the shortcut context to widget-only, | ||||
where it will avoid conflict with shortcuts already bound to the | ||||
widgets themselves. | ||||
MinRK
|
r5136 | """ | ||
menu.addAction(action) | ||||
self.addAction(action) | ||||
MinRK
|
r5140 | |||
if defer_shortcut: | ||||
action.setShortcutContext(QtCore.Qt.WidgetShortcut) | ||||
MinRK
|
r5136 | |||
def init_menu_bar(self): | ||||
#create menu in the order they should appear in the menu bar | ||||
self.init_file_menu() | ||||
self.init_edit_menu() | ||||
self.init_view_menu() | ||||
self.init_kernel_menu() | ||||
self.init_magic_menu() | ||||
self.init_window_menu() | ||||
self.init_help_menu() | ||||
def init_file_menu(self): | ||||
self.file_menu = self.menuBar().addMenu("&File") | ||||
self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel", | ||||
self, | ||||
shortcut="Ctrl+T", | ||||
triggered=self.create_tab_with_new_frontend) | ||||
self.add_menu_action(self.file_menu, self.new_kernel_tab_act) | ||||
self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel", | ||||
self, | ||||
shortcut="Ctrl+Shift+T", | ||||
triggered=self.create_tab_with_current_kernel) | ||||
self.add_menu_action(self.file_menu, self.slave_kernel_tab_act) | ||||
self.file_menu.addSeparator() | ||||
self.close_action=QtGui.QAction("&Close Tab", | ||||
self, | ||||
MinRK
|
r5140 | shortcut=QtGui.QKeySequence.Close, | ||
MinRK
|
r5136 | triggered=self.close_active_frontend | ||
) | ||||
self.add_menu_action(self.file_menu, self.close_action) | ||||
self.export_action=QtGui.QAction("&Save to HTML/XHTML", | ||||
self, | ||||
MinRK
|
r5140 | shortcut=QtGui.QKeySequence.Save, | ||
MinRK
|
r5136 | triggered=self.export_action_active_frontend | ||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.file_menu, self.export_action, True) | ||
MinRK
|
r5136 | |||
self.file_menu.addSeparator() | ||||
MinRK
|
r5140 | printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print) | ||
if printkey.matches("Ctrl+P") and sys.platform != 'darwin': | ||||
# Only override the default if there is a collision. | ||||
# Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. | ||||
printkey = "Ctrl+Shift+P" | ||||
MinRK
|
r5136 | self.print_action = QtGui.QAction("&Print", | ||
self, | ||||
MinRK
|
r5140 | shortcut=printkey, | ||
MinRK
|
r5136 | triggered=self.print_action_active_frontend) | ||
MinRK
|
r5140 | self.add_menu_action(self.file_menu, self.print_action, True) | ||
MinRK
|
r5136 | |||
MinRK
|
r5139 | if sys.platform != 'darwin': | ||
MinRK
|
r5136 | # OSX always has Quit in the Application menu, only add it | ||
# to the File menu elsewhere. | ||||
self.file_menu.addSeparator() | ||||
self.quit_action = QtGui.QAction("&Quit", | ||||
self, | ||||
shortcut=QtGui.QKeySequence.Quit, | ||||
triggered=self.close, | ||||
) | ||||
self.add_menu_action(self.file_menu, self.quit_action) | ||||
def init_edit_menu(self): | ||||
self.edit_menu = self.menuBar().addMenu("&Edit") | ||||
self.undo_action = QtGui.QAction("&Undo", | ||||
self, | ||||
MinRK
|
r5140 | shortcut=QtGui.QKeySequence.Undo, | ||
MinRK
|
r5136 | statusTip="Undo last action if possible", | ||
triggered=self.undo_active_frontend | ||||
) | ||||
self.add_menu_action(self.edit_menu, self.undo_action) | ||||
self.redo_action = QtGui.QAction("&Redo", | ||||
self, | ||||
MinRK
|
r5140 | shortcut=QtGui.QKeySequence.Redo, | ||
MinRK
|
r5136 | statusTip="Redo last action if possible", | ||
triggered=self.redo_active_frontend) | ||||
self.add_menu_action(self.edit_menu, self.redo_action) | ||||
self.edit_menu.addSeparator() | ||||
self.cut_action = QtGui.QAction("&Cut", | ||||
self, | ||||
shortcut=QtGui.QKeySequence.Cut, | ||||
triggered=self.cut_active_frontend | ||||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.edit_menu, self.cut_action, True) | ||
MinRK
|
r5136 | |||
self.copy_action = QtGui.QAction("&Copy", | ||||
self, | ||||
shortcut=QtGui.QKeySequence.Copy, | ||||
triggered=self.copy_active_frontend | ||||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.edit_menu, self.copy_action, True) | ||
MinRK
|
r5136 | |||
self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)", | ||||
self, | ||||
shortcut="Ctrl+Shift+C", | ||||
triggered=self.copy_raw_active_frontend | ||||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.edit_menu, self.copy_raw_action, True) | ||
MinRK
|
r5136 | |||
self.paste_action = QtGui.QAction("&Paste", | ||||
self, | ||||
shortcut=QtGui.QKeySequence.Paste, | ||||
triggered=self.paste_active_frontend | ||||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.edit_menu, self.paste_action, True) | ||
MinRK
|
r5136 | |||
self.edit_menu.addSeparator() | ||||
MinRK
|
r5140 | |||
selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll) | ||||
if selectall.matches("Ctrl+A") and sys.platform != 'darwin': | ||||
# Only override the default if there is a collision. | ||||
# Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. | ||||
selectall = "Ctrl+Shift+A" | ||||
MinRK
|
r5136 | self.select_all_action = QtGui.QAction("Select &All", | ||
self, | ||||
MinRK
|
r5140 | shortcut=selectall, | ||
MinRK
|
r5136 | triggered=self.select_all_active_frontend | ||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.edit_menu, self.select_all_action, True) | ||
MinRK
|
r5136 | |||
def init_view_menu(self): | ||||
self.view_menu = self.menuBar().addMenu("&View") | ||||
if sys.platform != 'darwin': | ||||
# disable on OSX, where there is always a menu bar | ||||
self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar", | ||||
self, | ||||
MinRK
|
r5137 | shortcut="Ctrl+Shift+M", | ||
MinRK
|
r5136 | statusTip="Toggle visibility of menubar", | ||
triggered=self.toggle_menu_bar) | ||||
self.add_menu_action(self.view_menu, self.toggle_menu_bar_act) | ||||
fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11" | ||||
self.full_screen_act = QtGui.QAction("&Full Screen", | ||||
self, | ||||
shortcut=fs_key, | ||||
statusTip="Toggle between Fullscreen and Normal Size", | ||||
triggered=self.toggleFullScreen) | ||||
self.add_menu_action(self.view_menu, self.full_screen_act) | ||||
self.view_menu.addSeparator() | ||||
self.increase_font_size = QtGui.QAction("Zoom &In", | ||||
self, | ||||
MinRK
|
r5140 | shortcut=QtGui.QKeySequence.ZoomIn, | ||
MinRK
|
r5136 | triggered=self.increase_font_size_active_frontend | ||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.view_menu, self.increase_font_size, True) | ||
MinRK
|
r5136 | |||
self.decrease_font_size = QtGui.QAction("Zoom &Out", | ||||
self, | ||||
MinRK
|
r5140 | shortcut=QtGui.QKeySequence.ZoomOut, | ||
MinRK
|
r5136 | triggered=self.decrease_font_size_active_frontend | ||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.view_menu, self.decrease_font_size, True) | ||
MinRK
|
r5136 | |||
self.reset_font_size = QtGui.QAction("Zoom &Reset", | ||||
self, | ||||
shortcut="Ctrl+0", | ||||
triggered=self.reset_font_size_active_frontend | ||||
) | ||||
MinRK
|
r5140 | self.add_menu_action(self.view_menu, self.reset_font_size, True) | ||
MinRK
|
r5136 | |||
self.view_menu.addSeparator() | ||||
self.clear_action = QtGui.QAction("&Clear Screen", | ||||
self, | ||||
shortcut='Ctrl+L', | ||||
statusTip="Clear the console", | ||||
triggered=self.clear_magic_active_frontend) | ||||
self.add_menu_action(self.view_menu, self.clear_action) | ||||
def init_kernel_menu(self): | ||||
self.kernel_menu = self.menuBar().addMenu("&Kernel") | ||||
# Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl | ||||
# keep the signal shortcuts to ctrl, rather than | ||||
# platform-default like we do elsewhere. | ||||
ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl" | ||||
Paul Ivanov
|
r7592 | self.interrupt_kernel_action = QtGui.QAction("&Interrupt current Kernel", | ||
MinRK
|
r5136 | self, | ||
triggered=self.interrupt_kernel_active_frontend, | ||||
shortcut=ctrl+"+C", | ||||
) | ||||
self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action) | ||||
Paul Ivanov
|
r7592 | self.restart_kernel_action = QtGui.QAction("&Restart current Kernel", | ||
MinRK
|
r5136 | self, | ||
triggered=self.restart_kernel_active_frontend, | ||||
shortcut=ctrl+"+.", | ||||
) | ||||
self.add_menu_action(self.kernel_menu, self.restart_kernel_action) | ||||
self.kernel_menu.addSeparator() | ||||
Paul Ivanov
|
r7592 | self.confirm_restart_kernel_action = QtGui.QAction("&Confirm kernel restart", | ||
Paul Ivanov
|
r7591 | self, | ||
checkable=True, | ||||
checked=self.active_frontend.confirm_restart, | ||||
triggered=self.toggle_confirm_restart_active_frontend | ||||
) | ||||
self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action) | ||||
Paul Ivanov
|
r7596 | self.tab_widget.currentChanged.connect(self.update_restart_checkbox) | ||
Paul Ivanov
|
r7591 | |||
Matthias BUSSONNIER
|
r5395 | def _make_dynamic_magic(self,magic): | ||
"""Return a function `fun` that will execute `magic` on active frontend. | ||||
Matthias BUSSONNIER
|
r5396 | Parameters | ||
---------- | ||||
magic : string | ||||
string that will be executed as is when the returned function is called | ||||
Returns | ||||
------- | ||||
fun : function | ||||
function with no parameters, when called will execute `magic` on the | ||||
current active frontend at call time | ||||
Matthias BUSSONNIER
|
r5395 | |||
Matthias BUSSONNIER
|
r5396 | See Also | ||
-------- | ||||
populate_all_magic_menu : generate the "All Magics..." menu | ||||
Matthias BUSSONNIER
|
r5395 | |||
Matthias BUSSONNIER
|
r5396 | Notes | ||
----- | ||||
Paul Ivanov
|
r7589 | `fun` executes `magic` in active frontend at the moment it is triggered, | ||
not the active frontend at the moment it was created. | ||||
Matthias BUSSONNIER
|
r5395 | |||
This function is mostly used to create the "All Magics..." Menu at run time. | ||||
""" | ||||
Paul Ivanov
|
r7589 | # need two level nested function to be sure to pass magic | ||
# to active frontend **at run time**. | ||||
Matthias BUSSONNIER
|
r5395 | def inner_dynamic_magic(): | ||
self.active_frontend.execute(magic) | ||||
inner_dynamic_magic.__name__ = "dynamics_magic_s" | ||||
return inner_dynamic_magic | ||||
def populate_all_magic_menu(self, listofmagic=None): | ||||
"""Clean "All Magics..." menu and repopulate it with `listofmagic` | ||||
Matthias BUSSONNIER
|
r5396 | Parameters | ||
---------- | ||||
listofmagic : string, | ||||
repr() of a list of strings, send back by the kernel | ||||
Matthias BUSSONNIER
|
r5395 | |||
Matthias BUSSONNIER
|
r5396 | Notes | ||
----- | ||||
Matthias BUSSONNIER
|
r5395 | `listofmagic`is a repr() of list because it is fed with the result of | ||
a 'user_expression' | ||||
""" | ||||
Matthias BUSSONNIER
|
r7270 | for k,v in self._magic_menu_dict.items(): | ||
v.clear() | ||||
self.all_magic_menu.clear() | ||||
mlist=ast.literal_eval(listofmagic) | ||||
for magic in mlist: | ||||
cell = (magic['type'] == 'cell') | ||||
name = magic['name'] | ||||
mclass = magic['class'] | ||||
if cell : | ||||
prefix='%%' | ||||
else : | ||||
prefix='%' | ||||
magic_menu = self._get_magic_menu(mclass) | ||||
Bradley M. Froehle
|
r8279 | pmagic = '%s%s'%(prefix,name) | ||
Matthias BUSSONNIER
|
r7270 | |||
Matthias BUSSONNIER
|
r5395 | xaction = QtGui.QAction(pmagic, | ||
self, | ||||
triggered=self._make_dynamic_magic(pmagic) | ||||
) | ||||
Matthias BUSSONNIER
|
r7270 | magic_menu.addAction(xaction) | ||
self.all_magic_menu.addAction(xaction) | ||||
Matthias BUSSONNIER
|
r5395 | |||
Matthias BUSSONNIER
|
r5392 | def update_all_magic_menu(self): | ||
Paul Ivanov
|
r7589 | """ Update the list of magics in the "All Magics..." Menu | ||
Matthias BUSSONNIER
|
r5396 | |||
Paul Ivanov
|
r7589 | Request the kernel with the list of available magics and populate the | ||
Matthias BUSSONNIER
|
r5396 | menu with the list received back | ||
""" | ||||
Matthias BUSSONNIER
|
r7270 | self.active_frontend._silent_exec_callback('get_ipython().magics_manager.lsmagic_info()', | ||
self.populate_all_magic_menu) | ||||
def _get_magic_menu(self,menuidentifier, menulabel=None): | ||||
"""return a submagic menu by name, and create it if needed | ||||
parameters: | ||||
----------- | ||||
Matthias BUSSONNIER
|
r5392 | |||
Matthias BUSSONNIER
|
r7270 | menulabel : str | ||
Label for the menu | ||||
Will infere the menu name from the identifier at creation if menulabel not given. | ||||
To do so you have too give menuidentifier as a CamelCassedString | ||||
""" | ||||
menu = self._magic_menu_dict.get(menuidentifier,None) | ||||
if not menu : | ||||
if not menulabel: | ||||
menulabel = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>",menuidentifier) | ||||
menu = QtGui.QMenu(menulabel,self.magic_menu) | ||||
self._magic_menu_dict[menuidentifier]=menu | ||||
self.magic_menu.insertMenu(self.magic_menu_separator,menu) | ||||
return menu | ||||
MinRK
|
r5136 | def init_magic_menu(self): | ||
self.magic_menu = self.menuBar().addMenu("&Magic") | ||||
Matthias BUSSONNIER
|
r7270 | self.magic_menu_separator = self.magic_menu.addSeparator() | ||
self.all_magic_menu = self._get_magic_menu("AllMagics", menulabel="&All Magics...") | ||||
Matthias BUSSONNIER
|
r5393 | |||
Bussonnier Matthias
|
r5482 | # This action should usually not appear as it will be cleared when menu | ||
# is updated at first kernel response. Though, it is necessary when | ||||
# connecting through X-forwarding, as in this case, the menu is not | ||||
# auto updated, SO DO NOT DELETE. | ||||
Matthias BUSSONNIER
|
r5505 | self.pop = QtGui.QAction("&Update All Magic Menu ", | ||
self, triggered=self.update_all_magic_menu) | ||||
self.add_menu_action(self.all_magic_menu, self.pop) | ||||
Matthias BUSSONNIER
|
r5506 | # we need to populate the 'Magic Menu' once the kernel has answer at | ||
Paul Ivanov
|
r7589 | # least once let's do it immediately, but it's assured to works | ||
Matthias BUSSONNIER
|
r5506 | self.pop.trigger() | ||
Matthias BUSSONNIER
|
r5392 | |||
MinRK
|
r5136 | self.reset_action = QtGui.QAction("&Reset", | ||
self, | ||||
Paul Ivanov
|
r7589 | statusTip="Clear all variables from workspace", | ||
MinRK
|
r5136 | triggered=self.reset_magic_active_frontend) | ||
self.add_menu_action(self.magic_menu, self.reset_action) | ||||
self.history_action = QtGui.QAction("&History", | ||||
self, | ||||
statusTip="show command history", | ||||
triggered=self.history_magic_active_frontend) | ||||
self.add_menu_action(self.magic_menu, self.history_action) | ||||
self.save_action = QtGui.QAction("E&xport History ", | ||||
self, | ||||
statusTip="Export History as Python File", | ||||
triggered=self.save_magic_active_frontend) | ||||
self.add_menu_action(self.magic_menu, self.save_action) | ||||
self.who_action = QtGui.QAction("&Who", | ||||
self, | ||||
Paul Ivanov
|
r7589 | statusTip="List interactive variables", | ||
MinRK
|
r5136 | triggered=self.who_magic_active_frontend) | ||
self.add_menu_action(self.magic_menu, self.who_action) | ||||
self.who_ls_action = QtGui.QAction("Wh&o ls", | ||||
self, | ||||
Paul Ivanov
|
r7589 | statusTip="Return a list of interactive variables", | ||
MinRK
|
r5136 | triggered=self.who_ls_magic_active_frontend) | ||
self.add_menu_action(self.magic_menu, self.who_ls_action) | ||||
self.whos_action = QtGui.QAction("Who&s", | ||||
self, | ||||
Paul Ivanov
|
r7589 | statusTip="List interactive variables with details", | ||
MinRK
|
r5136 | triggered=self.whos_magic_active_frontend) | ||
self.add_menu_action(self.magic_menu, self.whos_action) | ||||
Matthias BUSSONNIER
|
r5396 | |||
MinRK
|
r5136 | def init_window_menu(self): | ||
self.window_menu = self.menuBar().addMenu("&Window") | ||||
if sys.platform == 'darwin': | ||||
# add min/maximize actions to OSX, which lacks default bindings. | ||||
self.minimizeAct = QtGui.QAction("Mini&mize", | ||||
self, | ||||
shortcut="Ctrl+m", | ||||
statusTip="Minimize the window/Restore Normal Size", | ||||
triggered=self.toggleMinimized) | ||||
# maximize is called 'Zoom' on OSX for some reason | ||||
self.maximizeAct = QtGui.QAction("&Zoom", | ||||
self, | ||||
shortcut="Ctrl+Shift+M", | ||||
statusTip="Maximize the window/Restore Normal Size", | ||||
triggered=self.toggleMaximized) | ||||
self.add_menu_action(self.window_menu, self.minimizeAct) | ||||
self.add_menu_action(self.window_menu, self.maximizeAct) | ||||
self.window_menu.addSeparator() | ||||
MinRK
|
r5137 | prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp" | ||
MinRK
|
r5136 | self.prev_tab_act = QtGui.QAction("Pre&vious Tab", | ||
self, | ||||
shortcut=prev_key, | ||||
statusTip="Select previous tab", | ||||
triggered=self.prev_tab) | ||||
self.add_menu_action(self.window_menu, self.prev_tab_act) | ||||
MinRK
|
r5137 | next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown" | ||
MinRK
|
r5136 | self.next_tab_act = QtGui.QAction("Ne&xt Tab", | ||
self, | ||||
shortcut=next_key, | ||||
statusTip="Select next tab", | ||||
triggered=self.next_tab) | ||||
self.add_menu_action(self.window_menu, self.next_tab_act) | ||||
def init_help_menu(self): | ||||
# please keep the Help menu in Mac Os even if empty. It will | ||||
# automatically contain a search field to search inside menus and | ||||
# please keep it spelled in English, as long as Qt Doesn't support | ||||
Paul Ivanov
|
r7589 | # a QAction.MenuRole like HelpMenuRole otherwise it will lose | ||
# this search field functionality | ||||
MinRK
|
r5136 | |||
self.help_menu = self.menuBar().addMenu("&Help") | ||||
# Help Menu | ||||
self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython", | ||||
self, | ||||
triggered=self.intro_active_frontend | ||||
) | ||||
self.add_menu_action(self.help_menu, self.intro_active_frontend_action) | ||||
self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet", | ||||
self, | ||||
triggered=self.quickref_active_frontend | ||||
) | ||||
self.add_menu_action(self.help_menu, self.quickref_active_frontend_action) | ||||
self.guiref_active_frontend_action = QtGui.QAction("&Qt Console", | ||||
self, | ||||
triggered=self.guiref_active_frontend | ||||
) | ||||
self.add_menu_action(self.help_menu, self.guiref_active_frontend_action) | ||||
self.onlineHelpAct = QtGui.QAction("Open Online &Help", | ||||
self, | ||||
triggered=self._open_online_help) | ||||
self.add_menu_action(self.help_menu, self.onlineHelpAct) | ||||
# minimize/maximize/fullscreen actions: | ||||
def toggle_menu_bar(self): | ||||
MinRK
|
r5138 | menu_bar = self.menuBar() | ||
MinRK
|
r5136 | if menu_bar.isVisible(): | ||
menu_bar.setVisible(False) | ||||
else: | ||||
menu_bar.setVisible(True) | ||||
def toggleMinimized(self): | ||||
if not self.isMinimized(): | ||||
self.showMinimized() | ||||
else: | ||||
self.showNormal() | ||||
def _open_online_help(self): | ||||
filename="http://ipython.org/ipython-doc/stable/index.html" | ||||
webbrowser.open(filename, new=1, autoraise=True) | ||||
def toggleMaximized(self): | ||||
if not self.isMaximized(): | ||||
self.showMaximized() | ||||
else: | ||||
self.showNormal() | ||||
# Min/Max imizing while in full screen give a bug | ||||
# when going out of full screen, at least on OSX | ||||
def toggleFullScreen(self): | ||||
if not self.isFullScreen(): | ||||
self.showFullScreen() | ||||
if sys.platform == 'darwin': | ||||
self.maximizeAct.setEnabled(False) | ||||
self.minimizeAct.setEnabled(False) | ||||
else: | ||||
self.showNormal() | ||||
if sys.platform == 'darwin': | ||||
self.maximizeAct.setEnabled(True) | ||||
self.minimizeAct.setEnabled(True) | ||||
def close_active_frontend(self): | ||||
self.close_tab(self.active_frontend) | ||||
def restart_kernel_active_frontend(self): | ||||
self.active_frontend.request_restart_kernel() | ||||
def interrupt_kernel_active_frontend(self): | ||||
self.active_frontend.request_interrupt_kernel() | ||||
Paul Ivanov
|
r7591 | def toggle_confirm_restart_active_frontend(self): | ||
widget = self.active_frontend | ||||
widget.confirm_restart = not widget.confirm_restart | ||||
self.confirm_restart_kernel_action.setChecked(widget.confirm_restart) | ||||
Paul Ivanov
|
r7593 | def update_restart_checkbox(self): | ||
MinRK
|
r7625 | if self.active_frontend is None: | ||
return | ||||
Paul Ivanov
|
r7593 | widget = self.active_frontend | ||
self.confirm_restart_kernel_action.setChecked(widget.confirm_restart) | ||||
Paul Ivanov
|
r7591 | |||
MinRK
|
r5136 | def cut_active_frontend(self): | ||
MinRK
|
r5140 | widget = self.active_frontend | ||
if widget.can_cut(): | ||||
widget.cut() | ||||
MinRK
|
r5136 | |||
def copy_active_frontend(self): | ||||
MinRK
|
r5140 | widget = self.active_frontend | ||
Matthias BUSSONNIER
|
r5561 | widget.copy() | ||
MinRK
|
r5136 | |||
def copy_raw_active_frontend(self): | ||||
self.active_frontend._copy_raw_action.trigger() | ||||
def paste_active_frontend(self): | ||||
MinRK
|
r5140 | widget = self.active_frontend | ||
if widget.can_paste(): | ||||
widget.paste() | ||||
MinRK
|
r5136 | |||
def undo_active_frontend(self): | ||||
self.active_frontend.undo() | ||||
def redo_active_frontend(self): | ||||
self.active_frontend.redo() | ||||
def reset_magic_active_frontend(self): | ||||
self.active_frontend.execute("%reset") | ||||
def history_magic_active_frontend(self): | ||||
self.active_frontend.execute("%history") | ||||
def save_magic_active_frontend(self): | ||||
self.active_frontend.save_magic() | ||||
def clear_magic_active_frontend(self): | ||||
self.active_frontend.execute("%clear") | ||||
def who_magic_active_frontend(self): | ||||
self.active_frontend.execute("%who") | ||||
def who_ls_magic_active_frontend(self): | ||||
self.active_frontend.execute("%who_ls") | ||||
def whos_magic_active_frontend(self): | ||||
self.active_frontend.execute("%whos") | ||||
def print_action_active_frontend(self): | ||||
self.active_frontend.print_action.trigger() | ||||
def export_action_active_frontend(self): | ||||
self.active_frontend.export_action.trigger() | ||||
def select_all_active_frontend(self): | ||||
self.active_frontend.select_all_action.trigger() | ||||
def increase_font_size_active_frontend(self): | ||||
self.active_frontend.increase_font_size.trigger() | ||||
def decrease_font_size_active_frontend(self): | ||||
self.active_frontend.decrease_font_size.trigger() | ||||
def reset_font_size_active_frontend(self): | ||||
self.active_frontend.reset_font_size.trigger() | ||||
def guiref_active_frontend(self): | ||||
self.active_frontend.execute("%guiref") | ||||
def intro_active_frontend(self): | ||||
self.active_frontend.execute("?") | ||||
def quickref_active_frontend(self): | ||||
self.active_frontend.execute("%quickref") | ||||
#--------------------------------------------------------------------------- | ||||
# QWidget interface | ||||
#--------------------------------------------------------------------------- | ||||
def closeEvent(self, event): | ||||
""" Forward the close event to every tabs contained by the windows | ||||
""" | ||||
MinRK
|
r5138 | if self.tab_widget.count() == 0: | ||
# no tabs, just close | ||||
event.accept() | ||||
return | ||||
MinRK
|
r5136 | # Do Not loop on the widget count as it change while closing | ||
MinRK
|
r5138 | title = self.window().windowTitle() | ||
cancel = QtGui.QMessageBox.Cancel | ||||
okay = QtGui.QMessageBox.Ok | ||||
if self.confirm_exit: | ||||
MinRK
|
r5264 | if self.tab_widget.count() > 1: | ||
msg = "Close all tabs, stop all kernels, and Quit?" | ||||
else: | ||||
msg = "Close console, stop kernel, and Quit?" | ||||
info = "Kernels not started here (e.g. notebooks) will be left alone." | ||||
MinRK
|
r5716 | closeall = QtGui.QPushButton("&Quit", self) | ||
closeall.setShortcut('Q') | ||||
MinRK
|
r5138 | box = QtGui.QMessageBox(QtGui.QMessageBox.Question, | ||
title, msg) | ||||
MinRK
|
r5264 | box.setInformativeText(info) | ||
MinRK
|
r5138 | box.addButton(cancel) | ||
box.addButton(closeall, QtGui.QMessageBox.YesRole) | ||||
box.setDefaultButton(closeall) | ||||
box.setEscapeButton(cancel) | ||||
pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64))) | ||||
box.setIconPixmap(pixmap) | ||||
reply = box.exec_() | ||||
else: | ||||
reply = okay | ||||
if reply == cancel: | ||||
Jens H. Nielsen
|
r5202 | event.ignore() | ||
MinRK
|
r5138 | return | ||
if reply == okay: | ||||
while self.tab_widget.count() >= 1: | ||||
# prevent further confirmations: | ||||
widget = self.active_frontend | ||||
widget._confirm_exit = False | ||||
self.close_tab(widget) | ||||
Jens H. Nielsen
|
r5202 | event.accept() | ||
MinRK
|
r5136 | |||