diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py index 621409c..60a3e01 100644 --- a/src/petab_gui/commands.py +++ b/src/petab_gui/commands.py @@ -318,3 +318,46 @@ def _apply(self, src, dst): self.model.dataChanged.emit( self.model_index, self.model_index, [Qt.DisplayRole] ) + + +class RenameValueCommand(QUndoCommand): + """Command to rename values in specified columns.""" + + def __init__( + self, model, old_id: str, new_id: str, column_names: str | list[str] + ): + super().__init__(f"Rename value {old_id} → {new_id}") + self.model = model + self.old_id = old_id + self.new_id = new_id + self.column_names = ( + column_names if isinstance(column_names, list) else [column_names] + ) + self.changes = {} # {(row_idx, col_name): (old_val, new_val)} + + df = self.model._data_frame + for col_name in self.column_names: + mask = df[col_name].eq(self.old_id) + for row_idx in df.index[mask]: + self.changes[(row_idx, col_name)] = (self.old_id, self.new_id) + + def redo(self): + self._apply_changes(use_new=True) + + def undo(self): + self._apply_changes(use_new=False) + + def _apply_changes(self, use_new: bool): + df = self.model._data_frame + for (row_idx, col_name), (old_val, new_val) in self.changes.items(): + df.at[row_idx, col_name] = new_val if use_new else old_val + + if self.changes: + rows = [df.index.get_loc(row) for (row, _) in self.changes] + cols = [df.columns.get_loc(col) + 1 for (_, col) in self.changes] + top_left = self.model.index(min(rows), min(cols)) + bottom_right = self.model.index(max(rows), max(cols)) + self.model.dataChanged.emit( + top_left, bottom_right, [Qt.DisplayRole, Qt.EditRole] + ) + self.model.something_changed.emit(True) diff --git a/src/petab_gui/controllers/default_handler.py b/src/petab_gui/controllers/default_handler.py index 6ccacd2..99428dc 100644 --- a/src/petab_gui/controllers/default_handler.py +++ b/src/petab_gui/controllers/default_handler.py @@ -66,7 +66,9 @@ def get_default( default_value = column_config.get(DEFAULT_VALUE, "") if strategy == USE_DEFAULT: - if np.issubdtype(self.model.dtypes[column_name], np.floating): + if column_name != self.model.index.name and np.issubdtype( + self.model.dtypes[column_name], np.floating + ): return float(default_value) return default_value if strategy == NO_DEFAULT: diff --git a/src/petab_gui/controllers/mother_controller.py b/src/petab_gui/controllers/mother_controller.py index 7ee6e5a..0626505 100644 --- a/src/petab_gui/controllers/mother_controller.py +++ b/src/petab_gui/controllers/mother_controller.py @@ -155,11 +155,11 @@ def __init__(self, view, model: PEtabModel): self.actions = self.setup_actions() self.view.setup_toolbar(self.actions) + self.plotter = None + self.init_plotter() self.setup_connections() self.setup_task_bar() self.setup_context_menu() - self.plotter = None - self.init_plotter() @property def window_title(self): @@ -194,6 +194,13 @@ def setup_connections(self): column_names="observableId", ) ) + self.observable_controller.observable_2be_renamed.connect( + partial( + self.visualization_controller.rename_value, + column_names="yValues", + ) + ) + # Maybe TODO: add renaming dataset id? # Rename Condition self.condition_controller.condition_2be_renamed.connect( partial( @@ -204,6 +211,13 @@ def setup_connections(self): ], ) ) + # Plotting Disable Temporarily + for controller in self.controllers: + if controller == self.sbml_controller: + continue + controller.model.plotting_needs_break.connect( + self.plotter.disable_plotting + ) # Add new condition or observable self.model.measurement.relevant_id_changed.connect( lambda x, y, z: self.observable_controller.maybe_add_observable( diff --git a/src/petab_gui/controllers/table_controllers.py b/src/petab_gui/controllers/table_controllers.py index 2b76d18..7e38e0a 100644 --- a/src/petab_gui/controllers/table_controllers.py +++ b/src/petab_gui/controllers/table_controllers.py @@ -18,6 +18,7 @@ ) from ..C import COLUMN, INDEX +from ..commands import RenameValueCommand from ..models.pandas_table_model import ( PandasTableFilterProxy, PandasTableModel, @@ -588,6 +589,25 @@ def save_table(self, file_name): f"Failed to save table: {str(e)}", ) + def rename_value( + self, old_id: str, new_id: str, column_names: str | list[str] + ): + """Rename the values in the dataframe. + + Triggered by changes in the original observable_df or condition_df id. + + Parameters + ---------- + old_id: + The old id, which was changed. + new_id: + The new id. + column_names: + The column or list of columns in which the id should be changed. + """ + command = RenameValueCommand(self.model, old_id, new_id, column_names) + self.undo_stack.push(command) + class MeasurementController(TableController): """Controller of the Measurement table.""" @@ -608,45 +628,6 @@ def check_petab_lint( observable_df=observable_df, ) - def rename_value( - self, old_id: str, new_id: str, column_names: str | list[str] - ): - """Rename the values in the measurement_df. - - Triggered by changes in the original observable_df or condition_df id. - - Parameters - ---------- - old_id: - The old id, which was changed. - new_id: - The new id. - """ - if not isinstance(column_names, list): - column_names = [column_names] - - for col_name in column_names: - # Find occurrences - mask = self.model._data_frame[col_name].eq(old_id) - if not mask.any(): - continue - - self.model._data_frame.loc[mask, col_name] = new_id - first_row, last_row = ( - mask.idxmax(), - mask[::-1].idxmax(), - ) - top_left = self.model.index(first_row, 1) - bottom_right = self.model.index( - last_row, self.model.columnCount() - 1 - ) - self.model.dataChanged.emit( - top_left, bottom_right, [Qt.DisplayRole, Qt.EditRole] - ) - - # Emit change signal - self.model.something_changed.emit(True) - def copy_noise_parameters( self, observable_id: str, condition_id: str | None = None ) -> str: diff --git a/src/petab_gui/models/pandas_table_model.py b/src/petab_gui/models/pandas_table_model.py index 310e020..806c49b 100644 --- a/src/petab_gui/models/pandas_table_model.py +++ b/src/petab_gui/models/pandas_table_model.py @@ -45,6 +45,7 @@ class PandasTableModel(QAbstractTableModel): cell_needs_validation = Signal(int, int) # row, column something_changed = Signal(bool) inserted_row = Signal(QModelIndex) + plotting_needs_break = Signal(bool) def __init__( self, @@ -310,6 +311,7 @@ def setData( if is_invalid(value) or value == "": value = None + self.plotting_needs_break.emit(True) # Temp disable plotting multi_row_change = False if check_multi: # check whether multiple rows but only one column is selected @@ -318,6 +320,7 @@ def setData( self.undo_stack.beginMacro("Set data") success = self._set_data_single(index, value) self.undo_stack.endMacro() + self.plotting_needs_break.emit(False) return success # multiple rows but only one column is selected all_set = [] @@ -325,6 +328,7 @@ def setData( for index in selected: all_set.append(self._set_data_single(index, value)) self.undo_stack.endMacro() + self.plotting_needs_break.emit(False) return all(all_set) def _set_data_single(self, index, value): diff --git a/src/petab_gui/settings_manager.py b/src/petab_gui/settings_manager.py index d20c542..9bb899a 100644 --- a/src/petab_gui/settings_manager.py +++ b/src/petab_gui/settings_manager.py @@ -286,7 +286,7 @@ def save_current_settings(self): def default_col_config(self): """Return default config for new columns.""" - return {"strategy": NO_DEFAULT} + return settings_manager.get_table_defaults(self.table_name) class SettingsDialog(QDialog): @@ -332,7 +332,8 @@ def init_general_page(self): # Header header = QLabel("Profile") desc = QLabel( - "These information can be automatically used when saving a COMBINE archive." + "These information can be automatically used when saving " + "a COMBINE archive." ) desc.setWordWrap(True) diff --git a/src/petab_gui/views/simple_plot_view.py b/src/petab_gui/views/simple_plot_view.py index fcce794..896221a 100644 --- a/src/petab_gui/views/simple_plot_view.py +++ b/src/petab_gui/views/simple_plot_view.py @@ -75,6 +75,7 @@ def __init__(self, parent=None): self.update_timer.setSingleShot(True) self.update_timer.timeout.connect(self.plot_it) self.observable_to_subplot = {} + self.no_plotting_rn = False def initialize( self, meas_proxy, sim_proxy, cond_proxy, vis_proxy, petab_model @@ -104,6 +105,8 @@ def initialize( self.plot_it() def plot_it(self): + if self.no_plotting_rn: + return if not self.meas_proxy or not self.cond_proxy: return if not self.isVisible(): @@ -336,6 +339,12 @@ def plot_residuals(self): # fig_fit.tight_layout() create_plot_tab(fig_fit, self, "Goodness of Fit") + def disable_plotting(self, disable: bool): + """Set self.no_plotting_rn to enable/disable plotting.""" + self.no_plotting_rn = disable + if not self.no_plotting_rn: + self._debounced_plot() + class MeasurementHighlighter: def __init__(self):