Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 7 additions & 13 deletions rascal2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def run_matlab(ready_event, close_event, engine_output):
eng.quit()


def get_matlab_engine(engine_ready, engine_output, is_local=False):
def get_matlab_engine(engine_ready, engine_output):
"""Get a MATLAB engine from the MatlabHelper or exception if no engine is available.

Parameters
Expand All @@ -129,8 +129,6 @@ def get_matlab_engine(engine_ready, engine_output, is_local=False):
An event to inform listeners that MATLAB is ready.
engine_output : multiprocessing.Manager.list
A list with the name of MATLAB engine instance or an exception from the MatlabHelper.
is_local : bool, default False
Indicates a local engine should be created other connect ratapi.

Returns
-------
Expand All @@ -143,17 +141,13 @@ def get_matlab_engine(engine_ready, engine_output, is_local=False):
if engine_output:
if isinstance(engine_output[0], bytes):
engine_name = engine_output[0].decode("utf-8")
if is_local:
import matlab.engine

engine_future = matlab.engine.connect_matlab(engine_name, background=True)
else:
import ratapi
import ratapi

engine_future = ratapi.wrappers.use_shared_matlab(
engine_name,
"Error occurred when connecting to MATLAB, please ensure MATLAB is installed and set up properly.",
)
engine_future = ratapi.wrappers.use_shared_matlab(
engine_name,
"Error occurred when connecting to MATLAB, please ensure MATLAB is installed and set up properly.",
)

return engine_future
elif isinstance(engine_output[0], Exception):
Expand Down Expand Up @@ -208,7 +202,7 @@ def get_local_engine(self):
if self.__engine is not None:
return self.__engine

result = get_matlab_engine(self.ready_event, self.engine_output, True)
result = get_matlab_engine(self.ready_event, self.engine_output)
if isinstance(result, Exception):
raise result

Expand Down
140 changes: 109 additions & 31 deletions rascal2/dialogs/custom_file_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ def edit_file(filename: str, language: Languages, parent: QtWidgets.QWidget):
LOGGER.error("Attempted to edit a custom file which does not exist!")
return

dialog = CustomFileEditorDialog(file, language, parent)
dialog.exec()
dialog = CustomFileEditorDialog(parent)
dialog.open_file(file, language)
dialog.setModal(False)
dialog.show()


def edit_file_matlab(filename: str):
Expand All @@ -41,25 +43,34 @@ def edit_file_matlab(filename: str):
engine.edit(str(filename))


class CustomFileEditorDialog(QtWidgets.QDialog):
class Singleton(type(QtWidgets.QDialog), type):
"""Metaclass used to create a PyQt singleton."""

def __init__(cls, name, bases, cls_dict):
super().__init__(name, bases, cls_dict)
cls._instance = None

def __call__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance


class CustomFileEditorDialog(QtWidgets.QDialog, metaclass=Singleton):
"""Dialog for editing custom files.

Parameters
----------
file : pathlib.Path
The file to edit.
language : Languages
The language for dialog highlighting.
parent : QtWidgets.QWidget
The parent of this widget.

"""

def __init__(self, file, language, parent):
def __init__(self, parent):
super().__init__(parent)

self.file = file

self.file = None
self.unchanged_text = ""
self.editor = Qsci.QsciScintilla()
self.editor.setBraceMatching(Qsci.QsciScintilla.BraceMatch.SloppyBraceMatch)
self.editor.setCaretLineVisible(True)
Expand All @@ -73,39 +84,26 @@ def __init__(self, file, language, parent):
self.editor.setAutoIndent(True)
self.editor.setTabWidth(4)

match language:
case Languages.Python:
self.editor.setLexer(Qsci.QsciLexerPython(self.editor))
case Languages.Matlab:
self.editor.setLexer(Qsci.QsciLexerMatlab(self.editor))
case _:
self.editor.setLexer(None)

# Set the default font
font = QtGui.QFont("Courier", 10)
font.setFixedPitch(True)
font = self.default_font
self.editor.setFont(font)

# Margin 0 is used for line numbers
font_metrics = QtGui.QFontMetrics(font)
self.editor.setMarginsFont(font)
self.editor.setMarginWidth(0, font_metrics.horizontalAdvance("00000") + 6)
self.editor.setMarginLineNumbers(0, True)
self.editor.setMarginsBackgroundColor(QtGui.QColor("#cccccc"))

if self.editor.lexer() is not None:
self.editor.lexer().setFont(font)
self.editor.setText(self.file.read_text())
self.editor.textChanged.connect(self.show_modified)

save_button = QtWidgets.QPushButton("Save", self)
save_button.clicked.connect(self.save_file)
cancel_button = QtWidgets.QPushButton("Cancel", self)
cancel_button.clicked.connect(self.reject)
close_button = QtWidgets.QPushButton("Close", self)
close_button.clicked.connect(self.reject)

button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch(1)
button_layout.addWidget(save_button)
button_layout.addWidget(cancel_button)
button_layout.addWidget(close_button)
button_layout.addSpacing(10)

layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.editor)
Expand All @@ -115,19 +113,99 @@ def __init__(self, file, language, parent):
self.setMinimumWidth(800)
self.setMinimumHeight(600)
layout.setContentsMargins(0, 0, 0, 0)

@property
def default_font(self):
"""Return default editor font.

Returns
-------
font : QtGui.QFont
The default font.

"""
# Set the default font
font = QtGui.QFont("Courier", 10)
font.setFixedPitch(True)
return font

@property
def is_modified(self):
"""Return if document is modified.

Returns
-------
modified : bool
Indicates if document is modified.

