Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Allow plugins to hook into File > Open #22564

Draft
wants to merge 43 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4b0a476
Move "Open file" action from Editor to Application plugin
jitseniesen Sep 27, 2024
00a2b3a
Move code for Open Files dialog from Editor to Application plugin
jitseniesen Sep 30, 2024
8ada661
Move code communicating with Editor to plugin level
jitseniesen Sep 30, 2024
448f900
Get information from Editor plugin properly
jitseniesen Oct 1, 2024
61aefd6
Unify triggering run and debug actions in editor
jitseniesen Oct 20, 2024
a7ccc85
Move "Open file" shortcut from Editor to Application plugin
jitseniesen Oct 2, 2024
92d2e3e
Call load() in Editor plugin properly from Application container
jitseniesen Oct 2, 2024
c5336e6
Connect Explorer and Projects plugins to open_file_in_plugin()
jitseniesen Oct 13, 2024
ff5362f
Allow files to be opened in another plugin than the editor
jitseniesen Oct 7, 2024
40e012d
Emit signal when focused plugin changes
jitseniesen Nov 28, 2024
5929b54
Allow other plugins to provide current filename
jitseniesen Oct 6, 2024
16fa211
Hook Variable Explorer into Open File action
jitseniesen Oct 19, 2024
66c5c2e
Use Application plugin to open files given on command line
jitseniesen Oct 20, 2024
c46f6a4
Use signal to call open_file_in_dialog() in Application container
jitseniesen Oct 20, 2024
5b10df5
Bug fix in SpyderMenu._add_section
jitseniesen Oct 21, 2024
23db9f9
Move "New file" action from Editor to Application plugin
jitseniesen Oct 20, 2024
2690409
Allow other plugins to hook into the "New file" action
jitseniesen Oct 26, 2024
7cdda57
Move "Open last closed" menu item to Application plugin
jitseniesen Nov 4, 2024
5ac557c
Allow plugins to hook into "Open last closed" action
jitseniesen Nov 10, 2024
b457dbb
Move "File > Open recent" from Editor to Application plugin
jitseniesen Nov 12, 2024
23cd4ec
Move "Save file" from Editor to Application plugin.
jitseniesen Nov 24, 2024
4c826e9
Allow other plugins to hook into "Save file" action
jitseniesen Dec 14, 2024
4cd6a78
Allow other plugins to enable or disable file actions
jitseniesen Dec 14, 2024
16babb9
Move "Save all" from Editor to Application plugin
jitseniesen Dec 10, 2024
3832176
Allow plugins to hook into the "Save all" action
jitseniesen Dec 14, 2024
a0a1c7d
Move "Save as" from Editor to Application plugin
jitseniesen Dec 14, 2024
fbb4bba
Allow plugins to hook into the "Save as" action
jitseniesen Dec 14, 2024
c60c66b
Move "Save copy as" from Editor to Application plugin
jitseniesen Dec 14, 2024
95614b4
Allow plugins to hook into the "Save copy as" action
jitseniesen Dec 18, 2024
c8683b0
Move "Revert file" from Editor to Application plugin
jitseniesen Dec 23, 2024
b44ed69
Allow plugins to hook into the "Revert file" action
jitseniesen Dec 23, 2024
2200307
Move "Close file" from Editor to Application plugin
jitseniesen Dec 23, 2024
b00bfa6
Allow plugins to hook into the "Close file" action
jitseniesen Dec 26, 2024
974a6ea
Move "Close all" from Editor to Application plugin
jitseniesen Dec 27, 2024
e561527
Allow plugins to hook into "Close all" action
jitseniesen Dec 27, 2024
91e83ac
TMP: Avoid ImportError when running editor tests
jitseniesen Dec 30, 2024
0e93224
Remove EditorStack.set_io_actions()
jitseniesen Dec 30, 2024
6c950d3
Python 3.8 compatibility fixes
jitseniesen Dec 30, 2024
cce1dec
TBF ... fix test_save_when_completions_are_visible
jitseniesen Dec 30, 2024
e27a914
TBF ... fix test_save_when_completions_are_visible 2
jitseniesen Dec 30, 2024
edca1ef
TBF ... Use plugin registry in change_last_focused_widget
jitseniesen Dec 31, 2024
67fead7
TBF ... Use open_file_in_plugin in test_pylint_follows_file
jitseniesen Dec 31, 2024
ee0ea9a
TBF ... Handle application plugin not defined in _enable_file_action
jitseniesen Dec 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 153 additions & 1 deletion spyder/api/plugins/new_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import os
import os.path as osp
import sys
from typing import List, Union
from typing import List, Optional, Union
import warnings

