diff --git a/pyproject.toml b/pyproject.toml index 99e992f..a7221e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,3 +50,9 @@ petab_gui="petab_gui:main" [project.scripts] petab_gui_cli="petab_gui:main" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.package-data] +"petab_gui.assets" = ["PEtab.png"] diff --git a/src/petab_gui/C.py b/src/petab_gui/C.py index 2a9efc4..814c95c 100644 --- a/src/petab_gui/C.py +++ b/src/petab_gui/C.py @@ -38,7 +38,7 @@ "conditionId": {"type": np.object_, "optional": False}, "conditionName": {"type": np.object_, "optional": False}, } - } +} CONFIG = { 'window_title': 'My Application', diff --git a/src/petab_gui/app.py b/src/petab_gui/app.py index a7ea349..189acf3 100644 --- a/src/petab_gui/app.py +++ b/src/petab_gui/app.py @@ -1,6 +1,7 @@ from PySide6.QtWidgets import QApplication -from PySide6.QtGui import QFileOpenEvent +from PySide6.QtGui import QFileOpenEvent, QIcon from PySide6.QtCore import QEvent +from importlib.resources import files import sys import os import petab.v1 as petab @@ -21,6 +22,15 @@ def find_example(path: Path) -> Path: raise FileNotFoundError("Could not find examples directory") +def get_icon() -> QIcon: + """Get the Icon for the Window""" + icon_path = files("petab_gui.assets").joinpath("PEtab.png") + if not icon_path.is_file(): + raise FileNotFoundError(f"Icon file not found: {icon_path}") + icon = QIcon(str(icon_path)) + return icon + + class PEtabGuiApp(QApplication): def __init__(self): super().__init__(sys.argv) @@ -29,6 +39,7 @@ def __init__(self): # self.apply_stylesheet() self.model = PEtabModel() self.view = MainWindow() + self.view.setWindowIcon(get_icon()) self.controller = MainController(self.view, self.model) # hack to be discussed diff --git a/src/petab_gui/assets/PEtab.png b/src/petab_gui/assets/PEtab.png new file mode 100644 index 0000000..79372f4 Binary files /dev/null and b/src/petab_gui/assets/PEtab.png differ diff --git a/src/petab_gui/models/pandas_table_model.py b/src/petab_gui/models/pandas_table_model.py index c690783..9973885 100644 --- a/src/petab_gui/models/pandas_table_model.py +++ b/src/petab_gui/models/pandas_table_model.py @@ -180,10 +180,17 @@ def _set_data_single(self, index, value): column_name = self._data_frame.columns[column - col_setoff] old_value = self._data_frame.iloc[row, column - col_setoff] # cast to numeric if necessary - if not self._data_frame[column_name].dtype == "object": - try: - value = float(value) - except ValueError: + expected_type = self._allowed_columns.get(column_name, None) + if is_invalid(value): + if not expected_type["optional"]: + return False + self._data_frame.iloc[row, column - col_setoff] = None + self.dataChanged.emit(index, index, [Qt.DisplayRole]) + return True + if expected_type: + expected_type = expected_type["type"] + value, error_message = validate_value(value, expected_type) + if error_message: self.new_log_message.emit( f"Column '{column_name}' expects a numeric value", "red" diff --git a/src/petab_gui/utils.py b/src/petab_gui/utils.py index 6045b00..279bb16 100644 --- a/src/petab_gui/utils.py +++ b/src/petab_gui/utils.py @@ -488,12 +488,10 @@ def highlightBlock(self, text): def validate_value(value, expected_type): try: - if expected_type == "STRING": + if expected_type == np.object_: value = str(value) - elif expected_type == "NUMERIC": + elif expected_type == np.float64: value = float(value) - elif expected_type == "BOOLEAN": - value = bool(value) except ValueError as e: return None, str(e) return value, None @@ -599,6 +597,12 @@ def get_selected_rectangles(table_view: QTableView) -> np.array: selected = get_selected(table_view, mode=INDEX) if not selected: return None + + model = table_view.model() + if hasattr(model, "mapToSource"): + # map all indices to source + selected = [model.mapToSource(index) for index in selected] + rows = [index.row() for index in selected] cols = [index.column() for index in selected] min_row, max_row = min(rows), max(rows) @@ -609,6 +613,7 @@ def get_selected_rectangles(table_view: QTableView) -> np.array: ) for index in selected: selected_rect[index.row() - min_row, index.column() - min_col] = True + return selected_rect, rect_start diff --git a/src/petab_gui/views/table_view.py b/src/petab_gui/views/table_view.py index 12d4180..87c50a0 100644 --- a/src/petab_gui/views/table_view.py +++ b/src/petab_gui/views/table_view.py @@ -41,33 +41,43 @@ def paste_from_clipboard(self): text = clipboard.text() if not text: return + + # Get the proxy and source models + proxy_model = self.table_view.model() + source_model = proxy_model.sourceModel() + + # Get the start index from the current selection start_index = self.table_view.selectionModel().currentIndex() if not start_index.isValid(): return - model = self.table_view.model() - row_start, col_start = start_index.row(), start_index.column() - # identify which invalid cells are being pasted into + + # Map the start index to the source model + source_index = proxy_model.mapToSource(start_index) + row_start, col_start = source_index.row(), source_index.column() + + # Parse clipboard data pasted_data = [line.split("\t") for line in text.split("\n") if line.strip()] num_rows = len(pasted_data) - num_cols = max([len(line) for line in pasted_data]) + num_cols = max(len(line) for line in pasted_data) + + # Identify which cells are being overridden overridden_cells = { (row_start + r, col_start + c) for r in range(num_rows) for c in range(num_cols) - if model.index(row_start + r, col_start + c).isValid() + if source_model.index(row_start + r, col_start + c).isValid() } - invalid_overridden_cells = overridden_cells.intersection( - model._invalid_cells - ) - if invalid_overridden_cells: + + # Handle invalid cells + if hasattr(source_model, "_invalid_cells"): + invalid_overridden_cells = overridden_cells.intersection( + source_model._invalid_cells) for row_invalid, col_invalid in invalid_overridden_cells: - model.discard_invalid_cell(row_invalid, col_invalid) + source_model.discard_invalid_cell(row_invalid, col_invalid) - model.setDataFromText( - text, start_index.row(), - start_index.column() - ) + # Paste the data into the source model + source_model.setDataFromText(text, row_start, col_start) class ComboBoxDelegate(QStyledItemDelegate):