"""
return self.unchanged_text != self.editor.text()

def show_modified(self):
"""Show modified state in window title."""
pre = "* " if self.is_modified else ""
self.setWindowTitle(f"{pre}Edit {str(self.file)}")

def open_file(self, file, language):
"""Open a custom file.

Parameters
----------
file : pathlib.Path
The file to edit.
language : Languages
The language for dialog highlighting.
"""
if file == self.file:
return # file is already opened

if self.is_modified:
result = QtWidgets.QMessageBox.question(
self,
"Save File",
"Do you want to save changes to this file?",
QtWidgets.QMessageBox.StandardButton.Discard | QtWidgets.QMessageBox.StandardButton.Save,
QtWidgets.QMessageBox.StandardButton.Save,
)
if result == QtWidgets.QMessageBox.StandardButton.Save:
self.save_file()

self.file = file
self.setWindowTitle(f"Edit {str(file)}")
match language:
case Languages.Python:
self.editor.setLexer(Qsci.QsciLexerPython(self.editor))
case Languages.Matlab:
self.editor.setLexer(Qsci.QsciLexerMatlab(self.editor))
case _:
self.editor.setLexer(None)

if self.editor.lexer() is not None:
self.editor.lexer().setFont(self.default_font)
self.unchanged_text = self.file.read_text()
self.editor.setText(self.unchanged_text)
self.editor.setModified(False)
self.setWindowModified(False)

def save_file(self):
"""Save and close the file."""
"""Save the custom file."""
if not self.is_modified:
return

if self.file.is_relative_to(EXAMPLES_PATH):
message = "Files cannot be saved into the examples directory, please copy the file to another directory."
QtWidgets.QMessageBox.warning(self, "Save File", message, QtWidgets.QMessageBox.StandardButton.Ok)
return

try:
self.file.write_text(self.editor.text())
self.accept()
self.unchanged_text = self.editor.text()
self.show_modified()
except OSError as ex:
message = f"Failed to save custom file to {self.file}.\n"
LOGGER.error(message, exc_info=ex)
QtWidgets.QMessageBox.critical(self, "Save File", message, QtWidgets.QMessageBox.StandardButton.Ok)

def reject(self):
CustomFileEditorDialog._instance = None
super().reject()
6 changes: 2 additions & 4 deletions rascal2/ui/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import ratapi as rat
import ratapi.wrappers

from rascal2.config import LOGGER, MatlabHelper, get_matlab_engine
from rascal2.config import LOGGER, MatlabHelper
from rascal2.core import commands
from rascal2.core.enums import UnsavedReply
from rascal2.core.runner import LogData, RATRunner
Expand Down Expand Up @@ -190,9 +190,7 @@ def quick_run(self, project=None):
[file.language == "matlab" for file in self.model.project.custom_files]
):
matlab_helper = MatlabHelper()
result = get_matlab_engine(matlab_helper.ready_event, matlab_helper.engine_output)
if isinstance(result, Exception):
raise result
matlab_helper.get_local_engine()
return rat.run(project, rat.Controls(display="off"))[1]

def run(self):
Expand Down
5 changes: 0 additions & 5 deletions rascal2/widgets/delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,6 @@ def createEditor(self, parent, option, index):
max_val = float("inf")
min_val = -float("inf")

if self.field in ["min", "value"]:
max_val = index.siblingAtColumn(index.column() + 1).data(QtCore.Qt.ItemDataRole.DisplayRole)
if self.field in ["value", "max"]:
min_val = index.siblingAtColumn(index.column() - 1).data(QtCore.Qt.ItemDataRole.DisplayRole)

widget.setMinimum(min_val)
widget.setMaximum(max_val)

Expand Down
34 changes: 33 additions & 1 deletion rascal2/widgets/project/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool:
if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole:
row = index.row()
param = self.index_header(index)
if self.index_header(index) == "fit":
if param == "fit":
value = QtCore.Qt.CheckState(value) == QtCore.Qt.CheckState.Checked
if param is not None:
current_value = getattr(self.classlist[index.row()], param)
Expand Down Expand Up @@ -356,6 +356,38 @@ def flags(self, index):

return flags

def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool:
param = self.index_header(index)
if param == "min":
min_value = value
value_model_index = index.siblingAtColumn(index.column() + 1)
max_model_index = index.siblingAtColumn(index.column() + 2)

if min_value > max_model_index.data(QtCore.Qt.ItemDataRole.DisplayRole):
super().setData(max_model_index, min_value, role)
if min_value > value_model_index.data(QtCore.Qt.ItemDataRole.DisplayRole):
super().setData(value_model_index, min_value, role)

elif param == "value":
min_model_index = index.siblingAtColumn(index.column() - 1)
actual_value = value
max_model_index = index.siblingAtColumn(index.column() + 1)
if actual_value < min_model_index.data(QtCore.Qt.ItemDataRole.DisplayRole):
super().setData(min_model_index, actual_value, role)
if actual_value > max_model_index.data(QtCore.Qt.ItemDataRole.DisplayRole):
super().setData(max_model_index, actual_value, role)

elif param == "max":
min_model_index = index.siblingAtColumn(index.column() - 2)
value_model_index = index.siblingAtColumn(index.column() - 1)
max_value = value
if max_value < min_model_index.data(QtCore.Qt.ItemDataRole.DisplayRole):
super().setData(min_model_index, max_value, role)
if max_value < value_model_index.data(QtCore.Qt.ItemDataRole.DisplayRole):
super().setData(value_model_index, max_value, role)

return super().setData(index, value, role)


class ParameterFieldWidget(ProjectFieldWidget):
"""Subclass of field widgets for parameters."""
Expand Down
Loading
Loading