# Third party imports
Expand Down Expand Up @@ -298,6 +298,17 @@ class SpyderPluginV2(QObject, SpyderActionMixin, SpyderConfigurationObserver,
The window state.
"""

sig_focused_plugin_changed = Signal(object)
"""
This signal is emitted when the plugin with keyboard focus changes.

Parameters
----------
plugin: Optional[SpyderDockablePlugin]
The plugin that currently has keyboard focus, or None if no dockable
plugin has focus.
"""

# ---- Private attributes
# -------------------------------------------------------------------------
# Define configuration name map for plugin to split configuration
Expand Down Expand Up @@ -972,6 +983,18 @@ class SpyderDockablePlugin(SpyderPluginV2):
# the action to switch is called a second time.
RAISE_AND_FOCUS = False

# List of file extensions which the plugin can open.
# If the user opens a file with one of these extensions, then the file
# will open in this plugin using the `open_file` function.
# Example: ['.ipynb'] for spyder-notebook
FILE_EXTENSIONS = []

# Whether the plugin can handle file actions.
# If set to true, then the `create_new_file`, `open_last_closed`,
# `save_file`, `save_file_as`, `save_copy_as`, `save_all` and `revert_file`
# functions will be called to handle the corresponding actions.
CAN_HANDLE_FILE_ACTIONS = False

# ---- API: Available signals
# -------------------------------------------------------------------------
sig_focus_changed = Signal()
Expand Down Expand Up @@ -1053,6 +1076,135 @@ def __init__(self, parent, configuration):
widget.sig_update_ancestor_requested.connect(
self.sig_update_ancestor_requested)

# ---- API: Optional methods to override
# -------------------------------------------------------------------------
def create_new_file(self) -> None:
"""
Create a new file inside the plugin.

This function will be called if the user create a new file using
the `File > New` menu item or the "New file" button in the toolbar,
and `CAN_HANDLE_FILE_ACTIONS` is set to `True`.
"""
raise NotImplementedError

def open_file(self, filename: str):
"""
Open file inside plugin.

This method will be called if the user wants to open a file with one
of the file name extensions listed in `FILE_EXTENSIONS`, so you need
to define that variable too.

Parameters
----------
filename: str
The name of the file to be opened.
"""
raise NotImplementedError

def open_last_closed_file(self) -> None:
"""
Open the last closed file again.

This function will be called if the `File > Open last closed` menu item
is selected while the plugin has focus and `CAN_HANDLE_FILE_ACTIONS`
is set to `True`.
"""
raise NotImplementedError

def save_file(self) -> None:
"""
Save the current file.

This function will be called if the user saves the current file using
the `File > Save` menu item or the "Save file" button in the toolbar,
the plugin has focus, and `CAN_HANDLE_FILE_ACTIONS` is set to `True`.
"""
raise NotImplementedError

def save_file_as(self) -> None:
"""
Save the current file under a different name.

This function will be called if the `File > Save as` menu item is
selected while the plugin has focus and `CAN_HANDLE_FILE_ACTIONS` is
set to `True`.
"""
raise NotImplementedError

def save_copy_as(self) -> None:
"""
Save a copy of the current file under a different name.

This function will be called if the `File > Save copy as` menu item is
selected while the plugin has focus and `CAN_HANDLE_FILE_ACTIONS` is
set to `True`.
"""
raise NotImplementedError

def save_all(self) -> None:
"""
Save all files that are opened in the plugin.

This function will be called if the user saves all files using the
`File > Save all` menu item or the "Save all" button in the toolbar,
the plugin has focus, and `CAN_HANDLE_FILE_ACTIONS` is set to `True`.
"""
raise NotImplementedError

def revert_file(self) -> None:
"""
Revert the current file to the version stored on disk.

This function will be called if the `File > Revert` menu item is
selected while the plugin has focus and `CAN_HANDLE_FILE_ACTIONS` is
set to `True`.
"""
raise NotImplementedError

def close_file(self) -> None:
"""
Close the current file.

This function will be called if the `File > Close` menu item is
selected while the plugin has focus and `CAN_HANDLE_FILE_ACTIONS` is
set to `True`.
"""
raise NotImplementedError

def close_all(self) -> None:
"""
Close all opened files.

This function will be called if the `File > Close all` menu item is
selected while the plugin has focus and `CAN_HANDLE_FILE_ACTIONS` is
set to `True`.
"""
raise NotImplementedError

def get_current_filename(self) -> Optional[str]:
"""
Return file name of the file that is currently displayed.

This is meant for plugins like the Editor or Notebook plugin which
editor display files. Return `None` if no file is displayed or if this
does not display files.

This function is used in the `Open file` action to initialize the
"Open file" dialog.
"""
return None

def current_file_is_temporary(self) -> bool:
"""
Return whether currently displayed file is a temporary file.

This function should only be called if a file is displayed, that is,
if `self.get_current_filename()` does not return `None`.
"""
return False

# ---- API: available methods
# -------------------------------------------------------------------------
def before_long_process(self, message):
Expand Down
1 change: 0 additions & 1 deletion spyder/api/widgets/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,6 @@ def _add_section(self, section, before_section=None):
self._sections.remove(after_section)

idx = self._sections.index(section)
idx = idx if (idx == 0) else (idx - 1)
self._sections.insert(idx, after_section)

def _set_icons(self):
Expand Down
73 changes: 48 additions & 25 deletions spyder/app/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ class MainWindow(QMainWindow, SpyderMainWindowMixin, SpyderShortcutsMixin):
The window state.
"""

sig_focused_plugin_changed = Signal(object)
"""
This signal is emitted when the another plugin received keyboard focus.

Parameters
----------
plugin: Optional[SpyderDockablePlugin]
The plugin that currently has keyboard focus, or None if no dockable
plugin has focus.
"""

def __init__(self, splash=None, options=None):
QMainWindow.__init__(self)
qapp = QApplication.instance()
Expand Down Expand Up @@ -275,6 +286,7 @@ def signal_handler(signum, frame=None):

# To keep track of the last focused widget
self.last_focused_widget = None
self.last_focused_plugin = None
self.previous_focused_widget = None

# Server to open external files on a single instance
Expand Down Expand Up @@ -419,6 +431,8 @@ def register_plugin(self, plugin_name, external=False, omit_conf=False):
self.sig_resized.connect(plugin.sig_mainwindow_resized)
self.sig_window_state_changed.connect(
plugin.sig_mainwindow_state_changed)
self.sig_focused_plugin_changed.connect(
plugin.sig_focused_plugin_changed)

# Register plugin
plugin._register(omit_conf=omit_conf)
Expand Down Expand Up @@ -1063,14 +1077,43 @@ def set_splash(self, message):
QApplication.processEvents()

def change_last_focused_widget(self, old, now):
"""To keep track of to the last focused widget"""
"""
Keep track of widget and plugin that has keyboard focus.

This function is connected to the `focusChanged` signal. It keeps
track of the widget and plugin that currently has keyboard focus, so
that we can give focus to the last focused widget when restoring it
after minimization. It also emits `sig_focused_plugin_changed` if the
plugin with focus has changed.

Parameters
----------
old : Optional[QWidget]
Widget that used to have keyboard focus.
now : Optional[QWidget]
Widget that currently has keyboard focus.
"""
if (now is None and QApplication.activeWindow() is not None):
QApplication.activeWindow().setFocus()
self.last_focused_widget = QApplication.focusWidget()
elif now is not None:
self.last_focused_widget = now

self.previous_focused_widget = old
self.previous_focused_widget = old

if self.last_focused_widget:
for plugin_name, plugin in self.get_dockable_plugins():
if self.is_plugin_available(plugin_name):
plugin_widget = plugin.get_widget()
if plugin_widget.isAncestorOf(self.last_focused_widget):
focused_plugin = plugin
break
else:
focused_plugin = None

if focused_plugin != self.last_focused_plugin:
self.last_focused_plugin = focused_plugin
self.sig_focused_plugin_changed.emit(focused_plugin)

def closing(self, cancelable=False, close_immediately=False):
"""Exit tasks"""
Expand Down Expand Up @@ -1164,27 +1207,6 @@ def redirect_internalshell_stdio(self, state):
else:
console.restore_stds()

def open_file(self, fname, external=False):
"""
Open filename with the appropriate application
Redirect to the right widget (txt -> editor, spydata -> workspace, ...)
or open file outside Spyder (if extension is not supported)
"""
fname = to_text_string(fname)
ext = osp.splitext(fname)[1]
editor = self.get_plugin(Plugins.Editor, error=False)
variableexplorer = self.get_plugin(
Plugins.VariableExplorer, error=False)

if encoding.is_text_file(fname):
if editor:
editor.load(fname)
elif variableexplorer is not None and ext in IMPORT_EXT:
variableexplorer.get_widget().import_data(fname)
elif not external:
fname = file_uri(fname)
start_file(fname)

def get_initial_working_directory(self):
"""Return the initial working directory."""
return self.INITIAL_CWD
Expand All @@ -1209,8 +1231,9 @@ def open_external_file(self, fname):
if sys.platform == 'darwin' and 'bin/spyder' in fname:
return

if osp.isfile(fpath):
self.open_file(fpath, external=True)
application = self.get_plugin(Plugins.Application, error=False)
if osp.isfile(fpath) and application:
application.open_file_in_plugin(fpath)
elif osp.isdir(fpath):
QMessageBox.warning(
self, _("Error"),
Expand Down
5 changes: 3 additions & 2 deletions spyder/app/tests/test_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3001,6 +3001,7 @@ def test_pylint_follows_file(qtbot, tmpdir, main_window):
timeout=SHELL_TIMEOUT)

pylint_plugin = main_window.get_plugin(Plugins.Pylint)
application_plugin = main_window.get_plugin(Plugins.Application)

# Show pylint plugin
pylint_plugin.dockwidget.show()
Expand All @@ -3014,7 +3015,7 @@ def test_pylint_follows_file(qtbot, tmpdir, main_window):
fh = basedir.join('{}.py'.format(idx))
fname = str(fh)
fh.write('print("Hello world!")')
main_window.open_file(fh)
application_plugin.open_file_in_plugin(fname)
qtbot.wait(200)
assert fname == pylint_plugin.get_filename()

Expand All @@ -3029,7 +3030,7 @@ def test_pylint_follows_file(qtbot, tmpdir, main_window):
fh = basedir.join('{}.py'.format(idx))
fh.write('print("Hello world!")')
fname = str(fh)
main_window.open_file(fh)
application_plugin.open_file_in_plugin(fname)
qtbot.wait(200)
assert fname == pylint_plugin.get_filename()

Expand Down
Loading
Loading