|
20 | 20 | import numpy as np |
21 | 21 | import plotpy.io |
22 | 22 | from guidata.configtools import get_icon |
23 | | -from guidata.dataset import update_dataset |
| 23 | +from guidata.dataset import restore_dataset, update_dataset |
24 | 24 | from guidata.qthelpers import add_actions, create_action, exec_dialog |
25 | 25 | from plotpy.plot import PlotDialog |
26 | 26 | from plotpy.tools import ActionTool |
@@ -96,66 +96,137 @@ def is_plot_item_serializable(item: Any) -> bool: |
96 | 96 |
|
97 | 97 |
|
98 | 98 | class ObjectProp(QW.QWidget): |
99 | | - """Object handling panel properties""" |
| 99 | + """Object handling panel properties |
100 | 100 |
|
101 | | - def __init__(self, panel: BaseDataPanel, paramclass: SignalObj | ImageObj): |
| 101 | + Args: |
| 102 | + panel: parent data panel |
| 103 | + objclass: class of the object handled by the panel (SignalObj or ImageObj) |
| 104 | + """ |
| 105 | + |
| 106 | + def __init__(self, panel: BaseDataPanel, objclass: SignalObj | ImageObj) -> None: |
102 | 107 | super().__init__(panel) |
103 | | - self.paramclass = paramclass |
104 | | - self.properties = gdq.DataSetEditGroupBox(_("Properties"), paramclass) |
| 108 | + self.objclass = objclass |
| 109 | + self.properties = gdq.DataSetEditGroupBox(_("Properties"), objclass) |
105 | 110 | self.properties.SIG_APPLY_BUTTON_CLICKED.connect(panel.properties_changed) |
106 | 111 | self.properties.setEnabled(False) |
| 112 | + self.__original_values: dict[str, Any] = {} |
107 | 113 |
|
108 | 114 | self.add_prop_layout = QW.QHBoxLayout() |
109 | 115 | playout: QW.QGridLayout = self.properties.edit.layout |
110 | 116 | playout.addLayout( |
111 | 117 | self.add_prop_layout, playout.rowCount() - 1, 0, 1, 1, QC.Qt.AlignLeft |
112 | 118 | ) |
113 | 119 |
|
114 | | - self.param_label = QW.QLabel() |
115 | | - self.param_label.setTextFormat(QC.Qt.RichText) |
116 | | - self.param_label.setTextInteractionFlags( |
| 120 | + self.analysis_parameter_label = QW.QLabel() |
| 121 | + self.analysis_parameter_label.setTextFormat(QC.Qt.RichText) |
| 122 | + self.analysis_parameter_label.setTextInteractionFlags( |
117 | 123 | QC.Qt.TextBrowserInteraction | QC.Qt.TextSelectableByKeyboard |
118 | 124 | ) |
119 | | - self.param_label.setAlignment(QC.Qt.AlignTop) |
120 | | - param_scroll = QW.QScrollArea() |
121 | | - param_scroll.setWidgetResizable(True) |
122 | | - param_scroll.setWidget(self.param_label) |
| 125 | + self.analysis_parameter_label.setAlignment(QC.Qt.AlignTop) |
| 126 | + analysis_parameter_scroll = QW.QScrollArea() |
| 127 | + analysis_parameter_scroll.setWidgetResizable(True) |
| 128 | + analysis_parameter_scroll.setWidget(self.analysis_parameter_label) |
123 | 129 |
|
124 | 130 | child: QW.QTabWidget = None |
125 | 131 | for child in self.properties.children(): |
126 | 132 | if isinstance(child, QW.QTabWidget): |
127 | 133 | break |
128 | | - child.addTab(param_scroll, _("Analysis parameters")) |
| 134 | + child.addTab(analysis_parameter_scroll, _("Analysis parameters")) |
129 | 135 |
|
130 | 136 | vlayout = QW.QVBoxLayout() |
131 | 137 | vlayout.addWidget(self.properties) |
132 | 138 | self.setLayout(vlayout) |
133 | 139 |
|
134 | | - def add_button(self, button): |
| 140 | + def add_button(self, button: QW.QPushButton) -> None: |
135 | 141 | """Add additional button on bottom of properties panel""" |
136 | 142 | self.add_prop_layout.addWidget(button) |
137 | 143 |
|
138 | | - def set_param_label(self, param: SignalObj | ImageObj): |
139 | | - """Set computing parameters label""" |
| 144 | + def display_analysis_parameter(self, obj: SignalObj | ImageObj) -> None: |
| 145 | + """Set analysis parameter label. |
| 146 | +
|
| 147 | + Args: |
| 148 | + obj: Signal or Image object |
| 149 | + """ |
140 | 150 | text = "" |
141 | | - for key, value in param.metadata.items(): |
| 151 | + for key, value in obj.metadata.items(): |
142 | 152 | if key.endswith("__param_html") and isinstance(value, str): |
143 | 153 | if text: |
144 | 154 | text += "<br><br>" |
145 | 155 | text += value |
146 | | - self.param_label.setText(text) |
| 156 | + self.analysis_parameter_label.setText(text) |
147 | 157 |
|
148 | | - def update_properties_from(self, param: SignalObj | ImageObj | None = None): |
149 | | - """Update properties from signal/image dataset""" |
150 | | - self.properties.setDisabled(param is None) |
151 | | - if param is None: |
152 | | - param = self.paramclass() |
153 | | - self.properties.dataset.set_defaults() |
154 | | - update_dataset(self.properties.dataset, param) |
| 158 | + def update_properties_from(self, obj: SignalObj | ImageObj | None = None) -> None: |
| 159 | + """Update properties from signal/image dataset |
| 160 | +
|
| 161 | + Args: |
| 162 | + obj: Signal or Image object |
| 163 | + """ |
| 164 | + self.properties.setDisabled(obj is None) |
| 165 | + if obj is None: |
| 166 | + obj = self.objclass() |
| 167 | + dataset: SignalObj | ImageObj = self.properties.dataset |
| 168 | + dataset.set_defaults() |
| 169 | + update_dataset(dataset, obj) |
155 | 170 | self.properties.get() |
156 | | - self.set_param_label(param) |
| 171 | + self.display_analysis_parameter(obj) |
157 | 172 | self.properties.apply_button.setEnabled(False) |
158 | 173 |
|
| 174 | + # Store original values to detect which properties have changed |
| 175 | + # Using restore_dataset to convert the dataset to a dictionary |
| 176 | + self.__original_values = {} |
| 177 | + restore_dataset(dataset, self.__original_values) |
| 178 | + |
| 179 | + def get_changed_properties(self) -> dict[str, Any]: |
| 180 | + """Get dictionary of properties that have changed from original values. |
| 181 | +
|
| 182 | + Returns: |
| 183 | + Dictionary mapping property names to their new values, containing only |
| 184 | + the properties that were modified by the user. |
| 185 | + """ |
| 186 | + dataset = self.properties.dataset |
| 187 | + changed = {} |
| 188 | + |
| 189 | + # Get current values as a dictionary |
| 190 | + current_values = {} |
| 191 | + restore_dataset(dataset, current_values) |
| 192 | + |
| 193 | + # Compare with original values |
| 194 | + for key, current_value in current_values.items(): |
| 195 | + original_value = self.__original_values.get(key) |
| 196 | + # Check if value has changed |
| 197 | + if not self._values_equal(current_value, original_value): |
| 198 | + changed[key] = current_value |
| 199 | + return changed |
| 200 | + |
| 201 | + def update_original_values(self) -> None: |
| 202 | + """Update the stored original values to the current dataset values. |
| 203 | +
|
| 204 | + This should be called after applying changes to reset the baseline |
| 205 | + for detecting future changes. |
| 206 | + """ |
| 207 | + dataset = self.properties.dataset |
| 208 | + self.__original_values = {} |
| 209 | + restore_dataset(dataset, self.__original_values) |
| 210 | + |
| 211 | + @staticmethod |
| 212 | + def _values_equal(val1: Any, val2: Any) -> bool: |
| 213 | + """Compare two values, handling special cases like numpy arrays. |
| 214 | +
|
| 215 | + Args: |
| 216 | + val1: first value |
| 217 | + val2: second value |
| 218 | +
|
| 219 | + Returns: |
| 220 | + True if values are equal |
| 221 | + """ |
| 222 | + # Handle numpy arrays |
| 223 | + if isinstance(val1, np.ndarray) or isinstance(val2, np.ndarray): |
| 224 | + if not isinstance(val1, np.ndarray) or not isinstance(val2, np.ndarray): |
| 225 | + return False |
| 226 | + return np.array_equal(val1, val2) |
| 227 | + # Handle regular comparison |
| 228 | + return val1 == val2 |
| 229 | + |
159 | 230 |
|
160 | 231 | class AbstractPanelMeta(type(QW.QSplitter), abc.ABCMeta): |
161 | 232 | """Mixed metaclass to avoid conflicts""" |
@@ -1381,13 +1452,21 @@ def selection_changed(self, update_items: bool = False) -> None: |
1381 | 1452 | def properties_changed(self) -> None: |
1382 | 1453 | """The properties 'Apply' button was clicked: update object properties, |
1383 | 1454 | refresh plot and update object view.""" |
1384 | | - obj = self.objview.get_current_object() |
1385 | | - # if obj is not None: # XXX: Is it necessary? |
1386 | | - obj.mark_roi_as_changed() |
1387 | | - update_dataset(obj, self.objprop.properties.dataset) |
1388 | | - self.objview.update_item(get_uuid(obj)) |
| 1455 | + # Get only the properties that have changed from the original values |
| 1456 | + changed_props = self.objprop.get_changed_properties() |
| 1457 | + |
| 1458 | + # Apply only the changed properties to all selected objects |
| 1459 | + for obj in self.objview.get_sel_objects(include_groups=True): |
| 1460 | + obj.mark_roi_as_changed() |
| 1461 | + # Update only the changed properties instead of all properties |
| 1462 | + update_dataset(obj, changed_props) |
| 1463 | + self.objview.update_item(get_uuid(obj)) |
1389 | 1464 | self.refresh_plot("selected", True, False) |
1390 | 1465 |
|
| 1466 | + # Update the stored original values to reflect the new state |
| 1467 | + # This ensures subsequent changes are compared against the current values |
| 1468 | + self.objprop.update_original_values() |
| 1469 | + |
1391 | 1470 | # ------Plotting data in modal dialogs---------------------------------------------- |
1392 | 1471 | def add_plot_items_to_dialog(self, dlg: PlotDialog, oids: list[str]) -> None: |
1393 | 1472 | """Add plot items to dialog |
|
0 commit comments