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 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 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 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() self.update_tabs_text() 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: if PYQT5: 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: mb = QMessageBox(self) answer = mb.question(mb, 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') self.filenames.insert(index, fname) else: self.filenames.insert(index, fname) tab_name = self.get_tab_text(fname) self.update_tabs_text() 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.filenames.insert(index, fname) self.shellwidgets.insert(index, shellwidget) 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) def get_tab_text(self, fname): """Get tab text without ambiguation.""" files_path_list = [filename for filename in self.filenames if filename is not None] return sourcecode.get_file_title(files_path_list, fname) def update_tabs_text(self): """Update the text from the tabs.""" for index, fname in enumerate(self.filenames): if fname is not None: self.tabwidget.setTabText(index, self.get_tab_text(fname)) #------ 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') 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.""" 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 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.editors = [] self.filenames = [] self.font = None # 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): # 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_font(self.font) # --- 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)
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) # 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')) self.menu = QMenu(self) menu_btn.setMenu(self.menu) menu_btn.setPopupMode(menu_btn.InstantPopup) add_actions(self.menu, self.menu_actions) corner_widgets = {Qt.TopRightCorner: [new_notebook_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_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 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) 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.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, save_as_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.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 WEBENGINE: message = _("You are working with Qt4 or the PyQt5 wheels." "<br><br>In order to make the Notebook plugin " "work you need to update to Qt5 and/or use " "Anaconda or Miniconda.") 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) 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.")) return 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() 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) if not save: 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) client.shutdown_kernel() client.close() # Note: notebook index may have changed after closing related widgets self.tabwidget.removeTab(self.tabwidget.indexOf(client)) self.clients.remove(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) # ------ 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 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 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 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 HistoryLog(SpyderPluginWidget): """History log plugin.""" CONF_SECTION = 'historylog' CONFIGWIDGET_CLASS = HistoryConfigPage CONF_FILE = False 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.sig_refreshed.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 spyder-ide/spyder#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 TerminalPlugin(SpyderPluginWidget): """Terminal plugin.""" URL_ISSUES = ' https://github.com/spyder-ide/spyder-terminal/issues' CONF_SECTION = CONF_SECTION CONFIGWIDGET_CLASS = TerminalConfigPage CONF_DEFAULTS = CONF_DEFAULTS # CONF_VERSION = CONF_VERSION 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 = find_program(self.get_option('shell')) self.CONF = CONF 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.options_menu = QMenu(self) try: # Spyder 3 self.initialize_plugin() except AttributeError: # Spyder 4 pass layout = QVBoxLayout() new_term_btn = create_toolbutton(self, icon=ima.icon('expand'), tip=_('Open a new terminal'), triggered=self.create_new_term) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), tip=_('Options')) menu_btn.setMenu(self.options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) # 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.options_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_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('DollarFileIcon') 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) add_actions(self.options_menu, [new_terminal_cwd, self.new_terminal_project, new_terminal_file, rename_tab_action]) 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.add_dockwidget() 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.options_menu.aboutToShow.connect(self.setup_menu_actions) def apply_plugin_settings(self, options): """Apply the config settings.""" term_options = {} for option in options: term_options[option] = self.get_option(option) for term in self.get_terms(): term.apply_settings(term_options) # ------ Public API (for terminals) ------------------------- def get_terms(self): """Return terminal list.""" return [cl for cl in self.terms if isinstance(cl, TerminalWidget)] def get_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_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 NotebookPlugin(SpyderPluginWidget): """IPython Notebook plugin.""" CONF_SECTION = 'notebook' focus_changed = Signal() def __init__(self, parent, testing=False): """Constructor.""" if testing: self.CONF_FILE = False 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) layout = QVBoxLayout() new_notebook_btn = create_toolbutton(self, icon=ima.icon('options_more'), 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) 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.""" super(NotebookPlugin, self).register_plugin() self.focus_changed.connect(self.main.plugin_focus_changed) self.ipyconsole = self.main.ipyconsole self.create_new_client(give_focus=False) # Connect to switcher self.switcher = self.main.switcher self.switcher.sig_mode_selected.connect(self.handle_switcher_modes) self.switcher.sig_item_selected.connect(self.handle_switcher_selection) 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: self.set_option('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: self.switch_to_plugin() self.activateWindow() 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 handle_switcher_modes(self, mode): """ Populate switcher with opened notebooks. List the file names of the opened notebooks with their directories in the switcher. Only handle file mode, where `mode` is empty string. """ if mode != '': return paths = [client.get_filename() for client in self.clients] is_unsaved = [False for client in self.clients] short_paths = shorten_paths(paths, is_unsaved) icon = QIcon(os.path.join(PACKAGE_PATH, 'images', 'icon.svg')) section = self.get_plugin_title() for path, short_path, client in zip(paths, short_paths, self.clients): title = osp.basename(path) description = osp.dirname(path) if len(path) > 75: description = short_path is_last_item = (client == self.clients[-1]) self.switcher.add_item(title=title, description=description, icon=icon, section=section, data=client, last_item=is_last_item) def handle_switcher_selection(self, item, mode, search_text): """ Handle user selecting item in switcher. If the selected item is not in the section of the switcher that corresponds to this plugin, then ignore it. Otherwise, switch to selected item in notebook plugin and hide the switcher. """ if item.get_section() != self.get_plugin_title(): return client = item.get_data() index = self.clients.index(client) self.tabwidget.setCurrentIndex(index) self.switch_to_plugin() self.switcher.hide()
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()