From f91e9f0cc7d2eb88e31c01cdeac5044b39edbfa1 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Fri, 15 Nov 2024 11:59:36 +0000 Subject: [PATCH] can now open matlab file in matlab --- rascal2/core/__init__.py | 3 +- rascal2/core/matlab.py | 40 +++++++++++++++++++++++++ rascal2/dialogs/custom_file_editor.py | 34 ++++++++++++++++++---- rascal2/widgets/project/models.py | 42 +++++++++++++++++++++++---- 4 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 rascal2/core/matlab.py diff --git a/rascal2/core/__init__.py b/rascal2/core/__init__.py index c1b9a02..a0d8570 100644 --- a/rascal2/core/__init__.py +++ b/rascal2/core/__init__.py @@ -1,4 +1,5 @@ from rascal2.core.runner import RATRunner from rascal2.core.settings import Settings, get_global_settings +from rascal2.core.matlab import MatlabHandler -__all__ = ["RATRunner", "get_global_settings", "Settings"] +__all__ = ["MatlabHandler", "RATRunner", "get_global_settings", "Settings"] diff --git a/rascal2/core/matlab.py b/rascal2/core/matlab.py new file mode 100644 index 0000000..527ae77 --- /dev/null +++ b/rascal2/core/matlab.py @@ -0,0 +1,40 @@ +"""MATLAB engine that runs in the background.""" + +from contextlib import suppress + + +class MatlabHandler: + """A singleton class that provides a MATLAB engine.""" + + _instance = None + + def __init__(self): + raise RuntimeError("MatlabHandler should not be invoked directly. " "Use MatlabHandler.instance().") + + @classmethod + def instance(cls): + """Instantiate a MatlabHandler if one does not exist, or return the existing one if one exists.""" + if cls._instance is None: + cls._instance = cls.__new__(cls) + cls._instance.init() + return cls._instance + + def init(self): + """Instantiate the MatlabHandler.""" + self.future = None + self.engine = None + with suppress(ImportError): + import matlab.engine + + self.future = matlab.engine.start_matlab(background=True) + + def get_engine(self): + """Get the MATLAB engine.""" + if self.engine is not None: + return self.engine + + if self.future is None: + raise ImportError("Attempted to start MATLAB engine, but `matlabengine` is not installed!") + + self.engine = self.future.result() + return self.engine diff --git a/rascal2/dialogs/custom_file_editor.py b/rascal2/dialogs/custom_file_editor.py index 24eee2f..39b27fa 100644 --- a/rascal2/dialogs/custom_file_editor.py +++ b/rascal2/dialogs/custom_file_editor.py @@ -1,10 +1,13 @@ """Dialogs for editing custom files.""" +import logging from pathlib import Path from PyQt6 import Qsci, QtWidgets from RATapi.utils.enums import Languages +from rascal2.core import MatlabHandler + def edit_file(filename: str, language: Languages, parent: QtWidgets.QWidget): """Edit a file in the file editor. @@ -19,17 +22,36 @@ def edit_file(filename: str, language: Languages, parent: QtWidgets.QWidget): The parent of this widget. """ - dialog = CustomFileEditorDialog(filename, language, parent) + file = Path(filename) + if not file.is_file(): + logger = logging.getLogger("rascal_log") + logger.error("Attempted to edit a custom file which does not exist!") + return + + dialog = CustomFileEditorDialog(file, language, parent) dialog.exec() +def edit_file_matlab(filename: str): + """Open a file in MATLAB.""" + handler = MatlabHandler.instance() + try: + engine = handler.get_engine() + except ImportError as err: + logger = logging.getLogger("rascal_log") + logger.error(err) + return + + engine.edit(filename) + + class CustomFileEditorDialog(QtWidgets.QDialog): """Dialog for editing custom files. Parameters ---------- - filename : str - The name of the file to edit. + file : pathlib.Path + The file to edit. language : Languages The language for dialog highlighting. parent : QtWidgets.QWidget @@ -37,13 +59,13 @@ class CustomFileEditorDialog(QtWidgets.QDialog): """ - def __init__(self, filename, language, parent): + def __init__(self, file, language, parent): super().__init__(parent) self.setMinimumWidth(600) self.setMinimumHeight(400) - self.file = Path(filename) + self.file = file self.editor = Qsci.QsciScintilla() match language: @@ -70,7 +92,7 @@ def __init__(self, filename, language, parent): layout.addLayout(button_layout) self.setLayout(layout) - self.setWindowTitle(f"Edit {filename}") + self.setWindowTitle(f"Edit {str(file)}") def save_file(self): """Save and close the file.""" diff --git a/rascal2/widgets/project/models.py b/rascal2/widgets/project/models.py index 0c87686..705a61a 100644 --- a/rascal2/widgets/project/models.py +++ b/rascal2/widgets/project/models.py @@ -12,7 +12,7 @@ import rascal2.widgets.delegates as delegates from rascal2.config import path_for -from rascal2.dialogs.custom_file_editor import edit_file +from rascal2.dialogs.custom_file_editor import edit_file, edit_file_matlab class ClassListModel(QtCore.QAbstractTableModel): @@ -457,7 +457,6 @@ def setData(self, index, value, role=QtCore.Qt.ItemDataRole.DisplayRole): row = index.row() self.classlist[row].path = file_path.parent self.classlist[row].filename = str(file_path.name) - self.dataChanged.emit(index, index) # auto-set language from file extension if possible # & get file names for dropdown on Python @@ -485,6 +484,7 @@ def setData(self, index, value, role=QtCore.Qt.ItemDataRole.DisplayRole): if language is not None: self.classlist[row].language = language + self.dataChanged.emit(index, index) return True return super().setData(index, value, role) @@ -520,11 +520,43 @@ def edit(self): def make_edit_button(self, index): button = QtWidgets.QPushButton("Edit File", self.table) + q_scintilla_action = QtGui.QAction("Edit in RasCAL-2...", self.table) + q_scintilla_action.triggered.connect( + lambda: edit_file( + self.model.classlist[index].path / self.model.classlist[index].filename, + self.model.classlist[index].language, + self, + ) + ) + matlab_action = QtGui.QAction("Edit in MATLAB...", self.table) + matlab_action.triggered.connect( + lambda: edit_file_matlab(self.model.classlist[index].path / self.model.classlist[index].filename) + ) + menu = QtWidgets.QMenu(self.table) + menu.addActions([q_scintilla_action, matlab_action]) def file_editable(): - return ( - self.model.data(self.model.index(index, self.model.headers.index("language") + 1)) != Languages.Cpp - ) and (self.model.data(self.model.index(index, self.model.headers.index("filename") + 1)) != "Browse...") + language = self.model.data(self.model.index(index, self.model.headers.index("language") + 1)) + with contextlib.suppress(TypeError): + button.pressed.disconnect() + match language: + case Languages.Matlab: + button.setMenu(menu) + button.pressed.connect(button.showMenu) + case Languages.Python: + button.setMenu(None) + button.pressed.connect( + lambda: edit_file( + self.model.classlist[index].path / self.model.classlist[index].filename, + Languages.Python, + self, + ) + ) + + editable = (language != Languages.Cpp) and ( + self.model.data(self.model.index(index, self.model.headers.index("filename") + 1)) != "Browse..." + ) + return editable button.setEnabled(file_editable()) self.model.dataChanged.connect(lambda: button.setEnabled(file_editable()))