diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py index 48a1357d9..b58d8b198 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py @@ -48,8 +48,11 @@ def configure(self, value): self._original_input = value InputFileConfigurator.configure(self, value) - - inputTraj = IInputData.create("HDFTrajectoryInputData", self["value"]) + try: + inputTraj = IInputData.create("HDFTrajectoryInputData", self["value"]) + except KeyError: + self.error_status = f"Could not use {value} as input trajectory" + return self["hdf_trajectory"] = inputTraj diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py index 521ed3499..bbb047b0c 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py @@ -52,6 +52,7 @@ def __init__(self, name, formats=None, **kwargs): self._formats = ( formats if formats is not None else OutputFilesConfigurator._default[-1] ) + self._forbidden_files = [] def configure(self, value): """ @@ -98,7 +99,16 @@ def configure(self, value): self["root"] = root self["formats"] = formats - self["files"] = ["%s%s" % (root, IFormat.create(f).extension) for f in formats] + self["files"] = [] + for extension in [IFormat.create(f).extension for f in formats]: + if extension in root[-len(extension) :]: + self["files"].append(root) + else: + self["files"].append(root + extension) + for file in self["files"]: + if os.path.abspath(file) in self._forbidden_files: + self.error_status = f"File {file} is either open or being written into. Please pick another name." + return self["value"] = self["files"] self["log_level"] = logs diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py index 2b81e4f66..e7855f563 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py @@ -51,6 +51,7 @@ def __init__(self, name, formats=None, **kwargs): IConfigurator.__init__(self, name, **kwargs) self._formats = [fmt for fmt in ioformats if ioformats[fmt].can_write] + self._forbidden_files = [] def configure(self, value): """ @@ -87,6 +88,9 @@ def configure(self, value): self["root"] = root self["format"] = format self["file"] = root + if os.path.abspath(self["file"]) in self._forbidden_files: + self.error_status = f"File {self['file']} is either open or being written into. Please pick another name." + return self["log_level"] = logs if logs == "no logs": self["write_logs"] = False diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py index baf628082..ab575e3c6 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py @@ -54,6 +54,7 @@ def __init__(self, name, format=None, **kwargs): self._format = "MDTFormat" self._dtype = np.float64 self._compression = "none" + self._forbidden_files = [] def configure(self, value: tuple): self._original_input = value @@ -95,6 +96,9 @@ def configure(self, value: tuple): if not self["extension"] in temp_name[-5:]: # capture most extension lengths temp_name += self["extension"] self["file"] = temp_name + if os.path.abspath(self["file"]) in self._forbidden_files: + self.error_status = f"File {self['file']} is either open or being written into. Please pick another name." + return self["dtype"] = self._dtype self["compression"] = self._compression self["log_level"] = logs diff --git a/MDANSE/Src/MDANSE/Framework/Converters/DCD.py b/MDANSE/Src/MDANSE/Framework/Converters/DCD.py index 648123e9e..63b20b7a9 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/DCD.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/DCD.py @@ -275,6 +275,8 @@ class DCD(Converter): Converts a DCD trajectory to a HDF trajectory. """ + label = "DCD" + settings = collections.OrderedDict() settings["pdb_file"] = ( "InputFileConfigurator", diff --git a/MDANSE/Src/MDANSE/Framework/Converters/VASP.py b/MDANSE/Src/MDANSE/Framework/Converters/VASP.py index 2436db2be..4d15949b2 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/VASP.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/VASP.py @@ -34,7 +34,7 @@ class VASP(Converter): Converts a VASP trajectory to a HDF trajectory. """ - label = "VASP (>=5)" + label = "VASP v5" settings = collections.OrderedDict() settings["xdatcar_file"] = ( diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py b/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py index 52d6eaecb..ecd2a73d3 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py @@ -26,7 +26,7 @@ class Infrared(IJob): enabled = True - label = "Dipole AutoCorrelation Function" + label = "Infrared Spectrum" category = ( "Analysis", diff --git a/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py b/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py index f757a6111..844b7eb8a 100644 --- a/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py +++ b/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py @@ -35,7 +35,7 @@ def load_session(self, fname: str): class SessionSettings(AbstractSession): def __init__(self): super().__init__() - self.main_path = "." + self.main_path = os.path.abspath(".") def create_structured_project(self): self.relative_paths = { diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AseInputFileWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AseInputFileWidget.py index 6033dc98f..147b7283c 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AseInputFileWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AseInputFileWidget.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +import os + from qtpy.QtWidgets import QLineEdit, QPushButton, QFileDialog, QComboBox from qtpy.QtCore import Slot from ase.io.formats import filetype @@ -37,10 +39,10 @@ def __init__(self, *args, **kwargs): parent = kwargs.get("parent", None) self.default_path = parent.default_path except KeyError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error("KeyError in InputFileWidget - can't get default path.") except AttributeError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error("AttributeError in InputFileWidget - can't get default path.") default_value = kwargs.get("default", "") if self._tooltip: diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/InputFileWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/InputFileWidget.py index 6851fa577..d2d113efc 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/InputFileWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/InputFileWidget.py @@ -33,29 +33,19 @@ def __init__(self, *args, file_dialog=QFileDialog.getOpenFileName, **kwargs): else: default_value = "" parent = kwargs.get("parent", None) + self._parent = parent if parent is not None: self._job_name = parent._job_name self._settings = parent._settings try: - paths_group = self._settings.group("paths") - try: - self.default_path = paths_group.get(self._job_name) - except KeyError: - paths_group.add( - self._job_name, - ".", - f"The filesystem path recently used by {self._job_name}", - ) - self.default_path = "." + parent = kwargs.get("parent", None) + self.default_path = parent._default_path + except KeyError: + self.default_path = os.path.abspath(".") + LOG.error("KeyError in OutputFilesWidget - can't get default path.") except AttributeError: - try: - self.default_path = parent.default_path - except KeyError: - self.default_path = "." - LOG.error("KeyError in InputFileWidget - can't get default path.") - except AttributeError: - self.default_path = "." - LOG.error("AttributeError in InputFileWidget - can't get default path.") + self.default_path = os.path.abspath(".") + LOG.error("AttributeError in OutputFilesWidget - can't get default path.") default_value = kwargs.get("default", "") if self._tooltip: self._tooltip_text = self._tooltip @@ -95,15 +85,10 @@ def valueFromDialog(self): This will start a FileDialog, take the resulting path, and emit a signal to update the value show by the GUI. """ - paths_group = self._settings.group("paths") - try: - self.default_path = paths_group.get(self._job_name) - except: - LOG.warning(f"session.get_path failed for {self._job_name}") new_value = self._file_dialog( self.parent(), # the parent of the dialog "Load file", # the label of the window - self.default_path, # the initial search path + self._parent._default_path, # the initial search path self._qt_file_association, # text string specifying the file name filter. ) if new_value is not None and new_value[0]: @@ -113,7 +98,8 @@ def valueFromDialog(self): LOG.info( f"Settings path of {self._job_name} to {os.path.split(new_value[0])[0]}" ) - paths_group.set(self._job_name, os.path.split(new_value[0])[0]) + if self._parent is not None: + self._parent._default_path = os.path.split(new_value[0])[0] except: LOG.error( f"session.set_path failed for {self._job_name}, {os.path.split(new_value[0])[0]}" diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputFilesWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputFilesWidget.py index d74d9b5ee..be45702c8 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputFilesWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputFilesWidget.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob -import itertools import os import os.path @@ -35,18 +33,30 @@ def __init__(self, *args, **kwargs): super().__init__(*args, layout_type="QGridLayout", **kwargs) default_value = self._configurator.default try: - parent = kwargs.get("parent", None) - self.default_path = parent.default_path + self._parent = kwargs.get("parent", None) + self.default_path = self._parent._default_path except KeyError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error("KeyError in OutputFilesWidget - can't get default path.") except AttributeError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error("AttributeError in OutputFilesWidget - can't get default path.") + else: + self._session = self._parent._parent_tab._session + try: + self._parent = kwargs.get("parent", None) + jobname = str(self._parent._job_instance.label).replace(" ", "") + guess_name = os.path.join(self.default_path, jobname + "_result1") + except: + guess_name = default_value[0] + LOG.error("It was not possible to get the job name from the parent") + while os.path.exists(guess_name + ".mda"): + prefix, number = guess_name.split("_result") + guess_name = prefix + "_result" + str(1 + int(number)) self.file_association = "Output file name (*)" self._value = default_value - self._field = QLineEdit(default_value[0], self._base) - self._field.setPlaceholderText(default_value[0]) + self._field = QLineEdit(guess_name, self._base) + self._field.setPlaceholderText(guess_name) self.type_box = CheckableComboBox(self._base) self.type_box.addItems(self._configurator.formats) self.type_box.set_default("MDAFormat") @@ -92,6 +102,10 @@ def file_dialog(self): This will start a FileDialog, take the resulting path, and emit a signal to update the value show by the GUI. """ + try: + self.default_path = self._parent._default_path + except AttributeError: + self.default_path = os.path.abspath(".") new_value = QFileDialog.getSaveFileName( self._base, # the parent of the dialog "Save files", # the label of the window @@ -102,28 +116,8 @@ def file_dialog(self): self._field.setText(new_value[0]) self.updateValue() - @staticmethod - def _get_unique_filename(directory, basename): - filesInDirectory = [ - os.path.join(directory, e) - for e in itertools.chain( - glob.iglob(os.path.join(directory, "*")), - glob.iglob(os.path.join(directory, ".*")), - ) - if os.path.isfile(os.path.join(directory, e)) - ] - basenames = [os.path.splitext(f)[0] for f in filesInDirectory] - - initialPath = path = os.path.join(directory, basename) - comp = 1 - while True: - if path in basenames: - path = "%s(%d)" % (initialPath, comp) - comp += 1 - continue - return path - def get_widget_value(self): + self._configurator._forbidden_files = self._session.reserved_filenames() filename = self._field.text() if len(filename) < 1: filename = self._default_value[0] @@ -131,15 +125,4 @@ def get_widget_value(self): formats = self.type_box.checked_values() log_level = self.logs_combo.currentText() - return (filename, formats, log_level) - - def set_data(self, datakey): - basename = "%s_%s" % ( - os.path.splitext(os.path.basename(datakey))[0], - self._parent.type, - ) - trajectoryDir = os.path.dirname(datakey) - - path = OutputFilesWidget._get_unique_filename(trajectoryDir, basename) - - self._filename.SetValue(path) + return (os.path.abspath(filename), formats, log_level) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputStructureWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputStructureWidget.py index 406af50a5..00e3e6db7 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputStructureWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputStructureWidget.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob -import itertools import os import os.path @@ -37,17 +35,24 @@ def __init__(self, *args, **kwargs): parent = kwargs.get("parent", None) self.default_path = parent.default_path except KeyError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error("KeyError in OutputTrajectoryWidget - can't get default path.") except AttributeError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error( "AttributeError in OutputTrajectoryWidget - can't get default path." ) + try: + parent = kwargs.get("parent", None) + jobname = str(parent._job_instance.label).replace(" ", "") + guess_name = os.path.join(self.default_path, "POSCAR") + except: + guess_name = default_value[0] + LOG.error("It was not possible to get the job name from the parent") self.file_association = "Output file name (*)" self._value = default_value - self._field = QLineEdit(default_value[0], self._base) - self._field.setPlaceholderText(default_value[0]) + self._field = QLineEdit(guess_name, self._base) + self._field.setPlaceholderText(guess_name) self.format_box = QComboBox(self._base) self.format_box.addItems(self._configurator._formats) self.format_box.setCurrentText(default_value[1]) @@ -108,31 +113,10 @@ def file_dialog(self): self._field.setText(new_value[0]) self.updateValue() - @staticmethod - def _get_unique_filename(directory, basename): - filesInDirectory = [ - os.path.join(directory, e) - for e in itertools.chain( - glob.iglob(os.path.join(directory, "*")), - glob.iglob(os.path.join(directory, ".*")), - ) - if os.path.isfile(os.path.join(directory, e)) - ] - basenames = [os.path.splitext(f)[0] for f in filesInDirectory] - - initialPath = path = os.path.join(directory, basename) - comp = 1 - while True: - if path in basenames: - path = "%s(%d)" % (initialPath, comp) - comp += 1 - continue - return path - def get_widget_value(self): filename = self._field.text() if len(filename) < 1: filename = self._default_value[0] format = self.format_box.currentText() log_level = self.logs_combo.currentText() - return (filename, format, log_level) + return (os.path.abspath(filename), format, log_level) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputTrajectoryWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputTrajectoryWidget.py index 3f3d722e4..442ea1661 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputTrajectoryWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/OutputTrajectoryWidget.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob -import itertools import os import os.path @@ -44,20 +42,32 @@ def __init__(self, *args, **kwargs): super().__init__(*args, layout_type="QGridLayout", **kwargs) default_value = self._configurator.default try: - parent = kwargs.get("parent", None) - self.default_path = parent.default_path + self._parent = kwargs.get("parent", None) + self.default_path = self._parent._default_path except KeyError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error("KeyError in OutputTrajectoryWidget - can't get default path.") except AttributeError: - self.default_path = "." + self.default_path = os.path.abspath(".") LOG.error( "AttributeError in OutputTrajectoryWidget - can't get default path." ) + else: + self._session = self._parent._parent_tab._session + try: + self._parent = kwargs.get("parent", None) + jobname = str(self._parent._job_instance.label).replace(" ", "") + guess_name = os.path.join(self.default_path, jobname + "_trajectory1") + except: + guess_name = default_value[0] + LOG.error("It was not possible to get the job name from the parent") + while os.path.exists(guess_name + ".mdt"): + prefix, number = guess_name.split("_trajectory") + guess_name = prefix + "_trajectory" + str(1 + int(number)) self.file_association = "MDT trajectory (*.mdt)" self._value = default_value - self._field = QLineEdit(default_value[0], self._base) - self._field.setPlaceholderText(default_value[0]) + self._field = QLineEdit(guess_name, self._base) + self._field.setPlaceholderText(guess_name) self.dtype_box = QComboBox(self._base) self.dtype_box.addItems(["float16", "float32", "float64"]) self.dtype_box.setCurrentText("float64") @@ -113,6 +123,7 @@ def file_dialog(self): This will start a FileDialog, take the resulting path, and emit a signal to update the value show by the GUI. """ + self.default_path = self._parent._default_path new_value = QFileDialog.getSaveFileName( self._base, # the parent of the dialog "Save file", # the label of the window @@ -123,31 +134,13 @@ def file_dialog(self): self._field.setText(new_value[0]) self.updateValue() - @staticmethod - def _get_unique_filename(directory, basename): - filesInDirectory = [ - os.path.join(directory, e) - for e in itertools.chain( - glob.iglob(os.path.join(directory, "*")), - glob.iglob(os.path.join(directory, ".*")), - ) - if os.path.isfile(os.path.join(directory, e)) - ] - basenames = [os.path.splitext(f)[0] for f in filesInDirectory] - - initialPath = path = os.path.join(directory, basename) - comp = 1 - while True: - if path in basenames: - path = "%s(%d)" % (initialPath, comp) - comp += 1 - continue - return path - def get_widget_value(self): + self._configurator._forbidden_files = self._session.reserved_filenames() filename = self._field.text() if len(filename) < 1: filename = self._default_value[0] + else: + self._parent.set_trajectory(os.path.abspath(filename)) dtype = dtype_lookup[self.dtype_box.currentText()] compression = self.compression_box.currentText() logs = self.logs_combo.currentText() diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Session/LocalSession.py b/MDANSE_GUI/Src/MDANSE_GUI/Session/LocalSession.py index 72bfc2542..9d62f94dd 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Session/LocalSession.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Session/LocalSession.py @@ -77,7 +77,7 @@ def get_parameter(self, key: str) -> str: return value def get_path(self, key: str) -> str: - value = self._paths.get(key, ".") + value = self._paths.get(key, os.path.abspath(".")) return value def set_path(self, key: str, value: str): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Session/StructuredSession.py b/MDANSE_GUI/Src/MDANSE_GUI/Session/StructuredSession.py index 29044ef8e..7887091f9 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Session/StructuredSession.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Session/StructuredSession.py @@ -15,7 +15,7 @@ # import os -from typing import Dict +from typing import Dict, List from qtpy.QtCore import QObject, Signal, Slot, Qt, QModelIndex from qtpy.QtGui import QStandardItem, QStandardItemModel @@ -25,7 +25,6 @@ from MDANSE import PLATFORM from MDANSE.MLogging import LOG -from MDANSE.Framework.Units import measure, unit_lookup class UserSettingsModel(QStandardItemModel): @@ -38,7 +37,7 @@ def __init__(self, *args, settings_filename: str = "", **kwargs): self._settings = SettingsFile(settings_filename) self._settings.load_from_file() else: - LOG.warning(f"Called UserSettingsModel without settings_filename") + LOG.warning("Called UserSettingsModel without settings_filename") return self._entries_present = {} self._groups_present = {} @@ -51,7 +50,7 @@ def __init__(self, *args, settings_filename: str = "", **kwargs): # self.scan_model() def refresh(self): - self._settings.load_from_file() + # self._settings.load_from_file() self.populate_model() return @@ -166,6 +165,7 @@ def modify_item( @Slot("QStandardItem*") def on_value_changed(self, item: "QStandardItem"): + item_key = item.text() index = item.index() column = index.column() row = index.row() @@ -202,7 +202,7 @@ def add(self, varname: str, value: str, comment: str): self._comments[varname] = comment def set(self, varname: str, value: str): - if not varname in self._settings: + if varname not in self._settings: LOG.warning( f"Group {self._name} has no entry {varname}. Add it first using add()." ) @@ -211,7 +211,7 @@ def set(self, varname: str, value: str): return True def set_comment(self, varname: str, value: str): - if not varname in self._settings: + if varname not in self._settings: LOG.warning( f"Group {self._name} has no entry {varname}. Add it first using add()." ) @@ -353,6 +353,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._models = {} self._configs = {} + self._reserved_filenames = [] self._state = None self._main_config_name = "mdanse_general_settings" self._filename = kwargs.get("filename", self._main_config_name) @@ -374,6 +375,22 @@ def load(self, fname: str = None): """Included for compatibility with LocalSession only. Now each component loads its own config separately.""" + def reserved_filenames(self) -> List[str]: + return self._reserved_filenames + + @Slot(str) + def protect_filename(self, some_filename: str): + new_filename = os.path.abspath(some_filename) + if new_filename not in self._reserved_filenames: + self._reserved_filenames.append(os.path.abspath(new_filename)) + + @Slot(str) + def free_filename(self, some_filename: str): + filename = os.path.abspath(some_filename) + if filename in self._reserved_filenames: + index = self._reserved_filenames.index(filename) + self._reserved_filenames.pop(index) + def main_settings(self): return self._configs[self._main_config_name] @@ -438,7 +455,7 @@ def populate_defaults(self): def get_path(self, key: str) -> str: settings = self._configs[self._main_config_name] group = settings["paths"] - value = group.get(key, ".") + value = group.get(key, os.path.abspath(".")) return value def set_path(self, key: str, value: str): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py b/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py index dcbeac614..9bd06b89b 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py @@ -306,6 +306,9 @@ def createConverterViewer(self): job_tab.set_job_starter(self._job_holder) self.tabs.addTab(job_tab._core, name) self._tabs[name] = job_tab + self.tabs.tabBar().tabBarClicked.connect( + job_tab.update_action_on_tab_activation + ) def createActionsViewer(self): name = "Actions" @@ -319,7 +322,8 @@ def createActionsViewer(self): instrument_model=self._instrument_model, ) job_tab.set_job_starter(self._job_holder) - self.tabs.addTab(job_tab._core, name) + jobtab_index = self.tabs.addTab(job_tab._core, name) + job_tab.set_own_index(jobtab_index) self._tabs[name] = job_tab self.tabs.tabBar().tabBarClicked.connect( job_tab.update_action_on_tab_activation diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/ConverterTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/ConverterTab.py index 90d435b11..ac5e1b10e 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/ConverterTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/ConverterTab.py @@ -55,6 +55,10 @@ def set_job_starter(self, job_starter): def set_current_trajectory(self, new_name: str): self._current_trajectory = new_name + @Slot() + def update_action_on_tab_activation(self): + self.action.test_file_outputs() + def grouped_settings(self): results = super().grouped_settings() results += [ diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py index 79a89e5df..300a69085 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py @@ -13,8 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # - - +import os from typing import Dict, Tuple from qtpy.QtCore import QObject, Slot, Signal, QMessageLogger @@ -95,7 +94,7 @@ def grouped_settings(self): """ group1 = [ "Generic settings", # name of the group of settings - {"path": "."}, # a dictionary of settings + {"path": os.path.abspath(".")}, # a dictionary of settings { "path": "The path last used by this GUI element." }, # a dictionary of comments @@ -167,9 +166,11 @@ def get_path(self, path_key: str): path = paths_group.get(path_key) except KeyError: paths_group.add( - path_key, ".", f"Filesystem path recently used by {path_key}" + path_key, + os.path.abspath("."), + f"Filesystem path recently used by {path_key}", ) - path = "." + path = os.path.abspath(".") return path def set_path(self, path_key: str, path_value: str): @@ -178,6 +179,7 @@ def set_path(self, path_key: str, path_value: str): paths_group.add( path_key, path_value, f"Filesystem path recently used by {path_key}" ) + self._session.save() @Slot() def save_state(self): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/JobTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/JobTab.py index 1d2ead18e..8f6e86ec5 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/JobTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/JobTab.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +import os from functools import partial from qtpy.QtCore import Slot from qtpy.QtWidgets import QWidget, QComboBox, QLabel @@ -47,6 +48,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._current_trajectory = "" self._job_starter = None + self._own_index = -1 self._instrument_index = -1 self._trajectory_combo = QComboBox() self._trajectory_combo.setEditable(False) @@ -65,6 +67,9 @@ def __init__(self, *args, **kwargs): self.action._parent_tab = self self._visualiser._parent_tab = self + def set_own_index(self, index: int): + self._own_index = index + def set_job_starter(self, job_starter): self._job_starter = job_starter self.action.new_thread_objects.connect(self._job_starter.startProcess) @@ -91,7 +96,7 @@ def set_current_trajectory(self, index: int) -> None: if traj_model.rowCount() < 1: # the combobox changed and there are no trajectories, they # were probably deleted lets clear the action widgets - self.action.set_trajectory(path=None, trajectory=None) + self.action.set_trajectory(trajectory=None) self.action.clear_panel() return @@ -102,7 +107,7 @@ def set_current_trajectory(self, index: int) -> None: # The combobox was changed we need to update the action # widgets with the new trajectory self.action.set_trajectory( - path=None, trajectory=traj_model._nodes[node_number][0] + trajectory=os.path.abspath(traj_model._nodes[node_number][0]) ) current_item = self._core.current_item() if current_item is not None: @@ -128,8 +133,11 @@ def update_action_after_instrument_change(self, index: int): return self._needs_updating = True - @Slot() - def update_action_on_tab_activation(self): + @Slot(int) + def update_action_on_tab_activation(self, current_index: int): + if current_index != self._own_index: + return + self.action.test_file_outputs() if self._needs_updating: current_item = self._core.current_item() if current_item is not None: diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/JobHolder.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/JobHolder.py index 36d6ebd43..0c9158b72 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/JobHolder.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/JobHolder.py @@ -181,6 +181,21 @@ def on_finished(self, success: bool): self._current_state.fail() self.update_fields() + def expected_output(self) -> str: + try: + len(self._parameters["output_files"][1]) + except TypeError: # job is a converter + file_name = self._parameters["output_files"][0] + if ".mdt" not in file_name[-5:]: + file_name += ".mdt" + return file_name + else: # job is an analysis + if "MDAFormat" in self._parameters["output_files"][1]: + file_name = self._parameters["output_files"][0] + if ".mda" not in file_name[-5:]: + file_name += ".mda" + return file_name + @Slot(int) def on_started(self, target_steps: int): LOG.info(f"Item received on_started: {target_steps} total steps") @@ -238,6 +253,8 @@ class JobHolder(QStandardItemModel): trajectory_for_loading = Signal(str) results_for_loading = Signal(str) + protect_filename = Signal(str) + unprotect_filename = Signal(str) new_job_started = Signal() def __init__(self, parent: QObject = None): @@ -302,6 +319,7 @@ def startProcess(self, job_vars: list, load_afterwards=False): communicator.moveToThread(watcher_thread) entry_number = self.next_number item_th.parameters = job_vars[1] + # item_th.for_loading.connect(self.unprotect_filename) if load_afterwards: if job_vars[0] in Converter.subclasses(): item_th.for_loading.connect(self.trajectory_for_loading) @@ -324,6 +342,7 @@ def startProcess(self, job_vars: list, load_afterwards=False): task_name = str("This should have been a job name") name_item = QStandardItem(task_name) name_item.setData(entry_number, role=Qt.ItemDataRole.UserRole) + self.protect_filename.emit(item_th.expected_output()) self.appendRow( [ name_item, diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlotDataModel.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlotDataModel.py index 72c6c0023..5cb9b992e 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlotDataModel.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlotDataModel.py @@ -14,6 +14,7 @@ # along with this program. If not, see . # from abc import abstractmethod +import os import h5py from qtpy.QtCore import QObject, Slot, Signal, QMutex, QModelIndex, Qt @@ -70,7 +71,7 @@ def __init__(self, *args, **kwargs): def data_path(self) -> str: parent_path = self.parent().data_path() own_path = self.data(role=Qt.ItemDataRole.UserRole) - return "/".join([parent_path, own_path]) + return os.path.join(parent_path, own_path) def file_number(self) -> int: return self.parent().file_number() diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotSelectionTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotSelectionTab.py index 93c0cfd14..c43c628cb 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotSelectionTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotSelectionTab.py @@ -78,8 +78,9 @@ def load_files(self): @Slot(str) def load_results(self, fname: str): if len(fname) > 0: - _, short_name = os.path.split(fname) + fname = os.path.abspath(fname) self._model.add_file(fname) + self._session.protect_filename(fname) @classmethod def standard_instance(cls): @@ -118,6 +119,7 @@ def gui_instance( label_text=label_text, ) the_tab._visualiser._unit_lookup = the_tab + the_tab._view.free_name.connect(session.free_filename) return the_tab diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/RunTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/RunTab.py index a1248a414..01b752c25 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/RunTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/RunTab.py @@ -90,6 +90,8 @@ def gui_instance( ), label_text=run_tab_label, ) + the_tab._model.protect_filename.connect(session.protect_filename) + the_tab._model.unprotect_filename.connect(session.free_filename) the_tab._model.new_job_started.connect(the_tab.tab_notification) return the_tab diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/TrajectoryTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/TrajectoryTab.py index 778dc3915..d1473b296 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/TrajectoryTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/TrajectoryTab.py @@ -73,6 +73,7 @@ def load_trajectory(self, fname: str): self._core.error.emit(repr(e)) else: self._core._model.append_object(((fname, data), short_name)) + self._session.protect_filename(fname) @classmethod def standard_instance(cls): @@ -110,6 +111,7 @@ def gui_instance( layout=partial(MultiPanel, left_panels=[TrajectoryInfo()]), label_text=label_text, ) + the_tab._view.free_name.connect(session.free_filename) return the_tab diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/PlotDataView.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/PlotDataView.py index 5f26bdc04..a19908dbf 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/PlotDataView.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/PlotDataView.py @@ -29,6 +29,7 @@ class PlotDataView(QTreeView): execute_action = Signal(object) item_details = Signal(object) error = Signal(str) + free_name = Signal(str) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -76,6 +77,12 @@ def populateMenu(self, menu: QMenu, item: QStandardItem): def deleteNode(self): model = self.model() index = self.currentIndex() + mda_data_structure = model.inner_object(index) + try: + filename = mda_data_structure._file.filename + except AttributeError: + filename = mda_data_structure.file + self.free_name.emit(filename) model.removeRow(index.row()) self.item_details.emit("") diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/TrajectoryView.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/TrajectoryView.py index 37d469771..d63d76a9c 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/TrajectoryView.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/TrajectoryView.py @@ -27,6 +27,7 @@ class TrajectoryView(QListView): item_details = Signal(tuple) item_name = Signal(str) error = Signal(str) + free_name = Signal(str) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -53,8 +54,12 @@ def populateMenu(self, menu: QMenu, item: QStandardItem): def deleteNode(self): model = self.model() index = self.currentIndex() + node_number = model.itemFromIndex(index).data() + trajectory = model._nodes[node_number][0] + self.free_name.emit(trajectory) model.removeRow(index.row()) self.item_details.emit(("", None)) + self.free_name @Slot(QModelIndex) def item_picked(self, index: QModelIndex): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py index e95bf1121..a2d565db7 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +import os from typing import Optional import traceback @@ -89,19 +90,20 @@ class Action(QWidget): run_and_load = Signal(list) new_path = Signal(str) - last_paths = {} - def __init__(self, *args, use_preview=False, **kwargs): self._default_path = None self._input_trajectory = None self._parent_tab = None self._trajectory_configurator = None self._settings = None + self._job_name = None self._use_preview = use_preview self._current_instrument = None - default_path = kwargs.pop("path", None) + self._has_been_initialised = False + self.execute_button = None + self.post_execute_checkbox = None input_trajectory = kwargs.pop("trajectory", None) - self.set_trajectory(default_path, input_trajectory) + self.set_trajectory(input_trajectory) super().__init__(*args, **kwargs) self.layout = QVBoxLayout(self) @@ -112,7 +114,7 @@ def __init__(self, *args, use_preview=False, **kwargs): def set_settings(self, settings): self._settings = settings - def set_trajectory(self, path: Optional[str], trajectory: Optional[str]) -> None: + def set_trajectory(self, trajectory: str) -> None: """Set the trajectory path and filename. Parameters @@ -122,16 +124,13 @@ def set_trajectory(self, path: Optional[str], trajectory: Optional[str]) -> None trajectory : str or None The path and filename of the trajectory """ - self._default_path = path self._input_trajectory = trajectory - path = None if self._input_trajectory is not None: - path, filename = os.path.split(self._input_trajectory) - if self._default_path is None: - if path is None: - self._default_path = "." - else: - self._default_path = path + self._default_path, _ = os.path.split(self._input_trajectory) + else: + self._default_path = os.path.abspath(".") + if self._job_name is not None: + self._parent_tab.set_path(self._job_name, self._default_path) def set_instrument(self, instrument: SimpleInstrument) -> None: self._current_instrument = instrument @@ -160,9 +159,11 @@ def update_panel(self, job_name: str) -> None: The job name. """ self.clear_panel() + self._has_been_initialised = False self._job_name = job_name - self.last_paths[job_name] = self._parent_tab.get_path(job_name) + if self._default_path is None or self._default_path == os.path.abspath("."): + self._default_path = self._parent_tab.get_path(job_name) try: job_instance = IJob.create(job_name) except ValueError as e: @@ -229,16 +230,8 @@ def update_panel(self, job_name: str) -> None: input_widget.value_updated.connect(self.show_output_prediction) LOG.info(f"Set up the right widget for {key}") # self.handlers[key] = data_handler - configured = False - iterations = 0 - while not configured: - configured = True - for widget in self._widgets: - widget.value_from_configurator() - configured = configured and widget._configurator.is_configured() - iterations += 1 - if iterations > 5: - break + self._has_been_initialised = True + self.check_inputs() if self._use_preview: self._preview_box = QTextEdit(self) @@ -274,6 +267,27 @@ def update_panel(self, job_name: str) -> None: self._widgets_in_layout.append(buttonbase) self.apply_instrument() + def check_inputs(self): + configured = False + iterations = 0 + while not configured: + configured = True + for widget in self._widgets: + widget.value_from_configurator() + configured = configured and widget._configurator.is_configured() + iterations += 1 + if iterations > 5: + break + + @Slot() + def test_file_outputs(self): + if not self._has_been_initialised: + return + self.check_inputs() + for widget in self._widgets: + widget.updateValue() + self.allow_execution() + def apply_instrument(self): if self._current_instrument is not None: q_vector_tuple = self._current_instrument.create_q_vector_params() @@ -312,26 +326,22 @@ def show_output_prediction(self): text += f"

