diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 7022f8c79cc..7c6bd3d084e 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -1175,6 +1175,7 @@ def add_ipm_action(text, path): if isinstance(child, QMenu): try: child.aboutToShow.connect(self.update_edit_menu) + child.aboutToShow.connect(self.update_search_menu) except TypeError: pass @@ -1469,6 +1470,7 @@ def setup_layout(self, default=False): def setup_default_layouts(self, index, settings): """Setup default layouts when run for the first time""" + self.maximize_dockwidget(restore=True) self.set_window_settings(*settings) self.setUpdatesEnabled(False) @@ -2016,20 +2018,13 @@ def get_focus_widget_properties(self): """Get properties of focus widget Returns tuple (widget, properties) where properties is a tuple of booleans: (is_console, not_readonly, readwrite_editor)""" - widget = QApplication.focusWidget() - from spyder.widgets.shell import ShellBaseWidget from spyder.widgets.editor import TextEditBaseWidget from spyder.widgets.ipythonconsole import ControlWidget - - # if focused widget isn't valid try the last focused - if not isinstance(widget, (ShellBaseWidget, TextEditBaseWidget, - ControlWidget)): - widget = self.previous_focused_widget + widget = QApplication.focusWidget() textedit_properties = None - if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget, - ControlWidget)): - console = isinstance(widget, (ShellBaseWidget, ControlWidget)) + if isinstance(widget, (TextEditBaseWidget, ControlWidget)): + console = isinstance(widget, ControlWidget) not_readonly = not widget.isReadOnly() readwrite_editor = not_readonly and not console textedit_properties = (console, not_readonly, readwrite_editor) @@ -2040,7 +2035,8 @@ def update_edit_menu(self): widget, textedit_properties = self.get_focus_widget_properties() if textedit_properties is None: # widget is not an editor/console return - #!!! Below this line, widget is expected to be a QPlainTextEdit instance + # !!! Below this line, widget is expected to be a QPlainTextEdit + # instance console, not_readonly, readwrite_editor = textedit_properties # Editor has focus and there is no file opened in it @@ -2073,19 +2069,27 @@ def update_edit_menu(self): def update_search_menu(self): """Update search menu""" - if self.menuBar().hasFocus(): - return + # Disabling all actions except the last one + # (which is Find in files) to begin with + for child in self.search_menu.actions()[:-1]: + child.setEnabled(False) widget, textedit_properties = self.get_focus_widget_properties() - for action in self.editor.search_menu_actions: - try: - action.setEnabled(self.editor.isAncestorOf(widget)) - except RuntimeError: - pass if textedit_properties is None: # widget is not an editor/console return - #!!! Below this line, widget is expected to be a QPlainTextEdit instance - _x, _y, readwrite_editor = textedit_properties + + # !!! Below this line, widget is expected to be a QPlainTextEdit + # instance + console, not_readonly, readwrite_editor = textedit_properties + + # Find actions only trigger an effect in the Editor + if not console: + for action in self.search_menu.actions(): + try: + action.setEnabled(True) + except RuntimeError: + pass + # Disable the replace action for read-only files self.search_menu_actions[3].setEnabled(readwrite_editor) @@ -2545,13 +2549,12 @@ def global_callback(self): action = self.sender() callback = from_qvariant(action.data(), to_text_string) from spyder.widgets.editor import TextEditBaseWidget + from spyder.widgets.ipythonconsole import ControlWidget - # If focused widget isn't valid try the last focused - if not isinstance(widget, TextEditBaseWidget): - widget = self.previous_focused_widget - - if isinstance(widget, TextEditBaseWidget): + if isinstance(widget, (TextEditBaseWidget, ControlWidget)): getattr(widget, callback)() + else: + return def redirect_internalshell_stdio(self, state): if state: @@ -2584,11 +2587,10 @@ def execute_in_external_console(self, lines, focus_to_editor): to the Editor. """ console = self.ipyconsole - console.visibility_changed(True) - console.raise_() + console.switch_to_plugin() console.execute_code(lines) if focus_to_editor: - self.editor.visibility_changed(True) + self.editor.switch_to_plugin() def open_file(self, fname, external=False): """ @@ -2911,7 +2913,8 @@ def restart(self, reset=False): # ---- Interactive Tours def show_tour(self, index): - """ """ + """Show interactive tour.""" + self.maximize_dockwidget(restore=True) frames = self.tours_available[index] self.tour.set_tour(index, frames, self) self.tour.start_tour() diff --git a/spyder/plugins/__init__.py b/spyder/plugins/__init__.py index 5397cc43963..8975ac6fd90 100644 --- a/spyder/plugins/__init__.py +++ b/spyder/plugins/__init__.py @@ -405,10 +405,11 @@ def register_widget_shortcuts(self, widget): self.register_shortcut(qshortcut, context, name) def switch_to_plugin(self): - """Switch to plugin - This method is called when pressing plugin's shortcut key""" - if not self.ismaximized: - self.dockwidget.show() + """Switch to plugin.""" + if (self.main.last_plugin is not None and + self.main.last_plugin.ismaximized and + self.main.last_plugin is not self): + self.main.maximize_dockwidget() if not self.toggle_view_action.isChecked(): self.toggle_view_action.setChecked(True) self.visibility_changed(True) diff --git a/spyder/plugins/editor.py b/spyder/plugins/editor.py index a7b527f3b2d..3db5b5ef862 100644 --- a/spyder/plugins/editor.py +++ b/spyder/plugins/editor.py @@ -417,8 +417,6 @@ def __init__(self, parent, ignore_last_opened_files=False): self.pythonfile_dependent_actions = [] self.dock_toolbar_actions = None self.edit_menu_actions = None #XXX: find another way to notify Spyder - # (see spyder.py: 'update_edit_menu' method) - self.search_menu_actions = None #XXX: same thing ('update_search_menu') self.stack_menu_actions = None # Initialize plugin @@ -1063,8 +1061,7 @@ def get_plugin_actions(self): self.main.edit_toolbar_actions += edit_toolbar_actions # ---- Search menu/toolbar construction ---- - self.search_menu_actions = [gotoline_action] - self.main.search_menu_actions += self.search_menu_actions + self.main.search_menu_actions += [gotoline_action] self.main.search_toolbar_actions += [gotoline_action] # ---- Run menu/toolbar construction ---- @@ -1536,13 +1533,13 @@ def update_warning_menu(self): filename = self.get_current_filename() for message, line_number in check_results: error = 'syntax' in message - text = message[:1].upper()+message[1:] + text = message[:1].upper() + message[1:] icon = ima.icon('error') if error else ima.icon('warning') - # QAction.triggered works differently for PySide and PyQt - if not API == 'pyside': - slot = lambda _checked, _l=line_number: self.load(filename, goto=_l) - else: - slot = lambda _l=line_number: self.load(filename, goto=_l) + + def slot(): + self.switch_to_plugin() + self.load(filename, goto=line_number) + action = create_action(self, text=text, icon=icon, triggered=slot) self.warning_menu.addAction(action) @@ -1569,11 +1566,11 @@ def update_todo_menu(self): filename = self.get_current_filename() for text, line0 in results: icon = ima.icon('todo') - # QAction.triggered works differently for PySide and PyQt - if not API == 'pyside': - slot = lambda _checked, _l=line0: self.load(filename, goto=_l) - else: - slot = lambda _l=line0: self.load(filename, goto=_l) + + def slot(): + self.switch_to_plugin() + self.load(filename, goto=line0) + action = create_action(self, text=text, icon=icon, triggered=slot) self.todo_menu.addAction(action) self.update_todo_actions() @@ -2162,6 +2159,7 @@ def unblockcomment(self): editor.unblockcomment() @Slot() def go_to_next_todo(self): + self.switch_to_plugin() editor = self.get_current_editor() position = editor.go_to_next_todo() filename = self.get_current_filename() @@ -2169,6 +2167,7 @@ def go_to_next_todo(self): @Slot() def go_to_next_warning(self): + self.switch_to_plugin() editor = self.get_current_editor() position = editor.go_to_next_warning() filename = self.get_current_filename() @@ -2176,6 +2175,7 @@ def go_to_next_warning(self): @Slot() def go_to_previous_warning(self): + self.switch_to_plugin() editor = self.get_current_editor() position = editor.go_to_previous_warning() filename = self.get_current_filename() @@ -2203,20 +2203,24 @@ def toggle_eol_chars(self, os_name, checked): if checked: editor = self.get_current_editor() if self.__set_eol_chars: + self.switch_to_plugin() editor.set_eol_chars(sourcecode.get_eol_chars_from_os_name(os_name)) @Slot(bool) def toggle_show_blanks(self, checked): + self.switch_to_plugin() editor = self.get_current_editor() editor.set_blanks_enabled(checked) @Slot() def remove_trailing_spaces(self): + self.switch_to_plugin() editorstack = self.get_current_editorstack() editorstack.remove_trailing_spaces() @Slot() def fix_indentation(self): + self.switch_to_plugin() editorstack = self.get_current_editorstack() editorstack.fix_indentation() @@ -2306,10 +2310,12 @@ def __move_cursor_position(self, index_move): @Slot() def go_to_previous_cursor_position(self): + self.switch_to_plugin() self.__move_cursor_position(-1) @Slot() def go_to_next_cursor_position(self): + self.switch_to_plugin() self.__move_cursor_position(1) @Slot() @@ -2324,6 +2330,7 @@ def set_or_clear_breakpoint(self): """Set/Clear breakpoint""" editorstack = self.get_current_editorstack() if editorstack is not None: + self.switch_to_plugin() editorstack.set_or_clear_breakpoint() @Slot() @@ -2331,11 +2338,13 @@ def set_or_edit_conditional_breakpoint(self): """Set/Edit conditional breakpoint""" editorstack = self.get_current_editorstack() if editorstack is not None: + self.switch_to_plugin() editorstack.set_or_edit_conditional_breakpoint() @Slot() def clear_all_breakpoints(self): """Clear breakpoints in all files""" + self.switch_to_plugin() clear_all_breakpoints() self.breakpoints_saved.emit() editorstack = self.get_current_editorstack() @@ -2356,6 +2365,7 @@ def clear_breakpoint(self, filename, lineno): def debug_command(self, command): """Debug actions""" + self.switch_to_plugin() self.main.ipyconsole.write_to_stdin(command) focus_widget = self.main.ipyconsole.get_focus_widget() if focus_widget: @@ -2451,6 +2461,7 @@ def set_dialog_size(self, size): @Slot() def debug_file(self): """Debug current script""" + self.switch_to_plugin() self.run_file(debug=True) @Slot() diff --git a/spyder/plugins/help.py b/spyder/plugins/help.py index f3bb79a9d1a..902f3728378 100644 --- a/spyder/plugins/help.py +++ b/spyder/plugins/help.py @@ -720,25 +720,21 @@ def show_intro_message(self): def show_rich_text(self, text, collapse=False, img_path=''): """Show text in rich mode""" - self.visibility_changed(True) - self.raise_() + self.switch_to_plugin() self.switch_to_rich_text() context = generate_context(collapse=collapse, img_path=img_path) self.render_sphinx_doc(text, context) def show_plain_text(self, text): """Show text in plain mode""" - self.visibility_changed(True) - self.raise_() + self.switch_to_plugin() self.switch_to_plain_text() self.set_plain_text(text, is_code=False) @Slot() def show_tutorial(self): """Show the Spyder tutorial in the Help plugin, opening it if needed""" - if not self.dockwidget.isVisible(): - self.dockwidget.show() - self.toggle_view_action.setChecked(True) + self.switch_to_plugin() tutorial_path = get_module_source_path('spyder.utils.help') tutorial = osp.join(tutorial_path, 'tutorial.rst') text = open(tutorial).read() @@ -819,8 +815,7 @@ def __eventually_raise_help(self, text, force=False): if (self.console.dockwidget not in dockwidgets and self.main.ipyconsole is not None and self.main.ipyconsole.dockwidget not in dockwidgets): - self.dockwidget.show() - self.dockwidget.raise_() + self.switch_to_plugin() self._last_texts[index] = text def load_history(self, obj=None): diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py index a8148c75b2b..f748bf1f8b2 100644 --- a/spyder/plugins/ipythonconsole.py +++ b/spyder/plugins/ipythonconsole.py @@ -659,8 +659,7 @@ class IPythonConsole(SpyderPluginWidget): "required to create IPython consoles. Please " "make it writable.") - def __init__(self, parent, testing=False, test_dir=None, - test_no_stderr=False): + def __init__(self, parent, test_dir=None, test_no_stderr=False): """Ipython Console constructor.""" if PYQT5: SpyderPluginWidget.__init__(self, parent, main = parent) @@ -669,13 +668,6 @@ def __init__(self, parent, testing=False, test_dir=None, 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.editor = None # Editor plugin - self.projects = None # Projects plugin - self.master_clients = 0 self.clients = [] self.filenames = [] @@ -683,24 +675,17 @@ def __init__(self, parent, testing=False, test_dir=None, self.create_new_client_if_empty = True # Attrs for testing - self.testing = testing - if test_dir is None: - self.test_dir = get_temp_dir() - else: - self.test_dir = test_dir - + self.test_dir = test_dir self.test_no_stderr = test_no_stderr # Initialize plugin - if not self.testing: - self.initialize_plugin() + self.initialize_plugin() # Create temp dir on testing to save kernel errors - if self.testing and test_dir is not None: + if self.test_dir is not None: if not osp.isdir(osp.join(test_dir)): os.makedirs(osp.join(test_dir)) - layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions, rename_tabs=True, split_char='/', split_index=0) @@ -730,8 +715,7 @@ def __init__(self, parent, testing=False, test_dir=None, # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() - if not self.testing: - self.register_widget_shortcuts(self.find_widget) + self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) @@ -833,10 +817,10 @@ def refresh_plugin(self): widgets = [] self.find_widget.set_editor(control) self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets}) - if client and not self.testing: + if client: sw = client.shellwidget - self.variableexplorer.set_shellwidget_from_id(id(sw)) - self.help.set_shell(sw) + self.main.variableexplorer.set_shellwidget_from_id(id(sw)) + self.main.help.set_shell(sw) self.update_tabs_text() self.update_plugin_title.emit() @@ -884,31 +868,26 @@ 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.variableexplorer = self.main.variableexplorer - self.editor = self.main.editor - self.explorer = self.main.explorer - self.projects = self.main.projects - self.focus_changed.connect(self.main.plugin_focus_changed) - self.edit_goto.connect(self.editor.load) + self.edit_goto.connect(self.main.editor.load) self.edit_goto[str, int, str, bool].connect( lambda fname, lineno, word, processevents: - self.editor.load(fname, lineno, word, - processevents=processevents)) - self.editor.breakpoints_saved.connect(self.set_spyder_breakpoints) - self.editor.run_in_current_ipyclient.connect(self.run_script) + self.main.editor.load(fname, lineno, word, + processevents=processevents)) + self.main.editor.breakpoints_saved.connect(self.set_spyder_breakpoints) + self.main.editor.run_in_current_ipyclient.connect(self.run_script) self.main.workingdirectory.set_current_console_wd.connect( self.set_current_client_working_directory) self.tabwidget.currentChanged.connect(self.update_working_directory) - self.explorer.open_interpreter.connect(self.create_client_from_path) - self.explorer.run.connect(lambda fname: self.run_script( + self.main.explorer.open_interpreter.connect( + self.create_client_from_path) + self.main.explorer.run.connect(lambda fname: self.run_script( fname, osp.dirname(fname), '', False, False, False, True)) - self.projects.open_interpreter.connect(self.create_client_from_path) - self.projects.run.connect(lambda fname: self.run_script( + self.main.projects.open_interpreter.connect( + self.create_client_from_path) + self.main.projects.run.connect(lambda fname: self.run_script( fname, osp.dirname(fname), '', False, False, False, True)) self._remove_old_stderr_files() @@ -1002,8 +981,7 @@ def run_script(self, filename, wdir, args, debug, post_mortem, clear_variables)) except AttributeError: pass - self.visibility_changed(True) - self.raise_() + self.switch_to_plugin() else: #XXX: not sure it can really happen QMessageBox.warning(self, _('Warning'), @@ -1021,7 +999,7 @@ def set_working_directory(self, dirname): """Set current working directory. In the workingdirectory and explorer plugins. """ - if dirname and not self.testing: + if dirname: self.main.workingdirectory.chdir(dirname, refresh_explorer=True, refresh_console=False) @@ -1093,8 +1071,11 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False): show_elapsed_time=show_elapsed_time, reset_warning=reset_warning, ask_before_restart=ask_before_restart) - if self.testing: + + # Change stderr_dir if requested + if self.test_dir is not None: client.stderr_dir = self.test_dir + self.add_tab(client, name=client.get_name(), filename=filename) if cf is None: @@ -1143,12 +1124,7 @@ def create_client_for_kernel(self): def connect_client_to_kernel(self, client, is_cython=False): """Connect a client to its kernel""" connection_file = client.connection_file - - if self.test_no_stderr: - stderr_handle = None - else: - stderr_handle = client.stderr_handle - + stderr_handle = None if self.test_no_stderr else client.stderr_handle km, kc = self.create_kernel_manager_and_kernel_client( connection_file, stderr_handle, @@ -1309,9 +1285,9 @@ def register_client(self, client, give_focus=True): cwd_path = '' if CONF.get('workingdir', 'console/use_project_or_home_directory'): cwd_path = get_home_dir() - if (self.projects is not None and - self.projects.get_active_project() is not None): - cwd_path = self.projects.get_active_project_path() + if (self.main.projects is not None and + self.main.projects.get_active_project() is not None): + cwd_path = self.main.projects.get_active_project_path() elif CONF.get('workingdir', 'console/use_fixed_directory'): cwd_path = CONF.get('workingdir', 'console/fixed_directory') @@ -1322,14 +1298,15 @@ def register_client(self, client, give_focus=True): shellwidget.get_cwd() # Connect text widget to Help - if self.help is not None: - control.set_help(self.help) + if self.main.help is not None: + control.set_help(self.main.help) control.set_help_enabled(CONF.get('help', 'connect/ipython_console')) # Connect client to our history log - if self.historylog is not None: - self.historylog.add_history(client.history_filename) - client.append_to_history.connect(self.historylog.append_to_history) + if self.main.historylog is not None: + self.main.historylog.add_history(client.history_filename) + client.append_to_history.connect( + self.main.historylog.append_to_history) # Set font for client client.set_font( self.get_plugin_font() ) @@ -1550,9 +1527,8 @@ def create_kernel_spec(self, is_cython=False): """Create a kernel spec for our own kernels""" # Before creating our kernel spec, we always need to # set this value in spyder.ini - if not self.testing: - CONF.set('main', 'spyder_pythonpath', - self.main.get_spyder_pythonpath()) + CONF.set('main', 'spyder_pythonpath', + self.main.get_spyder_pythonpath()) return SpyderKernelSpec(is_cython=is_cython) def create_kernel_manager_and_kernel_client(self, connection_file, @@ -1593,8 +1569,8 @@ def create_kernel_manager_and_kernel_client(self, connection_file, def restart_kernel(self): """Restart kernel of current client.""" client = self.get_current_client() - if client is not None: + self.switch_to_plugin() client.restart_kernel() def connect_external_kernel(self, shellwidget): @@ -1604,14 +1580,14 @@ def connect_external_kernel(self, shellwidget): """ sw = shellwidget kc = shellwidget.kernel_client - if self.help is not None: - self.help.set_shell(sw) - if self.variableexplorer is not None: - self.variableexplorer.add_shellwidget(sw) + if self.main.help is not None: + self.main.help.set_shell(sw) + if self.main.variableexplorer is not None: + self.main.variableexplorer.add_shellwidget(sw) sw.set_namespace_view_settings() sw.refresh_namespacebrowser() kc.stopped_channels.connect(lambda : - self.variableexplorer.remove_shellwidget(id(sw))) + self.main.variableexplorer.remove_shellwidget(id(sw))) #------ Public API (for tabs) --------------------------------------------- def add_tab(self, widget, name, filename=''): @@ -1620,9 +1596,8 @@ def add_tab(self, widget, name, filename=''): index = self.tabwidget.addTab(widget, name) self.filenames.insert(index, filename) self.tabwidget.setCurrentIndex(index) - if self.dockwidget and not self.ismaximized: - self.dockwidget.setVisible(True) - self.dockwidget.raise_() + if self.dockwidget and not self.main.is_setting_up: + self.switch_to_plugin() self.activateWindow() widget.get_control().setFocus() self.update_tabs_text() @@ -1707,19 +1682,19 @@ def go_to_error(self, text): def show_intro(self): """Show intro to IPython help""" from IPython.core.usage import interactive_usage - self.help.show_rich_text(interactive_usage) + self.main.help.show_rich_text(interactive_usage) @Slot() def show_guiref(self): """Show qtconsole help""" from qtconsole.usage import gui_reference - self.help.show_rich_text(gui_reference, collapse=True) + self.main.help.show_rich_text(gui_reference, collapse=True) @Slot() def show_quickref(self): """Show IPython Cheat Sheet""" from IPython.core.usage import quick_reference - self.help.show_plain_text(quick_reference) + self.main.help.show_plain_text(quick_reference) #------ Private API ------------------------------------------------------- def _new_connection_file(self): @@ -1743,14 +1718,15 @@ def _new_connection_file(self): return cf def process_started(self, client): - if self.help is not None: - self.help.set_shell(client.shellwidget) - if self.variableexplorer is not None: - self.variableexplorer.add_shellwidget(client.shellwidget) + if self.main.help is not None: + self.main.help.set_shell(client.shellwidget) + if self.main.variableexplorer is not None: + self.main.variableexplorer.add_shellwidget(client.shellwidget) def process_finished(self, client): - if self.variableexplorer is not None: - self.variableexplorer.remove_shellwidget(id(client.shellwidget)) + if self.main.variableexplorer is not None: + self.main.variableexplorer.remove_shellwidget( + id(client.shellwidget)) def _create_client_for_kernel(self, connection_file, hostname, sshkey, password): @@ -1818,7 +1794,9 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey, show_elapsed_time=show_elapsed_time, reset_warning=reset_warning, ask_before_restart=ask_before_restart) - if self.testing: + + # Change stderr_dir if requested + if self.test_dir is not None: client.stderr_dir = self.test_dir # Create kernel client diff --git a/spyder/plugins/projects.py b/spyder/plugins/projects.py index a83cd81e012..f7595caed00 100644 --- a/spyder/plugins/projects.py +++ b/spyder/plugins/projects.py @@ -56,7 +56,7 @@ class Projects(ProjectExplorerWidget, SpyderPluginMixin): sig_project_closed = Signal(object) sig_new_file = Signal(str) - def __init__(self, parent=None, testing=False): + def __init__(self, parent=None): ProjectExplorerWidget.__init__(self, parent=parent, name_filters=self.get_option('name_filters'), show_all=self.get_option('show_all'), @@ -67,15 +67,8 @@ def __init__(self, parent=None, testing=False): self.current_active_project = None self.latest_project = None - self.editor = None - self.workingdirectory = None - - # For testing - self.testing = testing - # Initialize plugin - if not self.testing: - self.initialize_plugin() + self.initialize_plugin() self.setup_project(self.get_active_project_path()) #------ SpyderPluginWidget API --------------------------------------------- @@ -103,7 +96,7 @@ def get_plugin_actions(self): triggered=self.close_project) self.delete_project_action = create_action(self, _("Delete Project"), - triggered=self.delete_project) + triggered=self._delete_project) self.clear_recent_projects_action =\ create_action(self, _("Clear this list"), triggered=self.clear_recent_projects) @@ -113,54 +106,53 @@ def get_plugin_actions(self): self.recent_project_menu = QMenu(_("Recent Projects"), self) explorer_action = self.toggle_view_action - self.main.projects_menu_actions += [self.new_project_action, - MENU_SEPARATOR, - self.open_project_action, - self.close_project_action, - self.delete_project_action, - MENU_SEPARATOR, - self.recent_project_menu, - explorer_action] + if self.main is not None: + self.main.projects_menu_actions += [self.new_project_action, + MENU_SEPARATOR, + self.open_project_action, + self.close_project_action, + self.delete_project_action, + MENU_SEPARATOR, + self.recent_project_menu, + explorer_action] self.setup_menu_actions() return [] def register_plugin(self): """Register plugin in Spyder's main window""" - self.editor = self.main.editor - self.workingdirectory = self.main.workingdirectory - self.main.pythonpath_changed() self.main.restore_scrollbar_position.connect( self.restore_scrollbar_position) self.sig_pythonpath_changed.connect(self.main.pythonpath_changed) - self.create_module.connect(self.editor.new) - self.edit.connect(self.editor.load) - self.removed.connect(self.editor.removed) - self.removed_tree.connect(self.editor.removed_tree) - self.renamed.connect(self.editor.renamed) - self.renamed_tree.connect(self.editor.renamed_tree) - self.editor.set_projects(self) + self.create_module.connect(self.main.editor.new) + self.edit.connect(self.main.editor.load) + self.removed.connect(self.main.editor.removed) + self.removed_tree.connect(self.main.editor.removed_tree) + self.renamed.connect(self.main.editor.renamed) + self.renamed_tree.connect(self.main.editor.renamed_tree) + self.main.editor.set_projects(self) self.main.add_dockwidget(self) self.sig_open_file.connect(self.main.open_file) - self.sig_new_file.connect(lambda x: self.editor.new(text=x)) + self.sig_new_file.connect(lambda x: self.main.editor.new(text=x)) # New project connections. Order matters! self.sig_project_loaded.connect( - lambda v: self.workingdirectory.chdir(v)) + lambda v: self.main.workingdirectory.chdir(v)) self.sig_project_loaded.connect( lambda v: self.main.set_window_title()) self.sig_project_loaded.connect( - lambda v: self.editor.setup_open_files()) + lambda v: self.main.editor.setup_open_files()) self.sig_project_loaded.connect(self.update_explorer) self.sig_project_closed[object].connect( - lambda v: self.workingdirectory.chdir(self.get_last_working_dir())) + lambda v: self.main.workingdirectory.chdir( + self.get_last_working_dir())) self.sig_project_closed.connect( lambda v: self.main.set_window_title()) self.sig_project_closed.connect( - lambda v: self.editor.setup_open_files()) + lambda v: self.main.editor.setup_open_files()) self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions) def refresh_plugin(self): @@ -173,6 +165,20 @@ def closing_plugin(self, cancelable=False): self.closing_widget() return True + def switch_to_plugin(self): + """Switch to plugin.""" + # Unmaxizime currently maximized plugin + if (self.main.last_plugin is not None and + self.main.last_plugin.ismaximized and + self.main.last_plugin is not self): + self.main.maximize_dockwidget() + + # Show plugin only if it was already visible + if self.get_option('visible_if_project_open'): + if not self.toggle_view_action.isChecked(): + self.toggle_view_action.setChecked(True) + self.visibility_changed(True) + #------ Public API --------------------------------------------------------- def setup_menu_actions(self): """Setup and update the menu actions.""" @@ -182,10 +188,15 @@ def setup_menu_actions(self): for project in self.recent_projects: if self.is_valid_project(project): name = project.replace(get_home_dir(), '~') + + def slot(): + self.switch_to_plugin() + self.open_project(path=project) + action = create_action(self, name, icon = ima.icon('project'), - triggered=lambda v, path=project: self.open_project(path=path)) + triggered=slot) self.recent_projects_actions.append(action) else: self.recent_projects.remove(project) @@ -225,6 +236,7 @@ def edit_project_preferences(self): @Slot() def create_new_project(self): """Create new project""" + self.switch_to_plugin() active_project = self.current_active_project dlg = ProjectDialog(self) dlg.sig_project_creation_requested.connect(self._create_project) @@ -245,6 +257,7 @@ def _create_project(self, path): def open_project(self, path=None, restart_consoles=True, save_previous_files=True): """Open the project located in `path`""" + self.switch_to_plugin() if path is None: basedir = get_home_dir() path = getexistingdirectory(parent=self, @@ -263,23 +276,24 @@ def open_project(self, path=None, restart_consoles=True, # A project was not open before if self.current_active_project is None: - if save_previous_files and self.editor is not None: - self.editor.save_open_files() - if self.editor is not None: - self.editor.set_option('last_working_dir', getcwd_or_home()) + if save_previous_files and self.main.editor is not None: + self.main.editor.save_open_files() + if self.main.editor is not None: + self.main.editor.set_option('last_working_dir', + getcwd_or_home()) if self.get_option('visible_if_project_open'): self.show_explorer() else: # We are switching projects - if self.editor is not None: - self.set_project_filenames(self.editor.get_open_filenames()) + if self.main.editor is not None: + self.set_project_filenames( + self.main.editor.get_open_filenames()) self.current_active_project = EmptyProject(path) self.latest_project = EmptyProject(path) self.set_option('current_project_path', self.get_active_project_path()) - if not self.testing: - self.setup_menu_actions() + self.setup_menu_actions() self.sig_project_loaded.emit(path) self.sig_pythonpath_changed.emit() @@ -292,12 +306,11 @@ def close_project(self): project """ if self.current_active_project: + self.switch_to_plugin() path = self.current_active_project.root_path self.current_active_project = None self.set_option('current_project_path', None) - - if not self.testing: - self.setup_menu_actions() + self.setup_menu_actions() self.sig_project_closed.emit(path) self.sig_pythonpath_changed.emit() @@ -310,8 +323,15 @@ def close_project(self): self.clear() self.restart_consoles() - if self.editor is not None: - self.set_project_filenames(self.editor.get_open_filenames()) + if self.main.editor is not None: + self.set_project_filenames( + self.main.editor.get_open_filenames()) + + def _delete_project(self): + """Delete current project.""" + if self.current_active_project: + self.switch_to_plugin() + self.delete_project() def clear_recent_projects(self): """Clear the list of recent projects""" @@ -374,8 +394,8 @@ def get_pythonpath(self, at_start=False): def get_last_working_dir(self): """Get the path of the last working directory""" - return self.editor.get_option('last_working_dir', - default=getcwd_or_home()) + return self.main.editor.get_option('last_working_dir', + default=getcwd_or_home()) def save_config(self): """ @@ -422,7 +442,7 @@ def show_explorer(self): def restart_consoles(self): """Restart consoles when closing, opening and switching projects""" - if not self.testing and self.main.ipyconsole is not None: + if self.main.ipyconsole is not None: self.main.ipyconsole.restart() def is_valid_project(self, path): diff --git a/spyder/plugins/tests/test_help.py b/spyder/plugins/tests/test_help.py index 91431154080..494eddc81f8 100644 --- a/spyder/plugins/tests/test_help.py +++ b/spyder/plugins/tests/test_help.py @@ -107,21 +107,14 @@ def test_help_opens_when_show_tutorial_unit(help_plugin, qtbot): MockDockwidget = MagicMock() MockDockwidget.return_value.isVisible.return_value = False mockDockwidget_instance = MockDockwidget() - - MockAction = Mock() - mock_toggle_view_action = MockAction() mock_show_rich_text = Mock() help_plugin.dockwidget = mockDockwidget_instance - help_plugin.toggle_view_action = mock_toggle_view_action help_plugin.show_rich_text = mock_show_rich_text help_plugin.show_tutorial() qtbot.wait(100) - assert mockDockwidget_instance.show.call_count == 1 - assert mock_toggle_view_action.setChecked.call_count == 1 - mock_toggle_view_action.setChecked.assert_called_once_with(True) assert mock_show_rich_text.call_count == 1 MockDockwidget.return_value.isVisible.return_value = True @@ -130,8 +123,6 @@ def test_help_opens_when_show_tutorial_unit(help_plugin, qtbot): help_plugin.show_tutorial() qtbot.wait(100) - assert mockDockwidget_instance.show.call_count == 1 - assert mock_toggle_view_action.setChecked.call_count == 1 assert mock_show_rich_text.call_count == 2 diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py index 4e89a3eb288..af0ab73e883 100644 --- a/spyder/plugins/tests/test_ipythonconsole.py +++ b/spyder/plugins/tests/test_ipythonconsole.py @@ -18,6 +18,10 @@ import sys import tempfile from textwrap import dedent +try: + from unittest.mock import Mock +except ImportError: + from mock import Mock # Python 2 # Third party imports import cloudpickle @@ -28,7 +32,7 @@ from qtpy import PYQT5 from qtpy.QtCore import Qt from qtpy.QtWebEngineWidgets import WEBENGINE -from qtpy.QtWidgets import QMessageBox +from qtpy.QtWidgets import QMessageBox, QWidget # Local imports from spyder.config.gui import get_color_scheme @@ -46,7 +50,6 @@ SHELL_TIMEOUT = 20000 TEMP_DIRECTORY = tempfile.gettempdir() NON_ASCII_DIR = osp.join(TEMP_DIRECTORY, u'測試', u'اختبار') -ASCII_DIR = osp.join(TEMP_DIRECTORY, 'username') # ============================================================================= @@ -77,6 +80,14 @@ class FaultyKernelSpec(KernelSpec): @pytest.fixture def ipyconsole(qtbot, request): """IPython console fixture.""" + + class MainWindowMock(QWidget): + def __getattr__(self, attr): + if attr == 'consoles_menu_actions': + return [] + else: + return Mock() + # Tests assume inline backend CONF.set('ipython_console', 'pylab/backend', 0) @@ -85,7 +96,7 @@ def ipyconsole(qtbot, request): if non_ascii_dir: test_dir = NON_ASCII_DIR else: - test_dir = ASCII_DIR + test_dir = None # Instruct the console to not use a stderr file no_stderr_file = request.node.get_marker('no_stderr_file') @@ -100,10 +111,10 @@ def ipyconsole(qtbot, request): CONF.set('ipython_console', 'pylab/backend', 1) # Create the console and a new client - console = IPythonConsole(parent=None, - testing=True, + console = IPythonConsole(parent=MainWindowMock(), test_dir=test_dir, test_no_stderr=test_no_stderr) + console.dockwidget = Mock() console.create_new_client() # Close callback diff --git a/spyder/plugins/tests/test_projects.py b/spyder/plugins/tests/test_projects.py index 7b4bf6ded76..2edf6277bb3 100644 --- a/spyder/plugins/tests/test_projects.py +++ b/spyder/plugins/tests/test_projects.py @@ -10,6 +10,12 @@ Tests for the Projects plugin. """ +# Standard library imports +try: + from unittest.mock import Mock +except ImportError: + from mock import Mock # Python 2 + # Third party imports import pytest @@ -23,21 +29,31 @@ # Fixtures # ============================================================================= @pytest.fixture -def projects(qtbot): - """Projects plugin fixture""" - projects = Projects(parent=None, testing=True) - qtbot.addWidget(projects) - return projects +def projects(qtbot, mocker): + """Projects plugin fixture.""" + class MainWindowMock(object): + def __getattr__(self, attr): + if attr == 'ipyconsole' or attr == 'editor': + return None + else: + return Mock() -@pytest.fixture -def projects_with_dockwindow(projects, mocker): - """Fixture for Projects plugin with a dockwindow""" + # Create plugin + projects = Projects(parent=None) + + # Patching necessary to test visible_if_project_open projects.shortcut = None mocker.patch.object(spyder.plugins.SpyderDockWidget, 'install_tab_event_filter') mocker.patch.object(projects, 'toggle_view_action') projects.create_dockwidget() + + # This can only be done at this point + projects.main = MainWindowMock() + + qtbot.addWidget(projects) + projects.show() return projects @@ -61,12 +77,9 @@ def test_open_project(projects, tmpdir, test_directory): @pytest.mark.parametrize('value', [True, False]) -def test_close_project_sets_visible_config(projects_with_dockwindow, tmpdir, - value): +def test_close_project_sets_visible_config(projects, tmpdir, value): """Test that when project is closed, the config option visible_if_project_open is set to the correct value.""" - projects = projects_with_dockwindow - # Set config to opposite value so that we can check that it's set correctly projects.set_option('visible_if_project_open', not value) @@ -80,11 +93,9 @@ def test_close_project_sets_visible_config(projects_with_dockwindow, tmpdir, @pytest.mark.parametrize('value', [True, False]) -def test_closing_plugin_sets_visible_config( - projects_with_dockwindow, tmpdir, value): +def test_closing_plugin_sets_visible_config(projects, tmpdir, value): """Test that closing_plugin() sets config option visible_if_project_open if a project is open.""" - projects = projects_with_dockwindow projects.set_option('visible_if_project_open', not value) projects.closing_plugin() @@ -101,11 +112,9 @@ def test_closing_plugin_sets_visible_config( @pytest.mark.parametrize('value', [True, False]) -def test_open_project_uses_visible_config( - projects_with_dockwindow, tmpdir, value): +def test_open_project_uses_visible_config(projects, tmpdir, value): """Test that when a project is opened, the project explorer is only opened if the config option visible_if_project_open is set.""" - projects = projects_with_dockwindow projects.set_option('visible_if_project_open', value) projects.open_project(path=to_text_string(tmpdir)) assert projects.dockwidget.isVisible() == value diff --git a/spyder/widgets/ipythonconsole/namespacebrowser.py b/spyder/widgets/ipythonconsole/namespacebrowser.py index 483263eef8b..0256641e5ff 100644 --- a/spyder/widgets/ipythonconsole/namespacebrowser.py +++ b/spyder/widgets/ipythonconsole/namespacebrowser.py @@ -70,9 +70,12 @@ def refresh_namespacebrowser(self): def set_namespace_view_settings(self): """Set the namespace view settings""" - settings = to_text_string(self.namespacebrowser.get_view_settings()) - code = u"get_ipython().kernel.namespace_view_settings = %s" % settings - self.silent_execute(code) + if self.namespacebrowser: + settings = to_text_string( + self.namespacebrowser.get_view_settings()) + code =(u"get_ipython().kernel.namespace_view_settings = %s" % + settings) + self.silent_execute(code) def get_value(self, name): """Ask kernel for a value""" diff --git a/spyder_breakpoints/breakpoints.py b/spyder_breakpoints/breakpoints.py index 8203bec8f5d..90561abf340 100644 --- a/spyder_breakpoints/breakpoints.py +++ b/spyder_breakpoints/breakpoints.py @@ -98,7 +98,4 @@ def apply_plugin_settings(self, options): def show(self): """Show the breakpoints dockwidget""" - if self.dockwidget and not self.ismaximized: - self.dockwidget.setVisible(True) - self.dockwidget.setFocus() - self.dockwidget.raise_() + self.switch_to_plugin() diff --git a/spyder_profiler/profiler.py b/spyder_profiler/profiler.py index 19c01bd7a00..8aa062b347f 100644 --- a/spyder_profiler/profiler.py +++ b/spyder_profiler/profiler.py @@ -129,6 +129,7 @@ def apply_plugin_settings(self, options): def run_profiler(self): """Run profiler""" if self.main.editor.save(): + self.switch_to_plugin() self.analyze(self.main.editor.get_current_filename()) def analyze(self, filename): diff --git a/spyder_pylint/pylint.py b/spyder_pylint/pylint.py index 94127ff20b0..555d4046e54 100644 --- a/spyder_pylint/pylint.py +++ b/spyder_pylint/pylint.py @@ -168,10 +168,11 @@ def change_history_depth(self): @Slot() def run_pylint(self): """Run pylint code analysis""" - if self.get_option('save_before', True)\ - and not self.main.editor.save(): + if (self.get_option('save_before', True) + and not self.main.editor.save()): return - self.analyze( self.main.editor.get_current_filename() ) + self.switch_to_plugin() + self.analyze(self.main.editor.get_current_filename()) def analyze(self, filename): """Reimplement analyze method"""