class Console(SpyderPluginWidget): """ Console widget """ CONF_SECTION = 'internal_console' focus_changed = Signal() redirect_stdio = Signal(bool) edit_goto = Signal(str, int, str) def __init__(self, parent=None, namespace=None, commands=[], message=None, exitfunc=None, profile=False, multithreaded=False): SpyderPluginWidget.__init__(self, parent) logger.info("Initializing...") self.dialog_manager = DialogManager() # Shell light_background = self.get_option('light_background') self.shell = InternalShell(parent, namespace, commands, message, self.get_option('max_line_count'), self.get_plugin_font(), exitfunc, profile, multithreaded, light_background=light_background) self.shell.status.connect(lambda msg: self.show_message.emit(msg, 0)) self.shell.go_to_error.connect(self.go_to_error) self.shell.focus_changed.connect(lambda: self.focus_changed.emit()) # Redirecting some signals: self.shell.redirect_stdio.connect(lambda state: self.redirect_stdio.emit(state)) # Initialize plugin self.initialize_plugin() # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.set_editor(self.shell) self.find_widget.hide() self.register_widget_shortcuts(self.find_widget) # Main layout btn_layout = QHBoxLayout() btn_layout.setAlignment(Qt.AlignLeft) btn_layout.addStretch() btn_layout.addWidget(self.options_button, Qt.AlignRight) layout = create_plugin_layout(btn_layout) layout.addWidget(self.shell) layout.addWidget(self.find_widget) self.setLayout(layout) # Parameters self.shell.toggle_wrap_mode(self.get_option('wrap')) # Accepting drops self.setAcceptDrops(True) # Traceback MessageBox self.error_dlg = None self.error_traceback = "" self.dismiss_error = False #------ Private API -------------------------------------------------------- def set_historylog(self, historylog): """Bind historylog instance to this console Not used anymore since v2.0""" historylog.add_history(self.shell.history_filename) self.shell.append_to_history.connect(historylog.append_to_history) def set_help(self, help_plugin): """Bind help instance to this console""" self.shell.help = help_plugin #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Internal 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.shell def update_font(self): """Update font from Preferences""" font = self.get_plugin_font() self.shell.set_font(font) def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.dialog_manager.close_all() self.shell.exit_interpreter() return True def refresh_plugin(self): pass def get_plugin_actions(self): """Return a list of actions related to plugin""" quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), tip=_("Quit"), triggered=self.quit) self.register_shortcut(quit_action, "_", "Quit", "Ctrl+Q") run_action = create_action(self, _("&Run..."), None, ima.icon('run_small'), _("Run a Python script"), triggered=self.run_script) environ_action = create_action(self, _("Environment variables..."), icon=ima.icon('environ'), tip=_("Show and edit environment variables" " (for current session)"), triggered=self.show_env) syspath_action = create_action(self, _("Show sys.path contents..."), icon=ima.icon('syspath'), tip=_("Show (read-only) sys.path"), triggered=self.show_syspath) buffer_action = create_action(self, _("Buffer..."), None, tip=_("Set maximum line count"), triggered=self.change_max_line_count) exteditor_action = create_action(self, _("External editor path..."), None, None, _("Set external editor executable path"), triggered=self.change_exteditor) wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) wrap_action.setChecked(self.get_option('wrap')) calltips_action = create_action(self, _("Display balloon tips"), toggled=self.toggle_calltips) calltips_action.setChecked(self.get_option('calltips')) codecompletion_action = create_action(self, _("Automatic code completion"), toggled=self.toggle_codecompletion) codecompletion_action.setChecked(self.get_option('codecompletion/auto')) codecompenter_action = create_action(self, _("Enter key selects completion"), toggled=self.toggle_codecompletion_enter) codecompenter_action.setChecked(self.get_option( 'codecompletion/enter_key')) option_menu = QMenu(_('Internal console settings'), self) option_menu.setIcon(ima.icon('tooloptions')) add_actions(option_menu, (buffer_action, wrap_action, calltips_action, codecompletion_action, codecompenter_action, exteditor_action)) plugin_actions = [None, run_action, environ_action, syspath_action, option_menu, MENU_SEPARATOR, quit_action, self.undock_action] return plugin_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) # Connecting the following signal once the dockwidget has been created: self.shell.exception_occurred.connect(self.exception_occurred) def exception_occurred(self, text, is_traceback): """ Exception ocurred in the internal console. Show a QDialog or the internal console to warn the user. """ # Skip errors without traceback or dismiss if (not is_traceback and self.error_dlg is None) or self.dismiss_error: return if CONF.get('main', 'show_internal_errors'): if self.error_dlg is None: self.error_dlg = SpyderErrorDialog(self) self.error_dlg.close_btn.clicked.connect(self.close_error_dlg) self.error_dlg.rejected.connect(self.remove_error_dlg) self.error_dlg.details.go_to_error.connect(self.go_to_error) self.error_dlg.show() self.error_dlg.append_traceback(text) elif DEV or get_debug_level(): self.dockwidget.show() self.dockwidget.raise_() def close_error_dlg(self): """Close error dialog.""" if self.error_dlg.dismiss_box.isChecked(): self.dismiss_error = True self.error_dlg.reject() def remove_error_dlg(self): """Remove error dialog.""" self.error_dlg = None #------ Public API --------------------------------------------------------- @Slot() def quit(self): """Quit mainwindow""" self.main.close() @Slot() def show_env(self): """Show environment variables""" self.dialog_manager.show(EnvDialog()) @Slot() def show_syspath(self): """Show sys.path""" editor = CollectionsEditor() editor.setup(sys.path, title="sys.path", readonly=True, width=600, icon=ima.icon('syspath')) self.dialog_manager.show(editor) @Slot() def run_script(self, filename=None, silent=False, set_focus=False, args=None): """Run a Python script""" if filename is None: self.shell.interpreter.restore_stds() filename, _selfilter = getopenfilename( self, _("Run Python script"), getcwd_or_home(), _("Python scripts")+" (*.py ; *.pyw ; *.ipy)") self.shell.interpreter.redirect_stds() if filename: os.chdir( osp.dirname(filename) ) filename = osp.basename(filename) else: return logger.debug("Running script with %s", args) filename = osp.abspath(filename) rbs = remove_backslashes command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args)) if set_focus: self.shell.setFocus() if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() self.shell.write(command+'\n') self.shell.run_command(command) 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_script(fname, int(lnb)) def edit_script(self, filename=None, goto=-1): """Edit script""" # Called from InternalShell if not hasattr(self, 'main') \ or not hasattr(self.main, 'editor'): self.shell.external_editor(filename, goto) return if filename is not None: self.edit_goto.emit(osp.abspath(filename), goto, '') def execute_lines(self, lines): """Execute lines and give focus to shell""" self.shell.execute_lines(to_text_string(lines)) self.shell.setFocus() @Slot() def change_max_line_count(self): "Change maximum line count""" mlc, valid = QInputDialog.getInt(self, _('Buffer'), _('Maximum line count'), self.get_option('max_line_count'), 0, 1000000) if valid: self.shell.setMaximumBlockCount(mlc) self.set_option('max_line_count', mlc) @Slot() def change_exteditor(self): """Change external editor path""" path, valid = QInputDialog.getText(self, _('External editor'), _('External editor executable path:'), QLineEdit.Normal, self.get_option('external_editor/path')) if valid: self.set_option('external_editor/path', to_text_string(path)) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" self.shell.toggle_wrap_mode(checked) self.set_option('wrap', checked) @Slot(bool) def toggle_calltips(self, checked): """Toggle calltips""" self.shell.set_calltips(checked) self.set_option('calltips', checked) @Slot(bool) def toggle_codecompletion(self, checked): """Toggle automatic code completion""" self.shell.set_codecompletion_auto(checked) self.set_option('codecompletion/auto', checked) @Slot(bool) def toggle_codecompletion_enter(self, checked): """Toggle Enter key for code completion""" self.shell.set_codecompletion_enter(checked) self.set_option('codecompletion/enter_key', checked) #----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): 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() if source.hasUrls(): pathlist = mimedata2url(source) self.shell.drop_pathlist(pathlist) elif source.hasText(): lines = to_text_string(source.text()) self.shell.set_cursor_position('eof') self.shell.execute_lines(lines) event.acceptProposedAction()
class ApplicationContainer(PluginMainContainer): def setup(self): # Attributes self.dialog_manager = DialogManager() self.give_updates_feedback = False self.thread_updates = None self.worker_updates = None # Actions # Documentation actions self.documentation_action = self.create_action( ApplicationActions.SpyderDocumentationAction, text=_("Spyder documentation"), icon=self.create_icon("DialogHelpButton"), triggered=lambda: start_file(__docs_url__), context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context="_") spyder_video_url = ("https://www.youtube.com/playlist" "?list=PLPonohdiDqg9epClEcXoAPUiK0pN5eRoc") self.video_action = self.create_action( ApplicationActions.SpyderDocumentationVideoAction, text=_("Tutorial videos"), icon=self.create_icon("VideoIcon"), triggered=lambda: start_file(spyder_video_url)) # Support actions self.trouble_action = self.create_action( ApplicationActions.SpyderTroubleshootingAction, _("Troubleshooting..."), triggered=lambda: start_file(__trouble_url__)) self.dependencies_action = self.create_action( ApplicationActions.SpyderDependenciesAction, _("Dependencies..."), triggered=self.show_dependencies, icon=self.create_icon('advanced')) self.check_updates_action = self.create_action( ApplicationActions.SpyderCheckUpdatesAction, _("Check for updates..."), triggered=self.check_updates) self.support_group_action = self.create_action( ApplicationActions.SpyderSupportAction, _("Spyder support..."), triggered=lambda: start_file(__forum_url__)) # About action self.about_action = self.create_action( ApplicationActions.SpyderAbout, _("About %s...") % "Spyder", icon=self.create_icon('MessageBoxInformation'), triggered=self.show_about) if sys.platform == 'darwin': self.about_action.setMenuRole(QAction.AboutRole) # Tools actions if WinUserEnvDialog is not None: self.winenv_action = self.create_action( ApplicationActions.SpyderWindowsEnvVariables, _("Current user environment variables..."), icon=self.create_icon('win_env'), tip=_("Show and edit current user environment " "variables in Windows registry " "(i.e. for all sessions)"), triggered=self.show_windows_env_variables) else: self.winenv_action = None # Application base actions self.restart_action = self.create_action( ApplicationActions.SpyderRestart, _("&Restart"), icon=self.create_icon('restart'), tip=_("Restart"), triggered=self.sig_restart_requested, context=Qt.ApplicationShortcut, shortcut_context="_", register_shortcut=True) # Initialize if DEV is None and self.get_conf('check_updates_on_startup'): self.give_updates_feedback = False self.check_updates(startup=True) def update_actions(self): pass def on_close(self): self.dialog_manager.close_all() def _check_updates_ready(self): """Show results of the Spyder update checking process.""" # `feedback` = False is used on startup, so only positive feedback is # given. `feedback` = True is used when after startup (when using the # menu action, and gives feeback if updates are, or are not found. feedback = self.give_updates_feedback # Get results from worker update_available = self.worker_updates.update_available latest_release = self.worker_updates.latest_release error_msg = self.worker_updates.error # Release url url_r = __project_url__ + '/releases/tag/v{}'.format(latest_release) url_i = 'https://docs.spyder-ide.org/installation.html' # Define the custom QMessageBox box = MessageCheckBox(icon=QMessageBox.Information, parent=self) box.setWindowTitle(_("New Spyder version")) box.set_checkbox_text(_("Check for updates at startup")) box.setStandardButtons(QMessageBox.Ok) box.setDefaultButton(QMessageBox.Ok) # Adjust the checkbox depending on the stored configuration option = 'check_updates_on_startup' check_updates = self.get_conf(option) box.set_checked(check_updates) if error_msg is not None: msg = error_msg box.setText(msg) box.set_check_visible(False) box.exec_() check_updates = box.is_checked() else: if update_available: header = _("<b>Spyder {} is available!</b><br><br>").format( latest_release) footer = _( "For more information visit our " "<a href=\"{}\">installation guide</a>.").format(url_i) if is_anaconda(): content = _( "<b>Important note:</b> Since you installed " "Spyder with Anaconda, please <b>don't</b> use " "<code>pip</code> to update it as that will break " "your installation.<br><br>" "Instead, run the following commands in a " "terminal:<br>" "<code>conda update anaconda</code><br>" "<code>conda install spyder={}</code><br><br>").format( latest_release) else: content = _("Please go to <a href=\"{}\">this page</a> to " "download it.<br><br>").format(url_r) msg = header + content + footer box.setText(msg) box.set_check_visible(True) box.exec_() check_updates = box.is_checked() elif feedback: msg = _("Spyder is up to date.") box.setText(msg) box.set_check_visible(False) box.exec_() check_updates = box.is_checked() # Update checkbox based on user interaction self.set_conf(option, check_updates) # Enable check_updates_action after the thread has finished self.check_updates_action.setDisabled(False) # Provide feeback when clicking menu if check on startup is on self.give_updates_feedback = True @Slot() def check_updates(self, startup=False): """Check for spyder updates on github releases using a QThread.""" # Disable check_updates_action while the thread is working self.check_updates_action.setDisabled(True) if self.thread_updates is not None: self.thread_updates.terminate() self.thread_updates = QThread(self) self.worker_updates = WorkerUpdates(self, startup=startup) self.worker_updates.sig_ready.connect(self._check_updates_ready) self.worker_updates.sig_ready.connect(self.thread_updates.quit) self.worker_updates.moveToThread(self.thread_updates) self.thread_updates.started.connect(self.worker_updates.start) self.thread_updates.start() @Slot() def show_dependencies(self): """Show Spyder Dependencies dialog.""" dlg = DependenciesDialog(self) dlg.set_data(dependencies.DEPENDENCIES) dlg.show() @Slot() def show_about(self): """Show Spyder About dialog.""" abt = AboutDialog(self) abt.show() @Slot() def show_windows_env_variables(self): """Show Windows current user environment variables.""" self.dialog_manager.show(WinUserEnvDialog(self)) @Slot() def report_missing_dependencies(self): """Show a QMessageBox with a list of missing hard dependencies.""" # Declare dependencies before trying to detect the missing ones dependencies.declare_dependencies() missing_deps = dependencies.missing_dependencies() if missing_deps: # We change '<br>' by '\n', in order to replace the '<' # that appear in our deps by '<' (to not break html # formatting) and finally we restore '<br>' again. missing_deps = (missing_deps.replace('<br>', '\n').replace( '<', '<').replace('\n', '<br>')) QMessageBox.critical( self, _('Error'), _("<b>You have missing dependencies!</b>" "<br><br><tt>%s</tt><br>" "<b>Please install them to avoid this message.</b>" "<br><br>" "<i>Note</i>: Spyder could work without some of these " "dependencies, however to have a smooth experience when " "using Spyder we <i>strongly</i> recommend you to install " "all the listed missing dependencies.<br><br>" "Failing to install these dependencies might result in bugs." " Please be sure that any found bugs are not the direct " "result of missing dependencies, prior to reporting a new " "issue.") % missing_deps, QMessageBox.Ok)
class Console(SpyderPluginWidget): """ Console widget """ CONF_SECTION = 'internal_console' CONF_FILE = False focus_changed = Signal() redirect_stdio = Signal(bool) edit_goto = Signal(str, int, str) def __init__(self, parent=None, namespace=None, commands=[], message=None, exitfunc=None, profile=False, multithreaded=False): SpyderPluginWidget.__init__(self, parent) logger.info("Initializing...") self.dialog_manager = DialogManager() # Shell self.shell = InternalShell(parent, namespace, commands, message, self.get_option('max_line_count'), self.get_font(), exitfunc, profile, multithreaded) self.shell.status.connect( lambda msg: self.sig_show_status_message.emit(msg, 0)) self.shell.go_to_error.connect(self.go_to_error) self.shell.focus_changed.connect(lambda: self.focus_changed.emit()) # Redirecting some signals: self.shell.redirect_stdio.connect( lambda state: self.redirect_stdio.emit(state)) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.set_editor(self.shell) self.find_widget.hide() if parent is not None: self.register_widget_shortcuts(self.find_widget) # Main layout btn_layout = QHBoxLayout() btn_layout.setAlignment(Qt.AlignLeft) btn_layout.addStretch() btn_layout.addWidget(self.options_button, Qt.AlignRight) layout = create_plugin_layout(btn_layout) layout.addWidget(self.shell) layout.addWidget(self.find_widget) self.setLayout(layout) # Parameters self.shell.toggle_wrap_mode(self.get_option('wrap')) # Accepting drops self.setAcceptDrops(True) # Traceback MessageBox self.error_dlg = None self.error_traceback = "" self.dismiss_error = False #------ Private API -------------------------------------------------------- def set_historylog(self, historylog): """Bind historylog instance to this console Not used anymore since v2.0""" historylog.add_history(self.shell.history_filename) self.shell.append_to_history.connect(historylog.append_to_history) def set_help(self, help_plugin): """Bind help instance to this console""" self.shell.help = help_plugin #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Internal 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.shell def update_font(self): """Update font from Preferences""" font = self.get_font() self.shell.set_font(font) def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.dialog_manager.close_all() self.shell.exit_interpreter() return True def get_plugin_actions(self): """Return a list of actions related to plugin""" quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), tip=_("Quit"), triggered=self.quit) self.register_shortcut(quit_action, "_", "Quit", "Ctrl+Q") run_action = create_action(self, _("&Run..."), None, ima.icon('run_small'), _("Run a Python script"), triggered=self.run_script) environ_action = create_action( self, _("Environment variables..."), icon=ima.icon('environ'), tip=_("Show and edit environment variables" " (for current session)"), triggered=self.show_env) syspath_action = create_action(self, _("Show sys.path contents..."), icon=ima.icon('syspath'), tip=_("Show (read-only) sys.path"), triggered=self.show_syspath) buffer_action = create_action(self, _("Buffer..."), None, tip=_("Set maximum line count"), triggered=self.change_max_line_count) exteditor_action = create_action( self, _("External editor path..."), None, None, _("Set external editor executable path"), triggered=self.change_exteditor) wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) wrap_action.setChecked(self.get_option('wrap')) codecompletion_action = create_action( self, _("Automatic code completion"), toggled=self.toggle_codecompletion) codecompletion_action.setChecked( self.get_option('codecompletion/auto')) option_menu = QMenu(_('Internal console settings'), self) option_menu.setIcon(ima.icon('tooloptions')) add_actions(option_menu, (buffer_action, wrap_action, codecompletion_action, exteditor_action)) plugin_actions = [ None, run_action, environ_action, syspath_action, option_menu, MENU_SEPARATOR, quit_action ] return plugin_actions def register_plugin(self): """Register plugin in Spyder's main window""" self.focus_changed.connect(self.main.plugin_focus_changed) self.add_dockwidget() # Connecting the following signal once the dockwidget has been created: self.shell.exception_occurred.connect(self.exception_occurred) def exception_occurred(self, text, is_traceback, is_pyls_error=False): """ Exception ocurred in the internal console. Show a QDialog or the internal console to warn the user. """ # Skip errors without traceback or dismiss if (not is_traceback and self.error_dlg is None) or self.dismiss_error: return if CONF.get('main', 'show_internal_errors'): if self.error_dlg is None: self.error_dlg = SpyderErrorDialog(self) self.error_dlg.set_color_scheme( CONF.get('appearance', 'selected')) self.error_dlg.close_btn.clicked.connect(self.close_error_dlg) self.error_dlg.rejected.connect(self.remove_error_dlg) self.error_dlg.details.go_to_error.connect(self.go_to_error) if is_pyls_error: title = "Internal Python Language Server error" self.error_dlg.set_title(title) self.error_dlg.title.setEnabled(False) self.error_dlg.append_traceback(text) self.error_dlg.show() elif DEV or get_debug_level(): self.switch_to_plugin() def close_error_dlg(self): """Close error dialog.""" if self.error_dlg.dismiss_box.isChecked(): self.dismiss_error = True self.error_dlg.reject() def remove_error_dlg(self): """Remove error dialog.""" self.error_dlg = None #------ Public API --------------------------------------------------------- @Slot() def quit(self): """Quit mainwindow""" self.main.close() @Slot() def show_env(self): """Show environment variables""" self.dialog_manager.show(EnvDialog(parent=self)) def get_sys_path(self): """Return the `sys.path`.""" return sys.path @Slot() def show_syspath(self): """Show sys.path""" editor = CollectionsEditor(parent=self) editor.setup(sys.path, title="sys.path", readonly=True, width=600, icon=ima.icon('syspath')) self.dialog_manager.show(editor) @Slot() def run_script(self, filename=None, silent=False, set_focus=False, args=None): """Run a Python script""" if filename is None: self.shell.interpreter.restore_stds() filename, _selfilter = getopenfilename( self, _("Run Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw ; *.ipy)") self.shell.interpreter.redirect_stds() if filename: os.chdir(osp.dirname(filename)) filename = osp.basename(filename) else: return logger.debug("Running script with %s", args) filename = osp.abspath(filename) rbs = remove_backslashes command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args)) if set_focus: self.shell.setFocus() if self.dockwidget: self.switch_to_plugin() self.shell.write(command + '\n') self.shell.run_command(command) 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_script(fname, int(lnb)) def edit_script(self, filename=None, goto=-1): """Edit script""" # Called from InternalShell if not hasattr(self, 'main') \ or not hasattr(self.main, 'editor'): self.shell.external_editor(filename, goto) return if filename is not None: self.edit_goto.emit(osp.abspath(filename), goto, '') def execute_lines(self, lines): """Execute lines and give focus to shell""" self.shell.execute_lines(to_text_string(lines)) self.shell.setFocus() @Slot() def change_max_line_count(self): "Change maximum line count" "" mlc, valid = QInputDialog.getInt(self, _('Buffer'), _('Maximum line count'), self.get_option('max_line_count'), 0, 1000000) if valid: self.shell.setMaximumBlockCount(mlc) self.set_option('max_line_count', mlc) @Slot() def change_exteditor(self): """Change external editor path""" path, valid = QInputDialog.getText( self, _('External editor'), _('External editor executable path:'), QLineEdit.Normal, self.get_option('external_editor/path')) if valid: self.set_option('external_editor/path', to_text_string(path)) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" self.shell.toggle_wrap_mode(checked) self.set_option('wrap', checked) @Slot(bool) def toggle_codecompletion(self, checked): """Toggle automatic code completion""" self.shell.set_codecompletion_auto(checked) self.set_option('codecompletion/auto', checked) #----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): 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() if source.hasUrls(): pathlist = mimedata2url(source) self.shell.drop_pathlist(pathlist) elif source.hasText(): lines = to_text_string(source.text()) self.shell.set_cursor_position('eof') self.shell.execute_lines(lines) event.acceptProposedAction()
class ApplicationContainer(PluginMainContainer): sig_report_issue_requested = Signal() """ Signal to request reporting an issue to Github. """ sig_load_log_file = Signal(str) """ Signal to load a log file """ def __init__(self, name, plugin, parent=None): super().__init__(name, plugin, parent) # Keep track of dpi message self.current_dpi = None self.dpi_messagebox = None # ---- PluginMainContainer API # ------------------------------------------------------------------------- def setup(self): # Compute dependencies in a thread to not block the interface. self.dependencies_thread = QThread() # Attributes self.dialog_manager = DialogManager() self.give_updates_feedback = False self.thread_updates = None self.worker_updates = None # Actions # Documentation actions self.documentation_action = self.create_action( ApplicationActions.SpyderDocumentationAction, text=_("Spyder documentation"), icon=self.create_icon("DialogHelpButton"), triggered=lambda: start_file(__docs_url__), context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context="_") spyder_video_url = ("https://www.youtube.com/playlist" "?list=PLPonohdiDqg9epClEcXoAPUiK0pN5eRoc") self.video_action = self.create_action( ApplicationActions.SpyderDocumentationVideoAction, text=_("Tutorial videos"), icon=self.create_icon("VideoIcon"), triggered=lambda: start_file(spyder_video_url)) # Support actions self.trouble_action = self.create_action( ApplicationActions.SpyderTroubleshootingAction, _("Troubleshooting..."), triggered=lambda: start_file(__trouble_url__)) self.report_action = self.create_action( ConsoleActions.SpyderReportAction, _("Report issue..."), icon=self.create_icon('bug'), triggered=self.sig_report_issue_requested) self.dependencies_action = self.create_action( ApplicationActions.SpyderDependenciesAction, _("Dependencies..."), triggered=self.show_dependencies, icon=self.create_icon('advanced')) self.check_updates_action = self.create_action( ApplicationActions.SpyderCheckUpdatesAction, _("Check for updates..."), triggered=self.check_updates) self.support_group_action = self.create_action( ApplicationActions.SpyderSupportAction, _("Spyder support..."), triggered=lambda: start_file(__forum_url__)) # About action self.about_action = self.create_action( ApplicationActions.SpyderAbout, _("About %s...") % "Spyder", icon=self.create_icon('MessageBoxInformation'), triggered=self.show_about, menurole=QAction.AboutRole) # Tools actions if WinUserEnvDialog is not None: self.winenv_action = self.create_action( ApplicationActions.SpyderWindowsEnvVariables, _("Current user environment variables..."), icon=self.create_icon('win_env'), tip=_("Show and edit current user environment " "variables in Windows registry " "(i.e. for all sessions)"), triggered=self.show_windows_env_variables) else: self.winenv_action = None # Application base actions self.restart_action = self.create_action( ApplicationActions.SpyderRestart, _("&Restart"), icon=self.create_icon('restart'), tip=_("Restart"), triggered=self.restart_normal, context=Qt.ApplicationShortcut, shortcut_context="_", register_shortcut=True) self.restart_debug_action = self.create_action( ApplicationActions.SpyderRestartDebug, _("&Restart in debug mode"), tip=_("Restart in debug mode"), triggered=self.restart_debug, context=Qt.ApplicationShortcut, shortcut_context="_", register_shortcut=True) # Debug logs if get_debug_level() >= 2: self.menu_debug_logs = self.create_menu( ApplicationPluginMenus.DebugLogsMenu, _("Debug logs")) # The menu can't be built at startup because Completions can # start after Application. self.menu_debug_logs.aboutToShow.connect( self.create_debug_log_actions) def update_actions(self): pass # ---- Other functionality # ------------------------------------------------------------------------- def on_close(self): """To call from Spyder when the plugin is closed.""" self.dialog_manager.close_all() @Slot() def show_about(self): """Show Spyder About dialog.""" abt = AboutDialog(self) abt.show() @Slot() def show_windows_env_variables(self): """Show Windows current user environment variables.""" self.dialog_manager.show(WinUserEnvDialog(self)) # ---- Updates # ------------------------------------------------------------------------- def _check_updates_ready(self): """Show results of the Spyder update checking process.""" # `feedback` = False is used on startup, so only positive feedback is # given. `feedback` = True is used when after startup (when using the # menu action, and gives feeback if updates are, or are not found. feedback = self.give_updates_feedback # Get results from worker update_available = self.worker_updates.update_available latest_release = self.worker_updates.latest_release error_msg = self.worker_updates.error # Release url if sys.platform == 'darwin': url_r = ('https://github.com/spyder-ide/spyder/releases/latest/' 'download/Spyder.dmg') else: url_r = ('https://github.com/spyder-ide/spyder/releases/latest/' 'download/Spyder_64bit_full.exe') url_i = 'https://docs.spyder-ide.org/installation.html' # Define the custom QMessageBox box = MessageCheckBox(icon=QMessageBox.Information, parent=self) box.setWindowTitle(_("New Spyder version")) box.setAttribute(Qt.WA_ShowWithoutActivating) box.set_checkbox_text(_("Check for updates at startup")) box.setStandardButtons(QMessageBox.Ok) box.setDefaultButton(QMessageBox.Ok) # Adjust the checkbox depending on the stored configuration option = 'check_updates_on_startup' check_updates = self.get_conf(option) box.set_checked(check_updates) if error_msg is not None: msg = error_msg box.setText(msg) box.set_check_visible(False) box.exec_() check_updates = box.is_checked() else: if update_available: header = _("<b>Spyder {} is available!</b><br><br>").format( latest_release) footer = _( "For more information visit our " "<a href=\"{}\">installation guide</a>.").format(url_i) if is_anaconda(): content = _( "<b>Important note:</b> Since you installed " "Spyder with Anaconda, please <b>don't</b> use " "<code>pip</code> to update it as that will break " "your installation.<br><br>" "Instead, run the following commands in a " "terminal:<br>" "<code>conda update anaconda</code><br>" "<code>conda install spyder={}</code><br><br>").format( latest_release) else: content = _("Click <a href=\"{}\">this link</a> to " "download it.<br><br>").format(url_r) msg = header + content + footer box.setText(msg) box.set_check_visible(True) box.show() check_updates = box.is_checked() elif feedback: msg = _("Spyder is up to date.") box.setText(msg) box.set_check_visible(False) box.exec_() check_updates = box.is_checked() # Update checkbox based on user interaction self.set_conf(option, check_updates) # Enable check_updates_action after the thread has finished self.check_updates_action.setDisabled(False) # Provide feeback when clicking menu if check on startup is on self.give_updates_feedback = True @Slot() def check_updates(self, startup=False): """Check for spyder updates on github releases using a QThread.""" # Disable check_updates_action while the thread is working self.check_updates_action.setDisabled(True) if self.thread_updates is not None: self.thread_updates.terminate() self.thread_updates = QThread(self) self.worker_updates = WorkerUpdates(self, startup=startup) self.worker_updates.sig_ready.connect(self._check_updates_ready) self.worker_updates.sig_ready.connect(self.thread_updates.quit) self.worker_updates.moveToThread(self.thread_updates) self.thread_updates.started.connect(self.worker_updates.start) # Delay starting this check to avoid blocking the main window # while loading. # Fixes spyder-ide/spyder#15839 updates_timer = QTimer(self) updates_timer.setInterval(3000) updates_timer.setSingleShot(True) updates_timer.timeout.connect(self.thread_updates.start) updates_timer.start() # ---- Dependencies # ------------------------------------------------------------------------- @Slot() def show_dependencies(self): """Show Spyder Dependencies dialog.""" dlg = DependenciesDialog(self) dlg.set_data(dependencies.DEPENDENCIES) dlg.show() def compute_dependencies(self): """Compute dependencies""" self.dependencies_thread.run = dependencies.declare_dependencies self.dependencies_thread.finished.connect( self.report_missing_dependencies) # This avoids computing missing deps before the window is fully up dependencies_timer = QTimer(self) dependencies_timer.setInterval(10000) dependencies_timer.setSingleShot(True) dependencies_timer.timeout.connect(self.dependencies_thread.start) dependencies_timer.start() @Slot() def report_missing_dependencies(self): """Show a QMessageBox with a list of missing hard dependencies.""" missing_deps = dependencies.missing_dependencies() if missing_deps: InstallerMissingDependencies(missing_deps) # We change '<br>' by '\n', in order to replace the '<' # that appear in our deps by '<' (to not break html # formatting) and finally we restore '<br>' again. missing_deps = (missing_deps.replace('<br>', '\n').replace( '<', '<').replace('\n', '<br>')) message = ( _("<b>You have missing dependencies!</b>" "<br><br><tt>%s</tt><br>" "<b>Please install them to avoid this message.</b>" "<br><br>" "<i>Note</i>: Spyder could work without some of these " "dependencies, however to have a smooth experience when " "using Spyder we <i>strongly</i> recommend you to install " "all the listed missing dependencies.<br><br>" "Failing to install these dependencies might result in bugs." " Please be sure that any found bugs are not the direct " "result of missing dependencies, prior to reporting a new " "issue.") % missing_deps) message_box = QMessageBox(self) message_box.setIcon(QMessageBox.Critical) message_box.setAttribute(Qt.WA_DeleteOnClose) message_box.setAttribute(Qt.WA_ShowWithoutActivating) message_box.setStandardButtons(QMessageBox.Ok) message_box.setWindowModality(Qt.NonModal) message_box.setWindowTitle(_('Error')) message_box.setText(message) message_box.show() # ---- Restart # ------------------------------------------------------------------------- @Slot() def restart_normal(self): """Restart in standard mode.""" os.environ['SPYDER_DEBUG'] = '' self.sig_restart_requested.emit() @Slot() def restart_debug(self): """Restart in debug mode.""" box = QMessageBox(self) box.setWindowTitle(_("Question")) box.setIcon(QMessageBox.Question) box.setText(_("Which debug mode do you want Spyder to restart in?")) button_verbose = QPushButton(_('Verbose')) button_minimal = QPushButton(_('Minimal')) box.addButton(button_verbose, QMessageBox.AcceptRole) box.addButton(button_minimal, QMessageBox.AcceptRole) box.setStandardButtons(QMessageBox.Cancel) box.exec_() if box.clickedButton() == button_minimal: os.environ['SPYDER_DEBUG'] = '2' elif box.clickedButton() == button_verbose: os.environ['SPYDER_DEBUG'] = '3' else: return self.sig_restart_requested.emit() # ---- Log files # ------------------------------------------------------------------------- def create_debug_log_actions(self): """Create an action for each lsp and debug log file.""" self.menu_debug_logs.clear_actions() files = [os.environ['SPYDER_DEBUG_FILE']] files += glob.glob(os.path.join(get_conf_path('lsp_logs'), '*.log')) debug_logs_actions = [] for file in files: action = self.create_action( file, os.path.basename(file), tip=file, triggered=lambda _, file=file: self.load_log_file(file), overwrite=True, register_action=False) debug_logs_actions.append(action) # Add Spyder log on its own section self.add_item_to_menu(debug_logs_actions[0], self.menu_debug_logs, section=LogsMenuSections.SpyderLogSection) # Add LSP logs for action in debug_logs_actions[1:]: self.add_item_to_menu(action, self.menu_debug_logs, section=LogsMenuSections.LSPLogsSection) # Render menu self.menu_debug_logs._render() def load_log_file(self, file): """Load log file in editor""" self.sig_load_log_file.emit(file) # ---- DPI changes # ------------------------------------------------------------------------- def set_window(self, window): """Set window property of main window.""" self._window = window def handle_new_screen(self, new_screen): """Connect DPI signals for new screen.""" if new_screen is not None: new_screen_dpi = new_screen.logicalDotsPerInch() if self.current_dpi != new_screen_dpi: self.show_dpi_change_message(new_screen_dpi) else: new_screen.logicalDotsPerInchChanged.connect( self.show_dpi_change_message) def handle_dpi_change_response(self, result, dpi): """Handle dpi change message dialog result.""" if self.dpi_messagebox.is_checked(): self.set_conf('show_dpi_message', False) self.dpi_messagebox = None if result == 0: # Restart button was clicked # Activate HDPI auto-scaling option since is needed for a # proper display when using OS scaling self.set_conf('normal_screen_resolution', False) self.set_conf('high_dpi_scaling', True) self.set_conf('high_dpi_custom_scale_factor', False) self.sig_restart_requested.emit() else: # Update current dpi for future checks self.current_dpi = dpi def show_dpi_change_message(self, dpi): """Show message to restart Spyder since the DPI scale changed.""" if not self.get_conf('show_dpi_message'): return if self.current_dpi != dpi: # Check the window state to not show the message if the window # is in fullscreen mode. window = self._window.windowHandle() if (window.windowState() == Qt.WindowFullScreen and sys.platform == 'darwin'): return if self.dpi_messagebox is not None: return self.dpi_messagebox = MessageCheckBox(icon=QMessageBox.Warning, parent=self) self.dpi_messagebox.set_checkbox_text(_("Don't show again.")) self.dpi_messagebox.set_checked(False) self.dpi_messagebox.set_check_visible(True) self.dpi_messagebox.setText( _("A monitor scale change was detected. <br><br>" "We recommend restarting Spyder to ensure that it's properly " "displayed. If you don't want to do that, please be sure to " "activate the option<br><br><tt>Enable auto high DPI scaling" "</tt><br><br>in <tt>Preferences > Application > " "Interface</tt>, in case Spyder is not displayed " "correctly.<br><br>" "Do you want to restart Spyder?")) self.dpi_messagebox.addButton(_('Restart now'), QMessageBox.NoRole) dismiss_button = self.dpi_messagebox.addButton( _('Dismiss'), QMessageBox.NoRole) self.dpi_messagebox.setDefaultButton(dismiss_button) self.dpi_messagebox.finished.connect( lambda result: self.handle_dpi_change_response(result, dpi)) self.dpi_messagebox.open()