[{array[0]}, {array[1]}, {array[2]}, ..., {array[-1]}] ({new_unit})

" self._preview_box.setHtml(text) - @Slot(dict) - def parse_updated_params(self, new_params: dict): - if "path" in new_params.keys(): - self.default_path = new_params["path"] - self.new_path.emit(self.default_path) - @Slot() def allow_execution(self): allow = True for widget in self._widgets: if not widget._configurator.valid: allow = False - if allow: - self.execute_button.setEnabled(True) - else: - self.execute_button.setEnabled(False) - if self._job_name == "AverageStructure": - self.post_execute_checkbox.setEnabled(False) - else: - self.post_execute_checkbox.setEnabled(True) + if self.execute_button is not None: + if allow: + self.execute_button.setEnabled(True) + else: + self.execute_button.setEnabled(False) + if self.post_execute_checkbox is not None: + if self._job_name == "AverageStructure": + self.post_execute_checkbox.setEnabled(False) + else: + self.post_execute_checkbox.setEnabled(True) @Slot() def cancel_dialog(self): @@ -342,7 +352,7 @@ def save_dialog(self): try: cname = self._job_name except: - currentpath = "." + currentpath = os.path.abspath(".") else: currentpath = self._parent_tab.get_path(self._job_name + "_script") result, ftype = QFileDialog.getSaveFileName( @@ -356,7 +366,6 @@ def save_dialog(self): except: pass else: - self.last_paths[cname] = path self._parent_tab.set_path(self._job_name + "_script", path) pardict = self.set_parameters(labels=True) self._job_instance.save(result, pardict) @@ -376,6 +385,7 @@ def execute_converter(self): pardict = self.set_parameters() LOG.info(pardict) self._parent_tab.set_path(self._job_name, self._default_path) + self._parent_tab._session.save() # when we are ready, we can consider running it # self.converter_instance.run(pardict) # this would send the actual instance, which _may_ be wrong @@ -387,3 +397,7 @@ def execute_converter(self): self.run_and_load.emit([self._job_name, pardict]) else: self.new_thread_objects.emit([self._job_name, pardict]) + self.check_inputs() + for widget in self._widgets: + widget.updateValue() + self.allow_execution() diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataWidget.py index 62a9a2449..046bb36fd 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/DataWidget.py @@ -56,7 +56,7 @@ def __init__(self, *args, **kwargs) -> None: self._sliderpack = None self._plotting_context = None self._slider_max = 100 - self._current_path = "." + self._current_path = os.path.abspath(".") layout = QVBoxLayout(self) self.setLayout(layout) self.make_toolbar()