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 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 ExternalConsole(SpyderPluginWidget): """ Console widget """ CONF_SECTION = 'console' CONFIGWIDGET_CLASS = ExternalConsoleConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False edit_goto = Signal((str, int, str), (str, int, str, bool)) focus_changed = Signal() redirect_stdio = Signal(bool) def __init__(self, parent): if PYQT5: SpyderPluginWidget.__init__(self, parent, main=parent) else: SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.menu_actions = None self.help = None # Help plugin self.historylog = None # History log plugin self.variableexplorer = None # Variable explorer plugin self.python_count = 0 self.terminal_count = 0 # Python startup file selection if not osp.isfile(self.get_option('pythonstartup', '')): self.set_option('pythonstartup', SCIENTIFIC_STARTUP) # default/custom settings are mutually exclusive: self.set_option('pythonstartup/custom', not self.get_option('pythonstartup/default')) self.shellwidgets = [] self.filenames = [] self.icons = [] self.runfile_args = "" # Initialize plugin self.initialize_plugin() layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) 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.main.sig_pythonpath_changed.connect(self.set_path) self.tabwidget.set_close_function(self.close_console) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) # Accepting drops self.setAcceptDrops(True) def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) shell = self.shellwidgets.pop(index_from) icons = self.icons.pop(index_from) self.filenames.insert(index_to, filename) self.shellwidgets.insert(index_to, shell) self.icons.insert(index_to, icons) self.update_plugin_title.emit() def get_shell_index_from_id(self, shell_id): """Return shellwidget index from id""" for index, shell in enumerate(self.shellwidgets): if id(shell) == shell_id: return index def close_console(self, index=None): """Close console tab from index or widget (or close current tab)""" # Get tab index if not self.tabwidget.count(): return if index is None: index = self.tabwidget.currentIndex() # Closing logic self.tabwidget.widget(index).close() self.tabwidget.removeTab(index) self.filenames.pop(index) self.shellwidgets.pop(index) self.icons.pop(index) self.update_plugin_title.emit() def set_variableexplorer(self, variableexplorer): """Set variable explorer plugin""" self.variableexplorer = variableexplorer def set_path(self): """Set consoles PYTHONPATH if changed by the user""" from spyder.widgets.externalshell import pythonshell for sw in self.shellwidgets: if isinstance(sw, pythonshell.ExternalPythonShell): if sw.is_interpreter and sw.is_running(): sw.path = self.main.get_spyder_pythonpath() sw.shell.path = sw.path def __find_python_shell(self, interpreter_only=False): current_index = self.tabwidget.currentIndex() if current_index == -1: return from spyder.widgets.externalshell import pythonshell for index in [current_index] + list(range(self.tabwidget.count())): shellwidget = self.tabwidget.widget(index) if isinstance(shellwidget, pythonshell.ExternalPythonShell): if interpreter_only and not shellwidget.is_interpreter: continue elif not shellwidget.is_running(): continue else: self.tabwidget.setCurrentIndex(index) return shellwidget def get_current_shell(self): """ Called by Help to retrieve the current shell instance """ shellwidget = self.__find_python_shell() return shellwidget.shell def get_running_python_shell(self): """ Called by Help to retrieve a running Python shell instance """ current_index = self.tabwidget.currentIndex() if current_index == -1: return from spyder.widgets.externalshell import pythonshell shellwidgets = [ self.tabwidget.widget(index) for index in range(self.tabwidget.count()) ] shellwidgets = [_w for _w in shellwidgets if isinstance(_w, pythonshell.ExternalPythonShell) \ and _w.is_running()] if shellwidgets: # First, iterate on interpreters only: for shellwidget in shellwidgets: if shellwidget.is_interpreter: return shellwidget.shell else: return shellwidgets[0].shell def run_script_in_current_shell(self, filename, wdir, args, debug, post_mortem): """Run script in current shell, if any""" norm = lambda text: remove_backslashes(to_text_string(text)) line = "%s('%s'" % ('debugfile' if debug else 'runfile', norm(filename)) if args: line += ", args='%s'" % norm(args) if wdir: line += ", wdir='%s'" % norm(wdir) if post_mortem: line += ', post_mortem=True' line += ")" if not self.execute_code(line, interpreter_only=True): QMessageBox.warning( self, _('Warning'), _("No Python console is currently selected to run <b>%s</b>." "<br><br>Please select or open a new Python console " "and try again.") % osp.basename(norm(filename)), QMessageBox.Ok) else: self.visibility_changed(True) self.raise_() def set_current_shell_working_directory(self, directory): """Set current shell working directory""" shellwidget = self.__find_python_shell() if shellwidget is not None: directory = encoding.to_unicode_from_fs(directory) shellwidget.shell.set_cwd(directory) def execute_code(self, lines, interpreter_only=False): """Execute code in an already opened Python interpreter""" shellwidget = self.__find_python_shell( interpreter_only=interpreter_only) if shellwidget is not None: shellwidget.shell.execute_lines(to_text_string(lines)) self.activateWindow() shellwidget.shell.setFocus() return True else: return False def pdb_has_stopped(self, fname, lineno, shellwidget): """Python debugger has just stopped at frame (fname, lineno)""" # This is a unique form of the edit_goto signal that is intended to # prevent keyboard input from accidentally entering the editor # during repeated, rapid entry of debugging commands. self.edit_goto[str, int, str, bool].emit(fname, lineno, '', False) self.activateWindow() shellwidget.shell.setFocus() def set_spyder_breakpoints(self): """Set all Spyder breakpoints into all shells""" for shellwidget in self.shellwidgets: shellwidget.shell.set_spyder_breakpoints() def start(self, fname, wdir=None, args='', interact=False, debug=False, python=True, python_args='', post_mortem=True): """ Start new console fname: string: filename of script to run None: open an interpreter wdir: working directory args: command line options of the Python script interact: inspect script interactively after its execution debug: run pdb python: True: Python interpreter, False: terminal python_args: additionnal Python interpreter command line options (option "-u" is mandatory, see widgets.externalshell package) """ # Note: fname is None <=> Python interpreter if fname is not None and not is_text_string(fname): fname = to_text_string(fname) if wdir is not None and not is_text_string(wdir): wdir = to_text_string(wdir) if fname is not None and fname in self.filenames: index = self.filenames.index(fname) if self.get_option('single_tab'): old_shell = self.shellwidgets[index] if old_shell.is_running(): runconfig = get_run_configuration(fname) if runconfig is None or runconfig.show_kill_warning: answer = QMessageBox.question( self, self.get_plugin_title(), _("%s is already running in a separate process.\n" "Do you want to kill the process before starting " "a new one?") % osp.basename(fname), QMessageBox.Yes | QMessageBox.Cancel) else: answer = QMessageBox.Yes if answer == QMessageBox.Yes: old_shell.process.kill() old_shell.process.waitForFinished() else: return self.close_console(index) else: index = self.tabwidget.count() # Creating a new external shell pythonpath = self.main.get_spyder_pythonpath() light_background = self.get_option('light_background') show_elapsed_time = self.get_option('show_elapsed_time') if python: if CONF.get('main_interpreter', 'default'): pythonexecutable = get_python_executable() external_interpreter = False else: pythonexecutable = CONF.get('main_interpreter', 'executable') external_interpreter = True if self.get_option('pythonstartup/default'): pythonstartup = None else: pythonstartup = self.get_option('pythonstartup', None) monitor_enabled = self.get_option('monitor/enabled') mpl_backend = self.get_option('matplotlib/backend/value') ets_backend = self.get_option('ets_backend') qt_api = self.get_option('qt/api') if qt_api not in ('pyqt', 'pyside', 'pyqt5'): qt_api = None merge_output_channels = self.get_option('merge_output_channels') colorize_sys_stderr = self.get_option('colorize_sys_stderr') umr_enabled = CONF.get('main_interpreter', 'umr/enabled') umr_namelist = CONF.get('main_interpreter', 'umr/namelist') umr_verbose = CONF.get('main_interpreter', 'umr/verbose') ar_timeout = CONF.get('variable_explorer', 'autorefresh/timeout') ar_state = CONF.get('variable_explorer', 'autorefresh') sa_settings = None shellwidget = ExternalPythonShell( self, fname, wdir, interact, debug, post_mortem=post_mortem, path=pythonpath, python_args=python_args, arguments=args, stand_alone=sa_settings, pythonstartup=pythonstartup, pythonexecutable=pythonexecutable, external_interpreter=external_interpreter, umr_enabled=umr_enabled, umr_namelist=umr_namelist, umr_verbose=umr_verbose, ets_backend=ets_backend, monitor_enabled=monitor_enabled, mpl_backend=mpl_backend, qt_api=qt_api, merge_output_channels=merge_output_channels, colorize_sys_stderr=colorize_sys_stderr, autorefresh_timeout=ar_timeout, autorefresh_state=ar_state, light_background=light_background, menu_actions=self.menu_actions, show_buttons_inside=False, show_elapsed_time=show_elapsed_time) shellwidget.sig_pdb.connect( lambda fname, lineno, shellwidget=shellwidget: self. pdb_has_stopped(fname, lineno, shellwidget)) self.register_widget_shortcuts(shellwidget.shell) else: if os.name == 'posix': cmd = 'gnome-terminal' args = [] if programs.is_program_installed(cmd): if wdir: args.extend(['--working-directory=%s' % wdir]) programs.run_program(cmd, args) return cmd = 'konsole' if programs.is_program_installed(cmd): if wdir: args.extend(['--workdir', wdir]) programs.run_program(cmd, args) return shellwidget = ExternalSystemShell( self, wdir, path=pythonpath, light_background=light_background, menu_actions=self.menu_actions, show_buttons_inside=False, show_elapsed_time=show_elapsed_time) # Code completion / calltips shellwidget.shell.setMaximumBlockCount( self.get_option('max_line_count')) shellwidget.shell.set_font(self.get_plugin_font()) shellwidget.shell.toggle_wrap_mode(self.get_option('wrap')) shellwidget.shell.set_calltips(self.get_option('calltips')) shellwidget.shell.set_codecompletion_auto( self.get_option('codecompletion/auto')) shellwidget.shell.set_codecompletion_case( self.get_option('codecompletion/case_sensitive')) shellwidget.shell.set_codecompletion_enter( self.get_option('codecompletion/enter_key')) if python and self.help is not None: shellwidget.shell.set_help(self.help) shellwidget.shell.set_help_enabled( CONF.get('help', 'connect/python_console')) if self.historylog is not None: self.historylog.add_history(shellwidget.shell.history_filename) shellwidget.shell.append_to_history.connect( self.historylog.append_to_history) shellwidget.shell.go_to_error.connect(self.go_to_error) shellwidget.shell.focus_changed.connect( lambda: self.focus_changed.emit()) if python: if self.main.editor is not None: shellwidget.open_file.connect(self.open_file_in_spyder) if fname is None: self.python_count += 1 tab_name = "Python %d" % self.python_count tab_icon1 = ima.icon('python') tab_icon2 = ima.icon('python_t') else: tab_name = osp.basename(fname) tab_icon1 = ima.icon('run') tab_icon2 = ima.icon('terminated') else: fname = id(shellwidget) if os.name == 'nt': tab_name = _("Command Window") else: tab_name = _("Terminal") self.terminal_count += 1 tab_name += (" %d" % self.terminal_count) tab_icon1 = ima.icon('cmdprompt') tab_icon2 = ima.icon('cmdprompt_t') self.shellwidgets.insert(index, shellwidget) self.filenames.insert(index, fname) self.icons.insert(index, (tab_icon1, tab_icon2)) if index is None: index = self.tabwidget.addTab(shellwidget, tab_name) else: self.tabwidget.insertTab(index, shellwidget, tab_name) shellwidget.started.connect( lambda sid=id(shellwidget): self.process_started(sid)) shellwidget.sig_finished.connect( lambda sid=id(shellwidget): self.process_finished(sid)) self.find_widget.set_editor(shellwidget.shell) self.tabwidget.setTabToolTip(index, fname if wdir is None else wdir) self.tabwidget.setCurrentIndex(index) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() shellwidget.set_icontext_visible(self.get_option('show_icontext')) # Start process and give focus to console shellwidget.start_shell() def open_file_in_spyder(self, fname, lineno): """Open file in Spyder's editor from remote process""" self.main.editor.activateWindow() self.main.editor.raise_() self.main.editor.load(fname, lineno) #------ Private API ------------------------------------------------------- def process_started(self, shell_id): index = self.get_shell_index_from_id(shell_id) shell = self.shellwidgets[index] icon, _icon = self.icons[index] self.tabwidget.setTabIcon(index, icon) if self.help is not None: self.help.set_shell(shell.shell) if self.variableexplorer is not None: self.variableexplorer.add_shellwidget(shell) def process_finished(self, shell_id): index = self.get_shell_index_from_id(shell_id) if index is not None: # Not sure why it happens, but sometimes the shellwidget has # already been removed, so that's not bad if we can't change # the tab icon... _icon, icon = self.icons[index] self.tabwidget.setTabIcon(index, icon) if self.variableexplorer is not None: self.variableexplorer.remove_shellwidget(shell_id) #------ SpyderPluginWidget API -------------------------------------------- def get_plugin_title(self): """Return widget title""" title = _('Python console') if self.filenames: index = self.tabwidget.currentIndex() fname = self.filenames[index] if fname: title += ' - ' + to_text_string(fname) return title def get_plugin_icon(self): """Return widget icon""" return ima.icon('console') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def get_plugin_actions(self): """Return a list of actions related to plugin""" interpreter_action = create_action(self, _("Open a &Python console"), None, ima.icon('python'), triggered=self.open_interpreter) if os.name == 'nt': text = _("Open &command prompt") tip = _("Open a Windows command prompt") else: text = _("Open a &terminal") tip = _("Open a terminal window") terminal_action = create_action(self, text, None, None, tip, triggered=self.open_terminal) run_action = create_action(self, _("&Run..."), None, ima.icon('run_small'), _("Run a Python script"), triggered=self.run_script) consoles_menu_actions = [interpreter_action] tools_menu_actions = [terminal_action] self.menu_actions = [interpreter_action, terminal_action, run_action] self.main.consoles_menu_actions += consoles_menu_actions self.main.tools_menu_actions += tools_menu_actions return self.menu_actions + consoles_menu_actions + tools_menu_actions def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) self.help = self.main.help self.historylog = self.main.historylog self.edit_goto.connect(self.main.editor.load) self.edit_goto[str, int, str, bool].connect( lambda fname, lineno, word, processevents: self.main.editor.load( fname, lineno, word, processevents=processevents)) self.main.editor.run_in_current_extconsole.connect( self.run_script_in_current_shell) self.main.editor.breakpoints_saved.connect(self.set_spyder_breakpoints) self.main.editor.open_dir.connect( self.set_current_shell_working_directory) self.main.workingdirectory.set_current_console_wd.connect( self.set_current_shell_working_directory) self.focus_changed.connect(self.main.plugin_focus_changed) self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) expl = self.main.explorer if expl is not None: expl.open_terminal.connect(self.open_terminal) expl.open_interpreter.connect(self.open_interpreter) pexpl = self.main.projects if pexpl is not None: pexpl.open_terminal.connect(self.open_terminal) pexpl.open_interpreter.connect(self.open_interpreter) def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" for shellwidget in self.shellwidgets: shellwidget.close() return True def restart(self): """ Restart the console This is needed when we switch project to update PYTHONPATH and the selected interpreter """ self.python_count = 0 for i in range(len(self.shellwidgets)): self.close_console() self.open_interpreter() def refresh_plugin(self): """Refresh tabwidget""" shellwidget = None if self.tabwidget.count(): shellwidget = self.tabwidget.currentWidget() editor = shellwidget.shell editor.setFocus() widgets = [shellwidget.create_time_label(), 5 ] + shellwidget.get_toolbar_buttons() + [5] else: editor = None widgets = [] self.find_widget.set_editor(editor) self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets}) if shellwidget: shellwidget.update_time_label_visibility() self.variableexplorer.set_shellwidget_from_id(id(shellwidget)) self.help.set_shell(shellwidget.shell) self.main.last_console_plugin_focus_was_python = True self.update_plugin_title.emit() def update_font(self): """Update font from Preferences""" font = self.get_plugin_font() for shellwidget in self.shellwidgets: shellwidget.shell.set_font(font) completion_size = CONF.get('main', 'completion/size') comp_widget = shellwidget.shell.completion_widget comp_widget.setup_appearance(completion_size, font) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" showtime_n = 'show_elapsed_time' showtime_o = self.get_option(showtime_n) icontext_n = 'show_icontext' icontext_o = self.get_option(icontext_n) calltips_n = 'calltips' calltips_o = self.get_option(calltips_n) help_n = 'connect_to_oi' help_o = CONF.get('help', 'connect/python_console') wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) compauto_n = 'codecompletion/auto' compauto_o = self.get_option(compauto_n) case_comp_n = 'codecompletion/case_sensitive' case_comp_o = self.get_option(case_comp_n) compenter_n = 'codecompletion/enter_key' compenter_o = self.get_option(compenter_n) mlc_n = 'max_line_count' mlc_o = self.get_option(mlc_n) for shellwidget in self.shellwidgets: if showtime_n in options: shellwidget.set_elapsed_time_visible(showtime_o) if icontext_n in options: shellwidget.set_icontext_visible(icontext_o) if calltips_n in options: shellwidget.shell.set_calltips(calltips_o) if help_n in options: if isinstance(shellwidget, ExternalPythonShell): shellwidget.shell.set_help_enabled(help_o) if wrap_n in options: shellwidget.shell.toggle_wrap_mode(wrap_o) if compauto_n in options: shellwidget.shell.set_codecompletion_auto(compauto_o) if case_comp_n in options: shellwidget.shell.set_codecompletion_case(case_comp_o) if compenter_n in options: shellwidget.shell.set_codecompletion_enter(compenter_o) if mlc_n in options: shellwidget.shell.setMaximumBlockCount(mlc_o) #------ SpyderPluginMixin API --------------------------------------------- def toggle_view(self, checked): """Toggle view""" if checked: self.dockwidget.show() self.dockwidget.raise_() # Start a console in case there are none shown from spyder.widgets.externalshell import pythonshell consoles = None for sw in self.shellwidgets: if isinstance(sw, pythonshell.ExternalPythonShell): consoles = True break if not consoles: self.open_interpreter() else: self.dockwidget.hide() #------ Public API --------------------------------------------------------- @Slot(bool) @Slot(str) def open_interpreter(self, wdir=None): """Open interpreter""" if not wdir: wdir = getcwd() self.visibility_changed(True) self.start(fname=None, wdir=to_text_string(wdir), args='', interact=True, debug=False, python=True) @Slot(bool) @Slot(str) def open_terminal(self, wdir=None): """Open terminal""" if not wdir: wdir = getcwd() self.start(fname=None, wdir=to_text_string(wdir), args='', interact=True, debug=False, python=False) @Slot() def run_script(self): """Run a Python script""" self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Run Python script"), getcwd(), _("Python scripts") + " (*.py ; *.pyw ; *.ipy)") self.redirect_stdio.emit(True) if filename: self.start(fname=filename, wdir=None, args='', interact=False, debug=False) def go_to_error(self, text): """Go to error if relevant""" match = get_error_match(to_text_string(text)) if match: fname, lnb = match.groups() self.edit_goto.emit(osp.abspath(fname), int(lnb), '') #----Drag and drop def dragEnterEvent(self, event): """Reimplement Qt method Inform Qt about the types of data that the widget accepts""" source = event.mimeData() if source.hasUrls(): if mimedata2url(source): pathlist = mimedata2url(source) shellwidget = self.tabwidget.currentWidget() if all([ is_python_script(to_text_string(qstr)) for qstr in pathlist ]): event.acceptProposedAction() elif shellwidget is None or not shellwidget.is_running(): event.ignore() else: event.acceptProposedAction() else: event.ignore() elif source.hasText(): event.acceptProposedAction() def dropEvent(self, event): """Reimplement Qt method Unpack dropped data and handle it""" source = event.mimeData() shellwidget = self.tabwidget.currentWidget() if source.hasText(): qstr = source.text() if is_python_script(to_text_string(qstr)): self.start(qstr) elif shellwidget: shellwidget.shell.insert_text(qstr) elif source.hasUrls(): pathlist = mimedata2url(source) if all( [is_python_script(to_text_string(qstr)) for qstr in pathlist]): for fname in pathlist: self.start(fname) elif shellwidget: shellwidget.shell.drop_pathlist(pathlist) event.acceptProposedAction()
class HistoryLog(SpyderPluginWidget): """ History log widget """ CONF_SECTION = 'historylog' CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() def __init__(self, parent): self.tabwidget = None self.menu_actions = None self.dockviewer = None self.wrap_action = None self.editors = [] self.filenames = [] if PYQT5: SpyderPluginWidget.__init__(self, parent, main=parent) else: SpyderPluginWidget.__init__(self, parent) # Initialize plugin self.initialize_plugin() layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Menu as corner widget options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, self.menu_actions) options_button.setMenu(menu) self.tabwidget.setCornerWidget(options_button) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('History log') def get_plugin_icon(self): """Return widget icon""" return ima.icon('history') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh tabwidget""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) def get_plugin_actions(self): """Return a list of actions related to plugin""" history_action = create_action(self, _("History..."), None, ima.icon('history'), _("Set history maximum entries"), triggered=self.change_history_depth) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked(self.get_option('wrap')) self.menu_actions = [history_action, self.wrap_action] return self.menu_actions def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.ipyconsole, self) 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.console.set_historylog(self) self.main.console.shell.refresh.connect(self.refresh_plugin) def update_font(self): """Update font from Preferences""" color_scheme = self.get_color_scheme() font = self.get_plugin_font() for editor in self.editors: editor.set_font(font, color_scheme) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" color_scheme_n = 'color_scheme_name' color_scheme_o = self.get_color_scheme() font_n = 'plugin_font' font_o = self.get_plugin_font() wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) self.wrap_action.setChecked(wrap_o) for editor in self.editors: if font_n in options: scs = color_scheme_o if color_scheme_n in options else None editor.set_font(font_o, scs) elif color_scheme_n in options: editor.set_color_scheme(color_scheme_o) if wrap_n in options: editor.toggle_wrap_mode(wrap_o) #------ Private API -------------------------------------------------------- def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) #------ Public API --------------------------------------------------------- def add_history(self, filename): """ Add new history tab Slot for add_history signal emitted by shell instance """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == '.py': language = 'py' else: language = 'bat' editor.setup_editor(linenumbers=False, language=language, scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) color_scheme = self.get_color_scheme() editor.set_font(self.get_plugin_font(), color_scheme) editor.toggle_wrap_mode(self.get_option('wrap')) text, _ = encoding.read(filename) editor.set_text(text) editor.set_cursor_position('eof') self.editors.append(editor) self.filenames.append(filename) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) self.tabwidget.setCurrentIndex(index) def append_to_history(self, filename, command): """ Append an entry to history filename Slot for append_to_history signal emitted by shell instance """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') command = to_text_string(command) index = self.filenames.index(filename) self.editors[index].append(command) if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) @Slot() def change_history_depth(self): "Change history max entries" "" depth, valid = QInputDialog.getInt(self, _('History'), _('Maximum entries'), self.get_option('max_entries'), 10, 10000) if valid: self.set_option('max_entries', depth) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_wrap_mode(checked) self.set_option('wrap', checked)
class HistoryWidget(PluginMainWidget): """ History plugin main widget. """ # Signals sig_focus_changed = Signal() """ This signal is emitted when the focus of the code editor storing history changes. """ def __init__(self, name, plugin, parent): super().__init__(name, plugin, parent) # Attributes self.editors = [] self.filenames = [] self.tabwidget = None self.dockviewer = None self.wrap_action = None self.linenumbers_action = None self.filenames = [] self.font = None # Widgets self.tabwidget = Tabs(self) self.find_widget = FindReplace(self) # Setup self.tabwidget.setStyleSheet(self._tabs_stylesheet) self.find_widget.hide() # Layout layout = QVBoxLayout() # TODO: Move this to the tab container directly if sys.platform == 'darwin': tab_container = QWidget(self) tab_container.setObjectName('tab-container') tab_layout = QVBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) layout.addWidget(self.find_widget) self.setLayout(layout) # Signals self.tabwidget.currentChanged.connect(self.refresh) self.tabwidget.move_data.connect(self.move_tab) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('History') def get_focus_widget(self): return self.tabwidget.currentWidget() def setup(self): # Actions self.wrap_action = self.create_action( HistoryWidgetActions.ToggleWrap, text=_("Wrap lines"), toggled=True, initial=self.get_conf('wrap'), option='wrap' ) self.linenumbers_action = self.create_action( HistoryWidgetActions.ToggleLineNumbers, text=_("Show line numbers"), toggled=True, initial=self.get_conf('line_numbers'), option='line_numbers' ) # Menu menu = self.get_options_menu() for item in [self.wrap_action, self.linenumbers_action]: self.add_item_to_menu( item, menu=menu, section=HistoryWidgetOptionsMenuSections.Main, ) def update_actions(self): pass @on_conf_change(option='wrap') def on_wrap_update(self, value): for editor in self.editors: editor.toggle_wrap_mode(value) @on_conf_change(option='line_numbers') def on_line_numbers_update(self, value): for editor in self.editors: editor.toggle_line_numbers(value) @on_conf_change(option='selected', section='appearance') def on_color_scheme_change(self, value): for editor in self.editors: editor.set_color_scheme(value) # --- Public API # ------------------------------------------------------------------------ def update_font(self, font, color_scheme): """ Update font of the code editor. Parameters ---------- font: QFont Font object. color_scheme: str Name of the color scheme to use. """ self.color_scheme = color_scheme self.font = font for editor in self.editors: editor.set_font(font) editor.set_color_scheme(color_scheme) def move_tab(self, index_from, index_to): """ Move tab. Parameters ---------- index_from: int Move tab from this index. index_to: int Move tab to this index. Notes ----- Tabs themselves have already been moved by the history.tabwidget. """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) def get_filename_text(self, filename): """ Read and return content from filename. Parameters ---------- filename: str The file path to read. Returns ------- str Content of the filename. """ # Avoid a possible error when reading the history file try: text, _ = encoding.read(filename) except (IOError, OSError): text = "# Previous history could not be read from disk, sorry\n\n" text = normalize_eols(text) linebreaks = [m.start() for m in re.finditer('\n', text)] if len(linebreaks) > MAX_LINES: text = text[linebreaks[-MAX_LINES - 1] + 1:] # Avoid an error when trying to write the trimmed text to disk. # See spyder-ide/spyder#9093. try: encoding.write(text, filename) except (IOError, OSError): pass return text def add_history(self, filename): """ Create a history tab for `filename`. Parameters ---------- filename: str History filename. """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return # Widgets editor = SimpleCodeEditor(self) # Setup language = 'py' if osp.splitext(filename)[1] == '.py' else 'bat' editor.setup_editor( linenumbers=self.get_conf('line_numbers'), language=language, color_scheme=self.get_conf('selected', section='appearance'), font=self.font, wrap=self.get_conf('wrap'), ) editor.setReadOnly(True) editor.set_text(self.get_filename_text(filename)) editor.set_cursor_position('eof') self.find_widget.set_editor(editor) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.filenames.append(filename) self.editors.append(editor) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, filename) # Signals editor.sig_focus_changed.connect(lambda: self.sig_focus_changed.emit()) @Slot(str, str) def append_to_history(self, filename, command): """ Append command to history tab. Parameters ---------- filename: str History file. command: str Command to append to history file. """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') index = self.filenames.index(filename) command = to_text_string(command) self.editors[index].append(command) if self.get_conf('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) def refresh(self): """Refresh widget and update find widget on current editor.""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) @property def _tabs_stylesheet(self): """ Change style of tabs because we don't have a close button here. """ tabs_stylesheet = PANES_TABBAR_STYLESHEET.get_copy() css = tabs_stylesheet.get_stylesheet() css['QTabBar::tab'].setValues( marginTop='1.0em', padding='4px' ) css['QTabWidget::left-corner'].setValues( left='0px', ) css['QTabWidget::right-corner'].setValues( right='0px' ) return str(tabs_stylesheet)
class HistoryLog(SpyderPluginWidget): """History log plugin.""" CONF_SECTION = 'historylog' CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() def __init__(self, parent): """Initialize plugin and create History main widget.""" SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.dockviewer = None self.wrap_action = None self.linenumbers_action = None self.editors = [] self.filenames = [] layout = QVBoxLayout() self.tabwidget = Tabs(self, self._plugin_actions) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Menu as corner widget self.tabwidget.setCornerWidget(self.options_button) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title.""" return _('History') def get_plugin_icon(self): """Return widget icon.""" return ima.icon('history') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def refresh_plugin(self): """Refresh tabwidget""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) def get_plugin_actions(self): """Return a list of actions related to plugin""" self.history_action = create_action(self, _("History..."), None, ima.icon('history'), _("Set history maximum entries"), triggered=self.change_history_depth) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked( self.get_option('wrap') ) self.linenumbers_action = create_action( self, _("Show line numbers"), toggled=self.toggle_line_numbers) self.linenumbers_action.setChecked(self.get_option('line_numbers')) menu_actions = [self.history_action, self.wrap_action, self.linenumbers_action] return menu_actions def on_first_registration(self): """Action to be performed on first plugin registration""" self.tabify(self.main.ipyconsole) def register_plugin(self): """Register plugin in Spyder's main window""" self.focus_changed.connect(self.main.plugin_focus_changed) self.add_dockwidget() # self.main.console.set_historylog(self) self.main.console.shell.refresh.connect(self.refresh_plugin) def update_font(self): """Update font from Preferences""" color_scheme = self.get_color_scheme() font = self.get_font() for editor in self.editors: editor.set_font(font, color_scheme) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" color_scheme_n = 'color_scheme_name' color_scheme_o = self.get_color_scheme() font_n = 'plugin_font' font_o = self.get_font() wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) self.wrap_action.setChecked(wrap_o) linenb_n = 'line_numbers' linenb_o = self.get_option(linenb_n) for editor in self.editors: if font_n in options: scs = color_scheme_o if color_scheme_n in options else None editor.set_font(font_o, scs) elif color_scheme_n in options: editor.set_color_scheme(color_scheme_o) if wrap_n in options: editor.toggle_wrap_mode(wrap_o) if linenb_n in options: editor.toggle_line_numbers(linenumbers=linenb_o, markers=False) #------ Private API -------------------------------------------------------- def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) #------ Public API --------------------------------------------------------- def add_history(self, filename): """ Add new history tab Slot for add_history signal emitted by shell instance """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == '.py': language = 'py' else: language = 'bat' editor.setup_editor(linenumbers=self.get_option('line_numbers'), language=language, scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) color_scheme = self.get_color_scheme() editor.set_font(self.get_font(), color_scheme) editor.toggle_wrap_mode(self.get_option('wrap')) # Avoid a possible error when reading the history file try: text, _ = encoding.read(filename) except (IOError, OSError): text = "# Previous history could not be read from disk, sorry\n\n" text = normalize_eols(text) linebreaks = [m.start() for m in re.finditer('\n', text)] maxNline = self.get_option('max_entries') if len(linebreaks) > maxNline: text = text[linebreaks[-maxNline - 1] + 1:] # Avoid an error when trying to write the trimmed text to # disk. # See issue 9093 try: encoding.write(text, filename) except (IOError, OSError): pass editor.set_text(text) editor.set_cursor_position('eof') self.editors.append(editor) self.filenames.append(filename) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) self.tabwidget.setCurrentIndex(index) @Slot(str, str) def append_to_history(self, filename, command): """ Append an entry to history filename Slot for append_to_history signal emitted by shell instance """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') command = to_text_string(command) index = self.filenames.index(filename) self.editors[index].append(command) if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) @Slot() def change_history_depth(self): "Change history max entries""" depth, valid = QInputDialog.getInt(self, _('History'), _('Maximum entries'), self.get_option('max_entries'), 10, 10000) if valid: self.set_option('max_entries', depth) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_wrap_mode(checked) self.set_option('wrap', checked) @Slot(bool) def toggle_line_numbers(self, checked): """Toggle line numbers.""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_line_numbers(linenumbers=checked, markers=False) self.set_option('line_numbers', checked)
class HistoryLog(SpyderPluginWidget): """ History log widget """ CONF_SECTION = "historylog" CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() def __init__(self, parent): self.tabwidget = None self.menu_actions = None self.dockviewer = None self.wrap_action = None self.editors = [] self.filenames = [] if PYQT5: SpyderPluginWidget.__init__(self, parent, main=parent) else: SpyderPluginWidget.__init__(self, parent) # Initialize plugin self.initialize_plugin() layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) if sys.platform == "darwin": tab_container = QWidget() tab_container.setObjectName("tab-container") tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Menu as corner widget options_button = create_toolbutton(self, text=_("Options"), icon=ima.icon("tooloptions")) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, self.menu_actions) options_button.setMenu(menu) self.tabwidget.setCornerWidget(options_button) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) # ------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("History log") def get_plugin_icon(self): """Return widget icon""" return ima.icon("history") def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh tabwidget""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) def get_plugin_actions(self): """Return a list of actions related to plugin""" history_action = create_action( self, _("History..."), None, ima.icon("history"), _("Set history maximum entries"), triggered=self.change_history_depth, ) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked(self.get_option("wrap")) self.menu_actions = [history_action, self.wrap_action] return self.menu_actions def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.extconsole, self) 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.console.set_historylog(self) self.main.console.shell.refresh.connect(self.refresh_plugin) def update_font(self): """Update font from Preferences""" color_scheme = self.get_color_scheme() font = self.get_plugin_font() for editor in self.editors: editor.set_font(font, color_scheme) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" color_scheme_n = "color_scheme_name" color_scheme_o = self.get_color_scheme() font_n = "plugin_font" font_o = self.get_plugin_font() wrap_n = "wrap" wrap_o = self.get_option(wrap_n) self.wrap_action.setChecked(wrap_o) for editor in self.editors: if font_n in options: scs = color_scheme_o if color_scheme_n in options else None editor.set_font(font_o, scs) elif color_scheme_n in options: editor.set_color_scheme(color_scheme_o) if wrap_n in options: editor.toggle_wrap_mode(wrap_o) # ------ Private API -------------------------------------------------------- def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) # ------ Public API --------------------------------------------------------- def add_history(self, filename): """ Add new history tab Slot for add_history signal emitted by shell instance """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == ".py": language = "py" else: language = "bat" editor.setup_editor(linenumbers=False, language=language, scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) color_scheme = self.get_color_scheme() editor.set_font(self.get_plugin_font(), color_scheme) editor.toggle_wrap_mode(self.get_option("wrap")) text, _ = encoding.read(filename) editor.set_text(text) editor.set_cursor_position("eof") self.editors.append(editor) self.filenames.append(filename) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) self.tabwidget.setCurrentIndex(index) def append_to_history(self, filename, command): """ Append an entry to history filename Slot for append_to_history signal emitted by shell instance """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), "utf-8") command = to_text_string(command) index = self.filenames.index(filename) self.editors[index].append(command) if self.get_option("go_to_eof"): self.editors[index].set_cursor_position("eof") self.tabwidget.setCurrentIndex(index) @Slot() def change_history_depth(self): "Change history max entries" "" depth, valid = QInputDialog.getInt( self, _("History"), _("Maximum entries"), self.get_option("max_entries"), 10, 10000 ) if valid: self.set_option("max_entries", depth) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_wrap_mode(checked) self.set_option("wrap", checked)
class HistoryLog(SpyderPluginWidget): """History log plugin.""" CONF_SECTION = 'historylog' CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() def __init__(self, parent): """Initialize plugin and create History main widget.""" SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.dockviewer = None self.wrap_action = None self.linenumbers_action = None self.editors = [] self.filenames = [] # Initialize plugin actions, toolbutton and general signals self.initialize_plugin() layout = QVBoxLayout() self.tabwidget = Tabs(self, self.plugin_actions) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Menu as corner widget self.tabwidget.setCornerWidget(self.options_button) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title.""" return _('History log') def get_plugin_icon(self): """Return widget icon.""" return ima.icon('history') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh tabwidget""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) def get_plugin_actions(self): """Return a list of actions related to plugin""" self.history_action = create_action(self, _("History..."), None, ima.icon('history'), _("Set history maximum entries"), triggered=self.change_history_depth) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked( self.get_option('wrap') ) self.linenumbers_action = create_action( self, _("Show line numbers"), toggled=self.toggle_line_numbers) self.linenumbers_action.setChecked(self.get_option('line_numbers')) menu_actions = [self.history_action, self.wrap_action, self.linenumbers_action] return menu_actions def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.ipyconsole, self) 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.console.set_historylog(self) self.main.console.shell.refresh.connect(self.refresh_plugin) def update_font(self): """Update font from Preferences""" color_scheme = self.get_color_scheme() font = self.get_plugin_font() for editor in self.editors: editor.set_font(font, color_scheme) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" color_scheme_n = 'color_scheme_name' color_scheme_o = self.get_color_scheme() font_n = 'plugin_font' font_o = self.get_plugin_font() wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) self.wrap_action.setChecked(wrap_o) linenb_n = 'line_numbers' linenb_o = self.get_option(linenb_n) for editor in self.editors: if font_n in options: scs = color_scheme_o if color_scheme_n in options else None editor.set_font(font_o, scs) elif color_scheme_n in options: editor.set_color_scheme(color_scheme_o) if wrap_n in options: editor.toggle_wrap_mode(wrap_o) if linenb_n in options: editor.toggle_line_numbers(linenumbers=linenb_o, markers=False) #------ Private API -------------------------------------------------------- def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) #------ Public API --------------------------------------------------------- def add_history(self, filename): """ Add new history tab Slot for add_history signal emitted by shell instance """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == '.py': language = 'py' else: language = 'bat' editor.setup_editor(linenumbers=self.get_option('line_numbers'), language=language, scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) color_scheme = self.get_color_scheme() editor.set_font( self.get_plugin_font(), color_scheme ) editor.toggle_wrap_mode( self.get_option('wrap') ) text, _ = encoding.read(filename) text = normalize_eols(text) linebreaks = [m.start() for m in re.finditer('\n', text)] maxNline = self.get_option('max_entries') if len(linebreaks) > maxNline: text = text[linebreaks[-maxNline - 1] + 1:] encoding.write(text, filename) editor.set_text(text) editor.set_cursor_position('eof') self.editors.append(editor) self.filenames.append(filename) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) self.tabwidget.setCurrentIndex(index) @Slot(str, str) def append_to_history(self, filename, command): """ Append an entry to history filename Slot for append_to_history signal emitted by shell instance """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') command = to_text_string(command) index = self.filenames.index(filename) self.editors[index].append(command) if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) @Slot() def change_history_depth(self): "Change history max entries""" depth, valid = QInputDialog.getInt(self, _('History'), _('Maximum entries'), self.get_option('max_entries'), 10, 10000) if valid: self.set_option('max_entries', depth) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_wrap_mode(checked) self.set_option('wrap', checked) @Slot(bool) def toggle_line_numbers(self, checked): """Toggle line numbers.""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_line_numbers(linenumbers=checked, markers=False) self.set_option('line_numbers', checked)
class History(QWidget): """History plugin main widget.""" focus_changed = Signal() def __init__(self, parent): """Initialize widget and create layout.""" QWidget.__init__(self, parent) self.editors = [] self.filenames = [] self.tabwidget = None self.menu_actions = None layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) self.tabwidget.currentChanged.connect(self.refresh) self.tabwidget.move_data.connect(self.move_tab) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Menu as corner widget options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) options_button.setPopupMode(QToolButton.InstantPopup) self.menu = QMenu(self) options_button.setMenu(self.menu) self.tabwidget.setCornerWidget(options_button) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() layout.addWidget(self.find_widget) self.setLayout(layout) def set_menu_actions(self, menu_actions): """Add options to corner menu.""" if self.menu_actions is not None: add_actions(self.menu, self.menu_actions) def refresh(self): """Refresh tabwidget.""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) def move_tab(self, index_from, index_to): """ Move tab. (tabs themselves have already been moved by the history.tabwidget) """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) def add_history(self, filename, color_scheme, font, wrap): """ Add new history tab. Args: filename (str): file to be loaded in a new tab. """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == '.py': language = 'py' else: language = 'bat' editor.setup_editor(linenumbers=False, language=language, scrollflagarea=False, show_class_func_dropdown=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) editor.set_font(font, color_scheme) editor.toggle_wrap_mode(wrap) text, _ = encoding.read(filename) editor.set_text(text) editor.set_cursor_position('eof') self.editors.append(editor) self.filenames.append(filename) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) self.tabwidget.setCurrentIndex(index) def append_to_history(self, filename, command, go_to_eof): """ Append an entry to history filename. Args: filename (str): file to be updated in a new tab. command (str): line to be added. go_to_eof (bool): scroll to the end of file. """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') command = to_text_string(command) index = self.filenames.index(filename) self.editors[index].append(command) if go_to_eof: self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index)
class HistoryWidget(PluginMainWidget): """ History plugin main widget. """ DEFAULT_OPTIONS = { 'color_scheme_name': 'spyder/dark', 'font': QFont(), 'go_to_eof': True, 'line_numbers': True, 'max_entries': 100, 'wrap': True, } # Signals sig_focus_changed = Signal() """ This signal is emitted when the focus of the code editor storing history changes. """ def __init__(self, name, plugin, parent, options=DEFAULT_OPTIONS): super().__init__(name, plugin, parent, options) # Attributes self.editors = [] self.filenames = [] self.tabwidget = None self.dockviewer = None self.wrap_action = None self.linenumbers_action = None self.editors = [] self.filenames = [] # Widgets self.tabwidget = Tabs(self) self.find_widget = FindReplace(self) # Setup self.find_widget.hide() # Layout layout = QVBoxLayout() # TODO: Move this to the tab container directly if sys.platform == 'darwin': tab_container = QWidget(self) tab_container.setObjectName('tab-container') tab_layout = QVBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) layout.addWidget(self.find_widget) self.setLayout(layout) # Signals self.tabwidget.currentChanged.connect(self.refresh) self.tabwidget.move_data.connect(self.move_tab) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('History') def get_focus_widget(self): return self.tabwidget.currentWidget() def setup(self, options): # Actions self.history_action = self.create_action( HistoryWidgetActions.MaximumHistoryEntries, text=_("History..."), tip=_("Set history maximum entries"), icon=self.create_icon('history'), triggered=self.change_history_depth, ) self.wrap_action = self.create_action( HistoryWidgetActions.ToggleWrap, text=_("Wrap lines"), toggled=lambda value: self.set_option('wrap', value), initial=self.get_option('wrap'), ) self.linenumbers_action = self.create_action( HistoryWidgetActions.ToggleLineNumbers, text=_("Show line numbers"), toggled=lambda value: self.set_option('line_numbers', value), initial=self.get_option('line_numbers'), ) # Menu menu = self.get_options_menu() for item in [ self.history_action, self.wrap_action, self.linenumbers_action ]: self.add_item_to_menu( item, menu=menu, section=HistoryWidgetOptionsMenuSections.Main, ) def update_actions(self): pass def on_option_update(self, option, value): if self.tabwidget is not None: if option == 'wrap': for editor in self.editors: editor.toggle_wrap_mode(value) elif option == 'line_numbers': for editor in self.editors: editor.toggle_line_numbers( linenumbers=value, markers=False, ) elif option == 'color_scheme_name': for editor in self.editors: editor.set_font(self.get_option('font'), value) # --- Public API # ------------------------------------------------------------------------ def update_font(self, font, color_scheme): """ Update font of the code editor. Parameters ---------- font: QFont Font object. color_scheme: str Name of the color scheme to use. """ self.font = font self.color_scheme = color_scheme for editor in self.editors: editor.set_font(font, color_scheme) def move_tab(self, index_from, index_to): """ Move tab. Parameters ---------- index_from: int Move tab from this index. index_to: int Move tab to this index. Notes ----- Tabs themselves have already been moved by the history.tabwidget. """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) def get_filename_text(self, filename): """ Read and return content from filename. Parameters ---------- filename: str The file path to read. Returns ------- str Content of the filename. """ # Avoid a possible error when reading the history file try: text, _ = encoding.read(filename) except (IOError, OSError): text = "# Previous history could not be read from disk, sorry\n\n" text = normalize_eols(text) linebreaks = [m.start() for m in re.finditer('\n', text)] maxNline = self.get_option('max_entries') if len(linebreaks) > maxNline: text = text[linebreaks[-maxNline - 1] + 1:] # Avoid an error when trying to write the trimmed text to disk. # See spyder-ide/spyder#9093. try: encoding.write(text, filename) except (IOError, OSError): pass return text def add_history(self, filename): """ Create a history tab for `filename`. Parameters ---------- filename: str History filename. """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return # Widgets editor = codeeditor.CodeEditor(self) # Setup language = 'py' if osp.splitext(filename)[1] == '.py' else 'bat' editor.setup_editor( linenumbers=self.get_option('line_numbers'), language=language, scrollflagarea=False, show_debug_panel=False, ) editor.setReadOnly(True) editor.toggle_wrap_mode(self.get_option('wrap')) editor.set_text(self.get_filename_text(filename)) editor.set_cursor_position('eof') editor.set_font( self.get_option('font'), self.get_option('color_scheme_name'), ) self.find_widget.set_editor(editor) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.filenames.append(filename) self.editors.append(editor) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, filename) # Signals editor.focus_changed.connect(lambda: self.sig_focus_changed.emit()) @Slot(str, str) def append_to_history(self, filename, command): """ Append command to history tab. Parameters ---------- filename: str History file. command: str Command to append to histroy file. """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') index = self.filenames.index(filename) command = to_text_string(command) self.editors[index].append(command) if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) @Slot() @Slot(int) def change_history_depth(self, depth=None): """ Change history max entries. Parameters ---------- depth: int, optional Number of entries to use for the history. If None, an input dialog will be used. Default is None. """ valid = True if depth is None: depth, valid = QInputDialog.getInt( self, _('History'), _('Maximum entries'), self.get_option('max_entries'), 10, 10000, ) if valid: self.set_option('max_entries', depth) def refresh(self): """Refresh widget and update find widget on current editor.""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor)
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)
class ExternalConsole(SpyderPluginWidget): """ Console widget """ CONF_SECTION = 'console' CONFIGWIDGET_CLASS = ExternalConsoleConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False edit_goto = Signal((str, int, str), (str, int, str, bool)) focus_changed = Signal() redirect_stdio = Signal(bool) def __init__(self, parent): if PYQT5: SpyderPluginWidget.__init__(self, parent, main = parent) else: SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.menu_actions = None self.help = None # Help plugin self.historylog = None # History log plugin self.variableexplorer = None # Variable explorer plugin self.python_count = 0 self.terminal_count = 0 # Python startup file selection if not osp.isfile(self.get_option('pythonstartup', '')): self.set_option('pythonstartup', SCIENTIFIC_STARTUP) # default/custom settings are mutually exclusive: self.set_option('pythonstartup/custom', not self.get_option('pythonstartup/default')) self.shellwidgets = [] self.filenames = [] self.icons = [] self.runfile_args = "" # Initialize plugin self.initialize_plugin() layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) 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.main.sig_pythonpath_changed.connect(self.set_path) self.tabwidget.set_close_function(self.close_console) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) # Accepting drops self.setAcceptDrops(True) def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) shell = self.shellwidgets.pop(index_from) icons = self.icons.pop(index_from) self.filenames.insert(index_to, filename) self.shellwidgets.insert(index_to, shell) self.icons.insert(index_to, icons) self.update_plugin_title.emit() def get_shell_index_from_id(self, shell_id): """Return shellwidget index from id""" for index, shell in enumerate(self.shellwidgets): if id(shell) == shell_id: return index def close_console(self, index=None): """Close console tab from index or widget (or close current tab)""" # Get tab index if not self.tabwidget.count(): return if index is None: index = self.tabwidget.currentIndex() # Closing logic self.tabwidget.widget(index).close() self.tabwidget.removeTab(index) self.filenames.pop(index) self.shellwidgets.pop(index) self.icons.pop(index) self.update_plugin_title.emit() def set_variableexplorer(self, variableexplorer): """Set variable explorer plugin""" self.variableexplorer = variableexplorer def set_path(self): """Set consoles PYTHONPATH if changed by the user""" from spyder.widgets.externalshell import pythonshell for sw in self.shellwidgets: if isinstance(sw, pythonshell.ExternalPythonShell): if sw.is_interpreter and sw.is_running(): sw.path = self.main.get_spyder_pythonpath() sw.shell.path = sw.path def __find_python_shell(self, interpreter_only=False): current_index = self.tabwidget.currentIndex() if current_index == -1: return from spyder.widgets.externalshell import pythonshell for index in [current_index]+list(range(self.tabwidget.count())): shellwidget = self.tabwidget.widget(index) if isinstance(shellwidget, pythonshell.ExternalPythonShell): if interpreter_only and not shellwidget.is_interpreter: continue elif not shellwidget.is_running(): continue else: self.tabwidget.setCurrentIndex(index) return shellwidget def get_current_shell(self): """ Called by Help to retrieve the current shell instance """ shellwidget = self.__find_python_shell() return shellwidget.shell def get_running_python_shell(self): """ Called by Help to retrieve a running Python shell instance """ current_index = self.tabwidget.currentIndex() if current_index == -1: return from spyder.widgets.externalshell import pythonshell shellwidgets = [self.tabwidget.widget(index) for index in range(self.tabwidget.count())] shellwidgets = [_w for _w in shellwidgets if isinstance(_w, pythonshell.ExternalPythonShell) \ and _w.is_running()] if shellwidgets: # First, iterate on interpreters only: for shellwidget in shellwidgets: if shellwidget.is_interpreter: return shellwidget.shell else: return shellwidgets[0].shell def run_script_in_current_shell(self, filename, wdir, args, debug, post_mortem): """Run script in current shell, if any""" norm = lambda text: remove_backslashes(to_text_string(text)) line = "%s('%s'" % ('debugfile' if debug else 'runfile', norm(filename)) if args: line += ", args='%s'" % norm(args) if wdir: line += ", wdir='%s'" % norm(wdir) if post_mortem: line += ', post_mortem=True' line += ")" if not self.execute_code(line, interpreter_only=True): QMessageBox.warning(self, _('Warning'), _("No Python console is currently selected to run <b>%s</b>." "<br><br>Please select or open a new Python console " "and try again." ) % osp.basename(norm(filename)), QMessageBox.Ok) else: self.visibility_changed(True) self.raise_() def set_current_shell_working_directory(self, directory): """Set current shell working directory""" shellwidget = self.__find_python_shell() if shellwidget is not None: directory = encoding.to_unicode_from_fs(directory) shellwidget.shell.set_cwd(directory) def execute_code(self, lines, interpreter_only=False): """Execute code in an already opened Python interpreter""" shellwidget = self.__find_python_shell( interpreter_only=interpreter_only) if shellwidget is not None: shellwidget.shell.execute_lines(to_text_string(lines)) self.activateWindow() shellwidget.shell.setFocus() return True else: return False def pdb_has_stopped(self, fname, lineno, shellwidget): """Python debugger has just stopped at frame (fname, lineno)""" # This is a unique form of the edit_goto signal that is intended to # prevent keyboard input from accidentally entering the editor # during repeated, rapid entry of debugging commands. self.edit_goto[str, int, str, bool].emit(fname, lineno, '', False) self.activateWindow() shellwidget.shell.setFocus() def set_spyder_breakpoints(self): """Set all Spyder breakpoints into all shells""" for shellwidget in self.shellwidgets: shellwidget.shell.set_spyder_breakpoints() def start(self, fname, wdir=None, args='', interact=False, debug=False, python=True, python_args='', post_mortem=True): """ Start new console fname: string: filename of script to run None: open an interpreter wdir: working directory args: command line options of the Python script interact: inspect script interactively after its execution debug: run pdb python: True: Python interpreter, False: terminal python_args: additionnal Python interpreter command line options (option "-u" is mandatory, see widgets.externalshell package) """ # Note: fname is None <=> Python interpreter if fname is not None and not is_text_string(fname): fname = to_text_string(fname) if wdir is not None and not is_text_string(wdir): wdir = to_text_string(wdir) if fname is not None and fname in self.filenames: index = self.filenames.index(fname) if self.get_option('single_tab'): old_shell = self.shellwidgets[index] if old_shell.is_running(): runconfig = get_run_configuration(fname) if runconfig is None or runconfig.show_kill_warning: answer = QMessageBox.question(self, self.get_plugin_title(), _("%s is already running in a separate process.\n" "Do you want to kill the process before starting " "a new one?") % osp.basename(fname), QMessageBox.Yes | QMessageBox.Cancel) else: answer = QMessageBox.Yes if answer == QMessageBox.Yes: old_shell.process.kill() old_shell.process.waitForFinished() else: return self.close_console(index) else: index = self.tabwidget.count() # Creating a new external shell pythonpath = self.main.get_spyder_pythonpath() light_background = self.get_option('light_background') show_elapsed_time = self.get_option('show_elapsed_time') if python: if CONF.get('main_interpreter', 'default'): pythonexecutable = get_python_executable() external_interpreter = False else: pythonexecutable = CONF.get('main_interpreter', 'executable') external_interpreter = True if self.get_option('pythonstartup/default'): pythonstartup = None else: pythonstartup = self.get_option('pythonstartup', None) monitor_enabled = self.get_option('monitor/enabled') mpl_backend = self.get_option('matplotlib/backend/value') ets_backend = self.get_option('ets_backend') qt_api = self.get_option('qt/api') if qt_api not in ('pyqt', 'pyside', 'pyqt5'): qt_api = None merge_output_channels = self.get_option('merge_output_channels') colorize_sys_stderr = self.get_option('colorize_sys_stderr') umr_enabled = CONF.get('main_interpreter', 'umr/enabled') umr_namelist = CONF.get('main_interpreter', 'umr/namelist') umr_verbose = CONF.get('main_interpreter', 'umr/verbose') ar_timeout = CONF.get('variable_explorer', 'autorefresh/timeout') ar_state = CONF.get('variable_explorer', 'autorefresh') sa_settings = None shellwidget = ExternalPythonShell(self, fname, wdir, interact, debug, post_mortem=post_mortem, path=pythonpath, python_args=python_args, arguments=args, stand_alone=sa_settings, pythonstartup=pythonstartup, pythonexecutable=pythonexecutable, external_interpreter=external_interpreter, umr_enabled=umr_enabled, umr_namelist=umr_namelist, umr_verbose=umr_verbose, ets_backend=ets_backend, monitor_enabled=monitor_enabled, mpl_backend=mpl_backend, qt_api=qt_api, merge_output_channels=merge_output_channels, colorize_sys_stderr=colorize_sys_stderr, autorefresh_timeout=ar_timeout, autorefresh_state=ar_state, light_background=light_background, menu_actions=self.menu_actions, show_buttons_inside=False, show_elapsed_time=show_elapsed_time) shellwidget.sig_pdb.connect( lambda fname, lineno, shellwidget=shellwidget: self.pdb_has_stopped(fname, lineno, shellwidget)) self.register_widget_shortcuts(shellwidget.shell) else: if os.name == 'posix': cmd = 'gnome-terminal' args = [] if programs.is_program_installed(cmd): if wdir: args.extend(['--working-directory=%s' % wdir]) programs.run_program(cmd, args) return cmd = 'konsole' if programs.is_program_installed(cmd): if wdir: args.extend(['--workdir', wdir]) programs.run_program(cmd, args) return shellwidget = ExternalSystemShell(self, wdir, path=pythonpath, light_background=light_background, menu_actions=self.menu_actions, show_buttons_inside=False, show_elapsed_time=show_elapsed_time) # Code completion / calltips shellwidget.shell.setMaximumBlockCount( self.get_option('max_line_count') ) shellwidget.shell.set_font( self.get_plugin_font() ) shellwidget.shell.toggle_wrap_mode( self.get_option('wrap') ) shellwidget.shell.set_calltips( self.get_option('calltips') ) shellwidget.shell.set_codecompletion_auto( self.get_option('codecompletion/auto') ) shellwidget.shell.set_codecompletion_case( self.get_option('codecompletion/case_sensitive') ) shellwidget.shell.set_codecompletion_enter( self.get_option('codecompletion/enter_key') ) if python and self.help is not None: shellwidget.shell.set_help(self.help) shellwidget.shell.set_help_enabled( CONF.get('help', 'connect/python_console')) if self.historylog is not None: self.historylog.add_history(shellwidget.shell.history_filename) shellwidget.shell.append_to_history.connect( self.historylog.append_to_history) shellwidget.shell.go_to_error.connect(self.go_to_error) shellwidget.shell.focus_changed.connect( lambda: self.focus_changed.emit()) if python: if self.main.editor is not None: shellwidget.open_file.connect(self.open_file_in_spyder) if fname is None: self.python_count += 1 tab_name = "Python %d" % self.python_count tab_icon1 = ima.icon('python') tab_icon2 = ima.icon('python_t') else: tab_name = osp.basename(fname) tab_icon1 = ima.icon('run') tab_icon2 = ima.icon('terminated') else: fname = id(shellwidget) if os.name == 'nt': tab_name = _("Command Window") else: tab_name = _("Terminal") self.terminal_count += 1 tab_name += (" %d" % self.terminal_count) tab_icon1 = ima.icon('cmdprompt') tab_icon2 = ima.icon('cmdprompt_t') self.shellwidgets.insert(index, shellwidget) self.filenames.insert(index, fname) self.icons.insert(index, (tab_icon1, tab_icon2)) if index is None: index = self.tabwidget.addTab(shellwidget, tab_name) else: self.tabwidget.insertTab(index, shellwidget, tab_name) shellwidget.started.connect( lambda sid=id(shellwidget): self.process_started(sid)) shellwidget.sig_finished.connect( lambda sid=id(shellwidget): self.process_finished(sid)) self.find_widget.set_editor(shellwidget.shell) self.tabwidget.setTabToolTip(index, fname if wdir is None else wdir) self.tabwidget.setCurrentIndex(index) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() shellwidget.set_icontext_visible(self.get_option('show_icontext')) # Start process and give focus to console shellwidget.start_shell() def open_file_in_spyder(self, fname, lineno): """Open file in Spyder's editor from remote process""" self.main.editor.activateWindow() self.main.editor.raise_() self.main.editor.load(fname, lineno) #------ Private API ------------------------------------------------------- def process_started(self, shell_id): index = self.get_shell_index_from_id(shell_id) shell = self.shellwidgets[index] icon, _icon = self.icons[index] self.tabwidget.setTabIcon(index, icon) if self.help is not None: self.help.set_shell(shell.shell) if self.variableexplorer is not None: self.variableexplorer.add_shellwidget(shell) def process_finished(self, shell_id): index = self.get_shell_index_from_id(shell_id) if index is not None: # Not sure why it happens, but sometimes the shellwidget has # already been removed, so that's not bad if we can't change # the tab icon... _icon, icon = self.icons[index] self.tabwidget.setTabIcon(index, icon) if self.variableexplorer is not None: self.variableexplorer.remove_shellwidget(shell_id) #------ SpyderPluginWidget API -------------------------------------------- def get_plugin_title(self): """Return widget title""" title = _('Python console') if self.filenames: index = self.tabwidget.currentIndex() fname = self.filenames[index] if fname: title += ' - ' + to_text_string(fname) return title def get_plugin_icon(self): """Return widget icon""" return ima.icon('console') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def get_plugin_actions(self): """Return a list of actions related to plugin""" interpreter_action = create_action(self, _("Open a &Python console"), None, ima.icon('python'), triggered=self.open_interpreter) if os.name == 'nt': text = _("Open &command prompt") tip = _("Open a Windows command prompt") else: text = _("Open a &terminal") tip = _("Open a terminal window") terminal_action = create_action(self, text, None, None, tip, triggered=self.open_terminal) run_action = create_action(self, _("&Run..."), None, ima.icon('run_small'), _("Run a Python script"), triggered=self.run_script) consoles_menu_actions = [interpreter_action] tools_menu_actions = [terminal_action] self.menu_actions = [interpreter_action, terminal_action, run_action] self.main.consoles_menu_actions += consoles_menu_actions self.main.tools_menu_actions += tools_menu_actions return self.menu_actions+consoles_menu_actions+tools_menu_actions def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) self.help = self.main.help self.historylog = self.main.historylog self.edit_goto.connect(self.main.editor.load) self.edit_goto[str, int, str, bool].connect( lambda fname, lineno, word, processevents: self.main.editor.load(fname, lineno, word, processevents=processevents)) self.main.editor.run_in_current_extconsole.connect( self.run_script_in_current_shell) self.main.editor.breakpoints_saved.connect( self.set_spyder_breakpoints) self.main.editor.open_dir.connect( self.set_current_shell_working_directory) self.main.workingdirectory.set_current_console_wd.connect( self.set_current_shell_working_directory) self.focus_changed.connect( self.main.plugin_focus_changed) self.redirect_stdio.connect( self.main.redirect_internalshell_stdio) expl = self.main.explorer if expl is not None: expl.open_terminal.connect(self.open_terminal) expl.open_interpreter.connect(self.open_interpreter) pexpl = self.main.projects if pexpl is not None: pexpl.open_terminal.connect(self.open_terminal) pexpl.open_interpreter.connect(self.open_interpreter) def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" for shellwidget in self.shellwidgets: shellwidget.close() return True def restart(self): """ Restart the console This is needed when we switch project to update PYTHONPATH and the selected interpreter """ self.python_count = 0 for i in range(len(self.shellwidgets)): self.close_console() self.open_interpreter() def refresh_plugin(self): """Refresh tabwidget""" shellwidget = None if self.tabwidget.count(): shellwidget = self.tabwidget.currentWidget() editor = shellwidget.shell editor.setFocus() widgets = [shellwidget.create_time_label(), 5 ]+shellwidget.get_toolbar_buttons()+[5] else: editor = None widgets = [] self.find_widget.set_editor(editor) self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets}) if shellwidget: shellwidget.update_time_label_visibility() self.variableexplorer.set_shellwidget_from_id(id(shellwidget)) self.help.set_shell(shellwidget.shell) self.main.last_console_plugin_focus_was_python = True self.update_plugin_title.emit() def update_font(self): """Update font from Preferences""" font = self.get_plugin_font() for shellwidget in self.shellwidgets: shellwidget.shell.set_font(font) completion_size = CONF.get('main', 'completion/size') comp_widget = shellwidget.shell.completion_widget comp_widget.setup_appearance(completion_size, font) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" showtime_n = 'show_elapsed_time' showtime_o = self.get_option(showtime_n) icontext_n = 'show_icontext' icontext_o = self.get_option(icontext_n) calltips_n = 'calltips' calltips_o = self.get_option(calltips_n) help_n = 'connect_to_oi' help_o = CONF.get('help', 'connect/python_console') wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) compauto_n = 'codecompletion/auto' compauto_o = self.get_option(compauto_n) case_comp_n = 'codecompletion/case_sensitive' case_comp_o = self.get_option(case_comp_n) compenter_n = 'codecompletion/enter_key' compenter_o = self.get_option(compenter_n) mlc_n = 'max_line_count' mlc_o = self.get_option(mlc_n) for shellwidget in self.shellwidgets: if showtime_n in options: shellwidget.set_elapsed_time_visible(showtime_o) if icontext_n in options: shellwidget.set_icontext_visible(icontext_o) if calltips_n in options: shellwidget.shell.set_calltips(calltips_o) if help_n in options: if isinstance(shellwidget, ExternalPythonShell): shellwidget.shell.set_help_enabled(help_o) if wrap_n in options: shellwidget.shell.toggle_wrap_mode(wrap_o) if compauto_n in options: shellwidget.shell.set_codecompletion_auto(compauto_o) if case_comp_n in options: shellwidget.shell.set_codecompletion_case(case_comp_o) if compenter_n in options: shellwidget.shell.set_codecompletion_enter(compenter_o) if mlc_n in options: shellwidget.shell.setMaximumBlockCount(mlc_o) #------ SpyderPluginMixin API --------------------------------------------- def toggle_view(self, checked): """Toggle view""" if checked: self.dockwidget.show() self.dockwidget.raise_() # Start a console in case there are none shown from spyder.widgets.externalshell import pythonshell consoles = None for sw in self.shellwidgets: if isinstance(sw, pythonshell.ExternalPythonShell): consoles = True break if not consoles: self.open_interpreter() else: self.dockwidget.hide() #------ Public API --------------------------------------------------------- @Slot(bool) @Slot(str) def open_interpreter(self, wdir=None): """Open interpreter""" if not wdir: wdir = getcwd() self.visibility_changed(True) self.start(fname=None, wdir=to_text_string(wdir), args='', interact=True, debug=False, python=True) @Slot(bool) @Slot(str) def open_terminal(self, wdir=None): """Open terminal""" if not wdir: wdir = getcwd() self.start(fname=None, wdir=to_text_string(wdir), args='', interact=True, debug=False, python=False) @Slot() def run_script(self): """Run a Python script""" self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename(self, _("Run Python script"), getcwd(), _("Python scripts")+" (*.py ; *.pyw ; *.ipy)") self.redirect_stdio.emit(True) if filename: self.start(fname=filename, wdir=None, args='', interact=False, debug=False) def go_to_error(self, text): """Go to error if relevant""" match = get_error_match(to_text_string(text)) if match: fname, lnb = match.groups() self.edit_goto.emit(osp.abspath(fname), int(lnb), '') #----Drag and drop def dragEnterEvent(self, event): """Reimplement Qt method Inform Qt about the types of data that the widget accepts""" source = event.mimeData() if source.hasUrls(): if mimedata2url(source): pathlist = mimedata2url(source) shellwidget = self.tabwidget.currentWidget() if all([is_python_script(to_text_string(qstr)) for qstr in pathlist]): event.acceptProposedAction() elif shellwidget is None or not shellwidget.is_running(): event.ignore() else: event.acceptProposedAction() else: event.ignore() elif source.hasText(): event.acceptProposedAction() def dropEvent(self, event): """Reimplement Qt method Unpack dropped data and handle it""" source = event.mimeData() shellwidget = self.tabwidget.currentWidget() if source.hasText(): qstr = source.text() if is_python_script(to_text_string(qstr)): self.start(qstr) elif shellwidget: shellwidget.shell.insert_text(qstr) elif source.hasUrls(): pathlist = mimedata2url(source) if all([is_python_script(to_text_string(qstr)) for qstr in pathlist]): for fname in pathlist: self.start(fname) elif shellwidget: shellwidget.shell.drop_pathlist(pathlist) event.acceptProposedAction()