class NotebookPlugin(SpyderPluginWidget): """IPython Notebook plugin.""" CONF_SECTION = 'notebook' focus_changed = Signal() def __init__(self, parent, testing=False): """Constructor.""" SpyderPluginWidget.__init__(self, parent) self.testing = testing self.fileswitcher_dlg = None self.tabwidget = None self.menu_actions = None self.main = parent self.clients = [] self.untitled_num = 0 self.recent_notebooks = self.get_option('recent_notebooks', default=[]) self.recent_notebook_menu = QMenu(_("Open recent"), self) self.options_menu = QMenu(self) # Initialize plugin self.initialize_plugin() layout = QVBoxLayout() new_notebook_btn = create_toolbutton(self, icon=ima.icon('project_expanded'), tip=_('Open a new notebook'), triggered=self.create_new_client) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), tip=_('Options')) menu_btn.setMenu(self.options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) add_actions(self.options_menu, self.menu_actions) corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} self.tabwidget = Tabs(self, menu=self.options_menu, actions=self.menu_actions, corner_widgets=corner_widgets) if hasattr(self.tabwidget, 'setDocumentMode') \ and not sys.platform == 'darwin': # Don't set document mode to true on OSX because it generates # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_client) layout.addWidget(self.tabwidget) self.setLayout(layout) # ------ SpyderPluginMixin API -------------------------------------------- def on_first_registration(self): """Action to be performed on first plugin registration.""" self.main.tabify_plugins(self.main.editor, self) def update_font(self): """Update font from Preferences.""" # For now we're passing. We need to create an nbextension for # this. pass # ------ SpyderPluginWidget API ------------------------------------------- def get_plugin_title(self): """Return widget title.""" title = _('Notebook') return title def get_plugin_icon(self): """Return widget icon.""" return ima.icon('ipython_console') def get_focus_widget(self): """Return the widget to give focus to.""" client = self.tabwidget.currentWidget() if client is not None: return client.notebookwidget def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed.""" for cl in self.clients: cl.close() self.set_option('recent_notebooks', self.recent_notebooks) return True def refresh_plugin(self): """Refresh tabwidget.""" nb = None if self.tabwidget.count(): client = self.tabwidget.currentWidget() nb = client.notebookwidget nb.setFocus() else: nb = None self.update_notebook_actions() def get_plugin_actions(self): """Return a list of actions related to plugin.""" create_nb_action = create_action(self, _("New notebook"), icon=ima.icon('filenew'), triggered=self.create_new_client) self.save_as_action = create_action(self, _("Save as..."), icon=ima.icon('filesaveas'), triggered=self.save_as) open_action = create_action(self, _("Open..."), icon=ima.icon('fileopen'), triggered=self.open_notebook) self.open_console_action = create_action( self, _("Open console"), icon=ima.icon('ipython_console'), triggered=self.open_console) self.clear_recent_notebooks_action =\ create_action(self, _("Clear this list"), triggered=self.clear_recent_notebooks) # Plugin actions self.menu_actions = [ create_nb_action, open_action, self.recent_notebook_menu, MENU_SEPARATOR, self.save_as_action, MENU_SEPARATOR, self.open_console_action ] self.setup_menu_actions() return self.menu_actions def register_plugin(self): """Register plugin in Spyder's main window.""" self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) self.ipyconsole = self.main.ipyconsole self.create_new_client(give_focus=False) icon_path = os.path.join(PACKAGE_PATH, 'images', 'icon.svg') self.main.add_to_fileswitcher(self, self.tabwidget, self.clients, QIcon(icon_path)) self.recent_notebook_menu.aboutToShow.connect(self.setup_menu_actions) def check_compatibility(self): """Check compatibility for PyQt and sWebEngine.""" message = '' value = True if PYQT4 or PYSIDE: message = _("You are working with Qt4 and in order to use this " "plugin you need to have Qt5.<br><br>" "Please update your Qt and/or PyQt packages to " "meet this requirement.") value = False return value, message # ------ Public API (for clients) ----------------------------------------- def setup_menu_actions(self): """Setup and update the menu actions.""" self.recent_notebook_menu.clear() self.recent_notebooks_actions = [] if self.recent_notebooks: for notebook in self.recent_notebooks: name = notebook action = \ create_action(self, name, icon=ima.icon('filenew'), triggered=lambda v, path=notebook: self.create_new_client(filename=path)) self.recent_notebooks_actions.append(action) self.recent_notebooks_actions += \ [None, self.clear_recent_notebooks_action] else: self.recent_notebooks_actions = \ [self.clear_recent_notebooks_action] add_actions(self.recent_notebook_menu, self.recent_notebooks_actions) self.update_notebook_actions() def update_notebook_actions(self): """Update actions of the recent notebooks menu.""" if self.recent_notebooks: self.clear_recent_notebooks_action.setEnabled(True) else: self.clear_recent_notebooks_action.setEnabled(False) client = self.get_current_client() if client: if client.get_filename() != WELCOME: self.save_as_action.setEnabled(True) self.open_console_action.setEnabled(True) self.options_menu.clear() add_actions(self.options_menu, self.menu_actions) return self.save_as_action.setEnabled(False) self.open_console_action.setEnabled(False) self.options_menu.clear() add_actions(self.options_menu, self.menu_actions) def add_to_recent(self, notebook): """ Add an entry to recent notebooks. We only maintain the list of the 20 most recent notebooks. """ if notebook not in self.recent_notebooks: self.recent_notebooks.insert(0, notebook) self.recent_notebooks = self.recent_notebooks[:20] def clear_recent_notebooks(self): """Clear the list of recent notebooks.""" self.recent_notebooks = [] self.setup_menu_actions() def get_clients(self): """Return notebooks list.""" return [cl for cl in self.clients if isinstance(cl, NotebookClient)] def get_focus_client(self): """Return current notebook with focus, if any.""" widget = QApplication.focusWidget() for client in self.get_clients(): if widget is client or widget is client.notebookwidget: return client def get_current_client(self): """Return the currently selected notebook.""" try: client = self.tabwidget.currentWidget() except AttributeError: client = None if client is not None: return client def get_current_nbwidget(self): """Return the notebookwidget of the current client.""" client = self.get_current_client() if client is not None: return client.notebookwidget def get_current_client_name(self, short=False): """Get the current client name.""" client = self.get_current_client() if client: if short: return client.get_short_name() else: return client.get_filename() def create_new_client(self, filename=None, give_focus=True): """Create a new notebook or load a pre-existing one.""" # Generate the notebook name (in case of a new one) if not filename: if not osp.isdir(NOTEBOOK_TMPDIR): os.makedirs(NOTEBOOK_TMPDIR) nb_name = 'untitled' + str(self.untitled_num) + '.ipynb' filename = osp.join(NOTEBOOK_TMPDIR, nb_name) nb_contents = nbformat.v4.new_notebook() nbformat.write(nb_contents, filename) self.untitled_num += 1 # Save spyder_pythonpath before creating a client # because it's needed by our kernel spec. if not self.testing: CONF.set('main', 'spyder_pythonpath', self.main.get_spyder_pythonpath()) # Open the notebook with nbopen and get the url we need to render try: server_info = nbopen(filename) except (subprocess.CalledProcessError, NBServerError): QMessageBox.critical( self, _("Server error"), _("The Jupyter Notebook server failed to start or it is " "taking too much time to do it. Please start it in a " "system terminal with the command 'jupyter notebook' to " "check for errors.")) # Create a welcome widget # See issue 93 self.untitled_num -= 1 self.create_welcome_client() return welcome_client = self.create_welcome_client() client = NotebookClient(self, filename) self.add_tab(client) if NOTEBOOK_TMPDIR not in filename: self.add_to_recent(filename) self.setup_menu_actions() client.register(server_info) client.load_notebook() if welcome_client and not self.testing: self.tabwidget.setCurrentIndex(0) def close_client(self, index=None, client=None, save=False): """Close client tab from index or widget (or close current tab).""" if not self.tabwidget.count(): return if client is not None: index = self.tabwidget.indexOf(client) if index is None and client is None: index = self.tabwidget.currentIndex() if index is not None: client = self.tabwidget.widget(index) is_welcome = client.get_filename() == WELCOME if not save and not is_welcome: client.save() wait_save = QEventLoop() QTimer.singleShot(1000, wait_save.quit) wait_save.exec_() path = client.get_filename() fname = osp.basename(path) nb_contents = nbformat.read(path, as_version=4) if ('untitled' in fname and len(nb_contents['cells']) > 0 and len(nb_contents['cells'][0]['source']) > 0): buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.question( self, self.get_plugin_title(), _("<b>{0}</b> has been modified." "<br>Do you want to " "save changes?".format(fname)), buttons) if answer == QMessageBox.Yes: self.save_as(close=True) if not is_welcome: client.shutdown_kernel() client.close() # Delete notebook file if it is in temporary directory filename = client.get_filename() if filename.startswith(get_temp_dir()): try: os.remove(filename) except EnvironmentError: pass # Note: notebook index may have changed after closing related widgets self.tabwidget.removeTab(self.tabwidget.indexOf(client)) self.clients.remove(client) self.create_welcome_client() def create_welcome_client(self): """Create a welcome client with some instructions.""" if self.tabwidget.count() == 0: welcome = open(WELCOME).read() client = NotebookClient(self, WELCOME, ini_message=welcome) self.add_tab(client) return client def save_as(self, name=None, close=False): """Save notebook as.""" current_client = self.get_current_client() current_client.save() original_path = current_client.get_filename() if not name: original_name = osp.basename(original_path) else: original_name = name filename, _selfilter = getsavefilename(self, _("Save notebook"), original_name, FILES_FILTER) if filename: nb_contents = nbformat.read(original_path, as_version=4) nbformat.write(nb_contents, filename) if not close: self.close_client(save=True) self.create_new_client(filename=filename) def open_notebook(self, filenames=None): """Open a notebook from file.""" if not filenames: filenames, _selfilter = getopenfilenames(self, _("Open notebook"), '', FILES_FILTER) if filenames: for filename in filenames: self.create_new_client(filename=filename) def open_console(self, client=None): """Open an IPython console for the given client or the current one.""" if not client: client = self.get_current_client() if self.ipyconsole is not None: kernel_id = client.get_kernel_id() if not kernel_id: QMessageBox.critical( self, _('Error opening console'), _('There is no kernel associated to this notebook.')) return self.ipyconsole._create_client_for_kernel(kernel_id, None, None, None) ipyclient = self.ipyconsole.get_current_client() ipyclient.allow_rename = False self.ipyconsole.rename_client_tab(ipyclient, client.get_short_name()) # ------ Public API (for tabs) -------------------------------------------- def add_tab(self, widget): """Add tab.""" self.clients.append(widget) index = self.tabwidget.addTab(widget, widget.get_short_name()) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, widget.get_filename()) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() self.activateWindow() widget.notebookwidget.setFocus() def move_tab(self, index_from, index_to): """Move tab.""" client = self.clients.pop(index_from) self.clients.insert(index_to, client) # ------ Public API (for FileSwitcher) ------------------------------------ def set_stack_index(self, index, instance): """Set the index of the current notebook.""" if instance == self: self.tabwidget.setCurrentIndex(index) def get_current_tab_manager(self): """Get the widget with the TabWidget attribute.""" return self
class TerminalMainWidget(PluginMainWidget): """ Terminal plugin main widget. """ MAX_SERVER_CONTACT_RETRIES = 40 URL_ISSUES = ' https://github.com/spyder-ide/spyder-terminal/issues' # --- Signals # ------------------------------------------------------------------------ sig_server_is_ready = Signal() """ This signal is emitted when the server is ready to connect. """ def __init__(self, name, plugin, parent): """Widget constructor.""" self.terms = [] super().__init__(name, plugin, parent) # Attributes self.tab_widget = None self.menu_actions = None self.server_retries = 0 self.server_ready = False self.font = None self.port = select_port(default_port=8071) self.stdout_file = None self.stderr_file = None if get_debug_level() > 0: self.stdout_file = osp.join(os.getcwd(), 'spyder_terminal_out.log') self.stderr_file = osp.join(os.getcwd(), 'spyder_terminal_err.log') self.project_path = None self.current_file_path = None self.current_cwd = os.getcwd() # Widgets self.main = parent self.find_widget = FindTerminal(self) self.find_widget.hide() layout = QVBoxLayout() # Tab Widget self.tabwidget = Tabs(self, rename_tabs=True) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_term) if (hasattr(self.tabwidget, 'setDocumentMode') and not sys.platform == 'darwin'): # Don't set document mode to true on OSX because it generates # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) layout.addWidget(self.tabwidget) layout.addWidget(self.find_widget) self.setLayout(layout) css = qstylizer.style.StyleSheet() css.QTabWidget.pane.setValues(border=0) self.setStyleSheet(css.toString()) self.__wait_server_to_start() # ---- PluginMainWidget API # ------------------------------------------------------------------------ def get_focus_widget(self): """ Set focus on current selected terminal. Return the widget to give focus to when this plugin's dockwidget is raised on top-level. """ term = self.tabwidget.currentWidget() if term is not None: return term.view def get_title(self): """Define the title of the widget.""" return _('Terminal') def setup(self): """Perform the setup of plugin's main menu and signals.""" self.cmd = find_program(self.get_conf('shell')) server_args = [ sys.executable, '-m', 'spyder_terminal.server', '--port', str(self.port), '--shell', self.cmd] self.server = QProcess(self) env = self.server.processEnvironment() for var in os.environ: env.insert(var, os.environ[var]) self.server.setProcessEnvironment(env) self.server.errorOccurred.connect(self.handle_process_errors) self.server.setProcessChannelMode(QProcess.SeparateChannels) if self.stdout_file and self.stderr_file: self.server.setStandardOutputFile(self.stdout_file) self.server.setStandardErrorFile(self.stderr_file) self.server.start(server_args[0], server_args[1:]) self.color_scheme = self.get_conf('appearance', 'ui_theme') self.theme = self.get_conf('appearance', 'selected') # Menu menu = self.get_options_menu() # Actions new_terminal_toolbar_action = self.create_toolbutton( TerminalMainWidgetToolbarSections.New, text=_("Open a new terminal"), icon=self.create_icon('expand_selection'), triggered=lambda: self.create_new_term(), ) self.add_corner_widget( TerminalMainWidgetCornerToolbar.NewTerm, new_terminal_toolbar_action) new_terminal_cwd = self.create_action( TerminalMainWidgetActions.NewTerminalForCWD, text=_("New terminal in current working directory"), tip=_("Sets the pwd at the current working directory"), triggered=lambda: self.create_new_term(), shortcut_context='terminal', register_shortcut=True) self.new_terminal_project = self.create_action( TerminalMainWidgetActions.NewTerminalForProject, text=_("New terminal in current project"), tip=_("Sets the pwd at the current project directory"), triggered=lambda: self.create_new_term(path=self.project_path)) new_terminal_file = self.create_action( TerminalMainWidgetActions.NewTerminalForFile, text=_("New terminal in current Editor file"), tip=_("Sets the pwd at the directory that contains the current " "opened file"), triggered=lambda: self.create_new_term( path=self.current_file_path)) rename_tab_action = self.create_action( TerminalMainWidgetActions.RenameTab, text=_("Rename terminal"), triggered=lambda: self.tab_name_editor()) # Context menu actions self.create_action( TerminalMainWidgetActions.Copy, text=_('Copy text'), icon=self.create_icon('editcopy'), shortcut_context='terminal', triggered=lambda: self.copy(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.Paste, text=_('Paste text'), icon=self.create_icon('editpaste'), shortcut_context='terminal', triggered=lambda: self.paste(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.Clear, text=_('Clear terminal'), shortcut_context='terminal', triggered=lambda: self.clear(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.ZoomIn, text=_('Zoom in'), shortcut_context='terminal', triggered=lambda: self.increase_font(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.ZoomOut, text=_('Zoom out'), shortcut_context='terminal', triggered=lambda: self.decrease_font(), register_shortcut=True) # Create context menu self.create_menu(TermViewMenus.Context) # Add actions to options menu for item in [new_terminal_cwd, self.new_terminal_project, new_terminal_file]: self.add_item_to_menu( item, menu=menu, section=TerminalMainWidgetMenuSections.New) self.add_item_to_menu( rename_tab_action, menu=menu, section=TerminalMainWidgetMenuSections.TabActions) def update_actions(self): """Setup and update the actions in the options menu.""" if self.project_path is None: self.new_terminal_project.setEnabled(False) # ------ Private API ------------------------------------------ def copy(self): if self.get_focus_widget(): self.get_focus_widget().copy() def paste(self): if self.get_focus_widget(): self.get_focus_widget().paste() def clear(self): if self.get_focus_widget(): self.get_focus_widget().clear() def increase_font(self): if self.get_focus_widget(): self.get_focus_widget().increase_font() def decrease_font(self): if self.get_focus_widget(): self.get_focus_widget().decrease_font() def __wait_server_to_start(self): try: code = requests.get('http://127.0.0.1:{0}'.format( self.port)).status_code except: code = 500 if self.server_retries == self.MAX_SERVER_CONTACT_RETRIES: QMessageBox.critical(self, _('Spyder Terminal Error'), _("Terminal server could not be located at " '<a href="http://127.0.0.1:{0}">' 'http://127.0.0.1:{0}</a>,' ' please restart Spyder on debugging mode ' "and open an issue with the contents of " "<tt>{1}</tt> and <tt>{2}</tt> " "files at {3}.").format(self.port, self.stdout_file, self.stderr_file, self.URL_ISSUES), QMessageBox.Ok) elif code != 200: self.server_retries += 1 QTimer.singleShot(250, self.__wait_server_to_start) elif code == 200: self.sig_server_is_ready.emit() self.server_ready = True self.create_new_term(give_focus=False) # ------ Plugin API -------------------------------- def update_font(self, font): """Update font from Preferences.""" self.font = font for term in self.terms: term.set_font(font.family()) def on_close(self, cancelable=False): """Perform actions before parent main window is closed.""" for term in self.terms: term.close() self.server.kill() return True def refresh_plugin(self): """Refresh tabwidget.""" term = None if self.tabwidget.count(): term = self.tabwidget.currentWidget() term.view.setFocus() else: term = None @on_conf_change def apply_plugin_settings(self, options): """Apply the config settings.""" term_options = {} for option in options: if option == 'color_scheme_name': term_options[option] = option else: term_options[option] = self.get_conf(option) for term in self.get_terms(): term.apply_settings(term_options) # ------ Public API (for terminals) ------------------------- def get_terms(self): """Return terminal list.""" return [cl for cl in self.terms if isinstance(cl, TerminalWidget)] def get_current_term(self): """Return the currently selected terminal.""" try: terminal = self.tabwidget.currentWidget() except AttributeError: terminal = None if terminal is not None: return terminal def create_new_term(self, name=None, give_focus=True, path=None): """Add a new terminal tab.""" if path is None: path = self.current_cwd if self.project_path is not None: path = self.project_path path = path.replace('\\', '/') term = TerminalWidget( self, self.port, path=path, font=self.font.family(), theme=self.theme, color_scheme=self.color_scheme) self.add_tab(term) term.terminal_closed.connect(lambda: self.close_term(term=term)) def close_term(self, index=None, term=None): """Close a terminal tab.""" if not self.tabwidget.count(): return if term is not None: index = self.tabwidget.indexOf(term) if index is None and term is None: index = self.tabwidget.currentIndex() if index is not None: term = self.tabwidget.widget(index) if term: term.close() self.tabwidget.removeTab(self.tabwidget.indexOf(term)) if term in self.terms: self.terms.remove(term) if self.tabwidget.count() == 0: self.create_new_term() def set_project_path(self, path): """Refresh current project path.""" self.project_path = path self.new_terminal_project.setEnabled(True) def set_current_opened_file(self, path): """Get path of current opened file in editor.""" self.current_file_path = osp.dirname(path) def unset_project_path(self): """Refresh current project path.""" self.project_path = None self.new_terminal_project.setEnabled(False) @Slot(str) def set_current_cwd(self, cwd): """Update current working directory.""" self.current_cwd = cwd def server_is_ready(self): """Return server status.""" return self.server_ready def search_next(self, text, case=False, regex=False, word=False):
class TerminalPlugin(SpyderPluginWidget): """Terminal plugin.""" URL_ISSUES = ' https://github.com/spyder-ide/spyder-terminal/issues' CONF_SECTION = 'terminal' focus_changed = Signal() sig_server_is_ready = Signal() MAX_SERVER_CONTACT_RETRIES = 40 def __init__(self, parent): """Widget constructor.""" SpyderPluginWidget.__init__(self, parent) self.tab_widget = None self.menu_actions = None self.server_retries = 0 self.server_ready = False self.port = select_port(default_port=8071) self.cmd = 'bash' if WINDOWS: self.cmd = 'cmd' self.server_stdout = subprocess.PIPE self.server_stderr = subprocess.PIPE self.stdout_file = osp.join(getcwd(), 'spyder_terminal_out.log') self.stderr_file = osp.join(getcwd(), 'spyder_terminal_err.log') if DEV: self.server_stdout = open(self.stdout_file, 'w') self.server_stderr = open(self.stderr_file, 'w') self.server = subprocess.Popen( [sys.executable, '-m', 'spyder_terminal.server', '--port', str(self.port), '--shell', self.cmd], stdout=self.server_stdout, stderr=self.server_stderr) self.main = parent self.terms = [] self.untitled_num = 0 self.project_path = None self.current_file_path = None self.current_cwd = getcwd() self.initialize_plugin() layout = QVBoxLayout() new_term_btn = create_toolbutton(self, icon=ima.icon('project_expanded'), tip=_('Open a new terminal'), triggered=self.create_new_term) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), tip=_('Options')) self.menu = QMenu(self) menu_btn.setMenu(self.menu) menu_btn.setPopupMode(menu_btn.InstantPopup) add_actions(self.menu, self.menu_actions) # if self.get_option('first_time', True): # self.setup_shortcuts() # self.shortcuts = self.create_shortcuts() corner_widgets = {Qt.TopRightCorner: [new_term_btn, menu_btn]} self.tabwidget = Tabs(self, menu=self.menu, actions=self.menu_actions, corner_widgets=corner_widgets, rename_tabs=True) if hasattr(self.tabwidget, 'setDocumentMode') \ and not sys.platform == 'darwin': # Don't set document mode to true on OSX because it generates # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_term) layout.addWidget(self.tabwidget) self.setLayout(layout) new_term_shortcut = QShortcut(QKeySequence("Ctrl+Alt+Shift+T"), self, self.create_new_term) new_term_shortcut.setContext(Qt.WidgetWithChildrenShortcut) self.__wait_server_to_start() # ------ Private API ------------------------------------------ def __wait_server_to_start(self): try: code = requests.get('http://127.0.0.1:{0}'.format( self.port)).status_code except: code = 500 if self.server_retries == self.MAX_SERVER_CONTACT_RETRIES: QMessageBox.critical(self, _('Spyder Terminal Error'), _("Terminal server could not be located at " '<a href="http://127.0.0.1:{0}">' 'http://127.0.0.1:{0}</a>,' ' please restart Spyder on debugging mode ' "and open an issue with the contents of " "<tt>{1}</tt> and <tt>{2}</tt> " "files at {3}.").format(self.port, self.stdout_file, self.stderr_file, self.URL_ISSUES), QMessageBox.Ok) elif code != 200: self.server_retries += 1 QTimer.singleShot(250, self.__wait_server_to_start) elif code == 200: self.sig_server_is_ready.emit() self.server_ready = True self.create_new_term(give_focus=False) # ------ SpyderPluginMixin API -------------------------------- def on_first_registration(self): """Action to be performed on first plugin registration.""" self.main.tabify_plugins(self.main.ipyconsole, self) def update_font(self): """Update font from Preferences.""" font = self.get_plugin_font() for term in self.terms: term.set_font(font.family()) def check_compatibility(self): """Check if current Qt backend version is greater or equal to 5.""" message = '' valid = True if PYQT4 or PYSIDE: message = _('<b>spyder-terminal</b> doesn\'t work with Qt 4. ' 'Therefore, this plugin will be deactivated.') valid = False if WINDOWS: try: import winpty del winpty except: message = _('Unfortunately, the library that <b>spyder-termina' 'l</b> uses to create terminals is failing to ' 'work in your system. Therefore, this plugin will ' 'be deactivated.<br><br> This usually happens on ' 'Windows 7 systems. If that\'s the case, please ' 'consider updating to a newer Windows version.') valid = False return valid, message # ------ SpyderPluginWidget API ------------------------------ def get_plugin_title(self): """Return widget title.""" title = _('Terminal') return title def get_plugin_icon(self): """Return widget icon.""" return ima.icon('cmdprompt') def get_plugin_actions(self): """Get plugin actions.""" new_terminal_cwd = create_action(self, _("New terminal in current " "working directory"), tip=_("Sets the pwd at " "the current working " "directory"), triggered=self.create_new_term) self.new_terminal_project = create_action(self, _("New terminal in current " "project"), tip=_("Sets the pwd at " "the current project " "directory"), triggered=lambda: self.create_new_term( path=self.project_path)) new_terminal_file = create_action(self, _("New terminal in current Editor " "file"), tip=_("Sets the pwd at " "the directory that contains " "the current opened file"), triggered=lambda: self.create_new_term( path=self.current_file_path)) rename_tab_action = create_action(self, _("Rename terminal"), triggered=self.tab_name_editor) self.menu_actions = [new_terminal_cwd, self.new_terminal_project, new_terminal_file, MENU_SEPARATOR, rename_tab_action] self.setup_menu_actions() return self.menu_actions def setup_menu_actions(self): """Setup and update the Options menu actions.""" if self.project_path is None: self.new_terminal_project.setEnabled(False) def get_focus_widget(self): """ Set focus on current selected terminal. Return the widget to give focus to when this plugin's dockwidget is raised on top-level. """ term = self.tabwidget.currentWidget() if term is not None: return term.view def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed.""" for term in self.terms: term.close() self.server.terminate() if DEV: self.server_stdout.close() self.server_stderr.close() return True def refresh_plugin(self): """Refresh tabwidget.""" term = None if self.tabwidget.count(): term = self.tabwidget.currentWidget() term.view.setFocus() else: term = None def register_plugin(self): """Register plugin in Spyder's main window.""" self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) self.main.workingdirectory.set_explorer_cwd.connect( self.set_current_cwd) self.main.projects.sig_project_loaded.connect(self.set_project_path) self.main.projects.sig_project_closed.connect(self.unset_project_path) self.main.editor.open_file_update.connect(self.set_current_opened_file) self.menu.aboutToShow.connect(self.setup_menu_actions) # ------ Public API (for terminals) ------------------------- def get_terms(self): """Return terminal list.""" return [cl for cl in self.terms if isinstance(cl, TerminalWidget)] def get_focus_term(self): """Return current terminal with focus, if any.""" widget = QApplication.focusWidget() for term in self.get_terms(): if widget is term: return term def get_current_term(self): """Return the currently selected terminal.""" try: terminal = self.tabwidget.currentWidget() except AttributeError: terminal = None if terminal is not None: return terminal def create_new_term(self, name=None, give_focus=True, path=None): """Add a new terminal tab.""" if path is None: path = self.current_cwd path = path.replace('\\', '/') font = self.get_plugin_font() term = TerminalWidget(self, self.port, path=path, font=font.family()) self.add_tab(term) term.terminal_closed.connect(lambda: self.close_term(term=term)) def close_term(self, index=None, term=None): """Close a terminal tab.""" if not self.tabwidget.count(): return if term is not None: index = self.tabwidget.indexOf(term) if index is None and term is None: index = self.tabwidget.currentIndex() if index is not None: term = self.tabwidget.widget(index) term.close() self.tabwidget.removeTab(self.tabwidget.indexOf(term)) self.terms.remove(term) if self.tabwidget.count() == 0: self.create_new_term() def set_project_path(self, path): """Refresh current project path.""" self.project_path = path self.new_terminal_project.setEnabled(True) def set_current_opened_file(self, path): """Get path of current opened file in editor.""" self.current_file_path = osp.dirname(path) def unset_project_path(self): """Refresh current project path.""" self.project_path = None self.new_terminal_project.setEnabled(False) @Slot(str) def set_current_cwd(self, cwd): """Update current working directory.""" self.current_cwd = cwd def server_is_ready(self): """Return server status.""" return self.server_ready # ------ Public API (for tabs) --------------------------- def add_tab(self, widget): """Add tab.""" self.terms.append(widget) num_term = self.tabwidget.count() + 1 index = self.tabwidget.addTab(widget, "Terminal {0}".format(num_term)) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, "Terminal {0}".format(num_term)) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.activateWindow() widget.view.setFocus() def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget). Allows to change order of tabs. """ term = self.terms.pop(index_from) self.terms.insert(index_to, term) def tab_name_editor(self): """Trigger the tab name editor.""" index = self.tabwidget.currentIndex() self.tabwidget.tabBar().tab_name_editor.edit_tab(index)
class TerminalPlugin(SpyderPluginWidget): """Terminal plugin.""" CONF_SECTION = 'terminal' focus_changed = Signal() def __init__(self, parent): """Widget constructor.""" SpyderPluginWidget.__init__(self, parent) self.tab_widget = None self.menu_actions = None self.port = select_port(default_port=8070) self.server = subprocess.Popen([ sys.executable, osp.join(LOCATION, 'server', 'main.py'), '--port', str(self.port) ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(0.5) self.main = parent self.terms = [] self.untitled_num = 0 self.project_path = None self.current_file_path = None self.initialize_plugin() layout = QVBoxLayout() new_term_btn = create_toolbutton(self, icon=ima.icon('project_expanded'), tip=_('Open a new terminal'), triggered=self.create_new_term) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), tip=_('Options')) self.menu = QMenu(self) menu_btn.setMenu(self.menu) menu_btn.setPopupMode(menu_btn.InstantPopup) add_actions(self.menu, self.menu_actions) # if self.get_option('first_time', True): # self.setup_shortcuts() # self.shortcuts = self.create_shortcuts() corner_widgets = {Qt.TopRightCorner: [new_term_btn, menu_btn]} self.tabwidget = Tabs(self, menu=self.menu, actions=self.menu_actions, corner_widgets=corner_widgets) if hasattr(self.tabwidget, 'setDocumentMode') \ and not sys.platform == 'darwin': # Don't set document mode to true on OSX because it generates # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_term) layout.addWidget(self.tabwidget) self.setLayout(layout) paste_shortcut = QShortcut(QKeySequence("Ctrl+Shift+T"), self, self.create_new_term) paste_shortcut.setContext(Qt.WidgetWithChildrenShortcut) # ------ SpyderPluginMixin API -------------------------------- def on_first_registration(self): """Action to be performed on first plugin registration.""" self.main.tabify_plugins(self.main.extconsole, self) def update_font(self): """Update font from Preferences.""" font = self.get_plugin_font() for term in self.terms: term.set_font(font.family()) # ------ SpyderPluginWidget API ------------------------------ def get_plugin_title(self): """Return widget title.""" title = _('Terminal') return title def get_plugin_icon(self): """Return widget icon.""" return ima.icon('cmdprompt') def get_plugin_actions(self): """Get plugin actions.""" new_terminal_menu = QMenu(_("Create new terminal"), self) new_terminal_menu.setIcon(ima.icon('project_expanded')) new_terminal_cwd = create_action(self, _("Current workspace path"), icon=ima.icon('cmdprompt'), tip=_("Sets the pwd at " "the current workspace " "folder"), triggered=self.create_new_term) self.new_terminal_project = create_action( self, _("Current project folder"), icon=ima.icon('cmdprompt'), tip=_("Sets the pwd at " "the current project " "folder"), triggered=lambda: self.create_new_term(path=self.project_path)) if self.project_path is None: self.new_terminal_project.setEnabled(False) new_terminal_file = create_action( self, _("Current opened file folder"), icon=ima.icon('cmdprompt'), tip=_("Sets the pwd at " "the folder that contains " "the current opened file"), triggered=lambda: self.create_new_term(path=self.current_file_path )) add_actions( new_terminal_menu, (new_terminal_cwd, self.new_terminal_project, new_terminal_file)) self.menu_actions = [None, new_terminal_menu, None] return self.menu_actions def get_focus_widget(self): """ Set focus on current selected terminal. Return the widget to give focus to when this plugin's dockwidget is raised on top-level. """ term = self.tabwidget.currentWidget() if term is not None: return term.view def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed.""" for term in self.terms: term.close() self.server.terminate() return True def refresh_plugin(self): """Refresh tabwidget.""" term = None if self.tabwidget.count(): term = self.tabwidget.currentWidget() term.view.setFocus() else: term = None def register_plugin(self): """Register plugin in Spyder's main window.""" self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) self.main.projects.sig_project_loaded.connect(self.set_project_path) self.main.projects.sig_project_closed.connect(self.unset_project_path) self.main.editor.open_file_update.connect(self.set_current_opened_file) self.create_new_term(give_focus=False) # ------ Public API (for terminals) ------------------------- def get_terms(self): """Return terminal list.""" return [cl for cl in self.terms if isinstance(cl, TerminalWidget)] def get_focus_term(self): """Return current terminal with focus, if any.""" widget = QApplication.focusWidget() for term in self.get_terms(): if widget is term: return term def get_current_term(self): """Return the currently selected terminal.""" try: terminal = self.tabwidget.currentWidget() except AttributeError: terminal = None if terminal is not None: return terminal def create_new_term(self, name=None, give_focus=True, path=getcwd()): """Add a new terminal tab.""" font = self.get_plugin_font() term = TerminalWidget(self, self.port, path=path, font=font.family()) self.add_tab(term) def close_term(self, index=None, term=None): """Close a terminal tab.""" if not self.tabwidget.count(): return if term is not None: index = self.tabwidget.indexOf(term) if index is None and term is None: index = self.tabwidget.currentIndex() if index is not None: term = self.tabwidget.widget(index) term.close() self.tabwidget.removeTab(self.tabwidget.indexOf(term)) self.terms.remove(term) if self.tabwidget.count() == 0: self.create_new_term() def set_project_path(self, path): """Refresh current project path.""" self.project_path = path self.new_terminal_project.setEnabled(True) def set_current_opened_file(self, path): """Get path of current opened file in editor.""" self.current_file_path = osp.dirname(path) def unset_project_path(self): """Refresh current project path.""" self.project_path = None self.new_terminal_project.setEnabled(False) # ------ Public API (for tabs) --------------------------- def add_tab(self, widget): """Add tab.""" self.terms.append(widget) num_term = self.tabwidget.count() + 1 index = self.tabwidget.addTab(widget, "Terminal {0}".format(num_term)) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, "Terminal {0}".format(num_term)) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() self.activateWindow() widget.view.setFocus() def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget). Allows to change order of tabs. """ term = self.terms.pop(index_from) self.terms.insert(index_to, term)