Skip to content

Commit c425ee6

Browse files
committed
Add multi-object property editing support
Allow applying property changes to multiple selected signals/images simultaneously. Only modified properties are updated, preserving individual object settings.
1 parent 6db9ee1 commit c425ee6

File tree

4 files changed

+140
-35
lines changed

4 files changed

+140
-35
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ See DataLab [roadmap page](https://datalab-platform.com/en/contributing/roadmap.
66

77
💥 New features and enhancements:
88

9+
* Multi-object property editing:
10+
* The properties panel now supports applying property changes to multiple selected objects simultaneously.
11+
* When multiple signals or images are selected, modifying and applying properties updates all selected objects, not just the current one.
12+
* Only the properties that were actually modified are applied to the selected objects, preserving individual object properties that were not changed.
13+
* Typical use case: changing the LUT boundaries (Z scale bounds) or colormap of multiple images at once, or adjusting display properties for a group of signals.
14+
* This significantly improves workflow efficiency when working with multiple objects that need similar property adjustments.
15+
916
* Internal console status indicator added to the status bar:
1017
* The status bar now features an indicator for the internal console, visible only when the console is hidden.
1118
* Clicking the indicator opens the internal console.

datalab/gui/panel/base.py

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import numpy as np
2121
import plotpy.io
2222
from guidata.configtools import get_icon
23-
from guidata.dataset import update_dataset
23+
from guidata.dataset import restore_dataset, update_dataset
2424
from guidata.qthelpers import add_actions, create_action, exec_dialog
2525
from plotpy.plot import PlotDialog
2626
from plotpy.tools import ActionTool
@@ -96,66 +96,137 @@ def is_plot_item_serializable(item: Any) -> bool:
9696

9797

9898
class ObjectProp(QW.QWidget):
99-
"""Object handling panel properties"""
99+
"""Object handling panel properties
100100
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:
102107
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)
105110
self.properties.SIG_APPLY_BUTTON_CLICKED.connect(panel.properties_changed)
106111
self.properties.setEnabled(False)
112+
self.__original_values: dict[str, Any] = {}
107113

108114
self.add_prop_layout = QW.QHBoxLayout()
109115
playout: QW.QGridLayout = self.properties.edit.layout
110116
playout.addLayout(
111117
self.add_prop_layout, playout.rowCount() - 1, 0, 1, 1, QC.Qt.AlignLeft
112118
)
113119

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(
117123
QC.Qt.TextBrowserInteraction | QC.Qt.TextSelectableByKeyboard
118124
)
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)
123129

124130
child: QW.QTabWidget = None
125131
for child in self.properties.children():
126132
if isinstance(child, QW.QTabWidget):
127133
break
128-
child.addTab(param_scroll, _("Analysis parameters"))
134+
child.addTab(analysis_parameter_scroll, _("Analysis parameters"))
129135

130136
vlayout = QW.QVBoxLayout()
131137
vlayout.addWidget(self.properties)
132138
self.setLayout(vlayout)
133139

134-
def add_button(self, button):
140+
def add_button(self, button: QW.QPushButton) -> None:
135141
"""Add additional button on bottom of properties panel"""
136142
self.add_prop_layout.addWidget(button)
137143

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+
"""
140150
text = ""
141-
for key, value in param.metadata.items():
151+
for key, value in obj.metadata.items():
142152
if key.endswith("__param_html") and isinstance(value, str):
143153
if text:
144154
text += "<br><br>"
145155
text += value
146-
self.param_label.setText(text)
156+
self.analysis_parameter_label.setText(text)
147157

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)
155170
self.properties.get()
156-
self.set_param_label(param)
171+
self.display_analysis_parameter(obj)
157172
self.properties.apply_button.setEnabled(False)
158173

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+
159230

160231
class AbstractPanelMeta(type(QW.QSplitter), abc.ABCMeta):
161232
"""Mixed metaclass to avoid conflicts"""
@@ -1381,13 +1452,21 @@ def selection_changed(self, update_items: bool = False) -> None:
13811452
def properties_changed(self) -> None:
13821453
"""The properties 'Apply' button was clicked: update object properties,
13831454
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))
13891464
self.refresh_plot("selected", True, False)
13901465

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+
13911470
# ------Plotting data in modal dialogs----------------------------------------------
13921471
def add_plot_items_to_dialog(self, dlg: PlotDialog, oids: list[str]) -> None:
13931472
"""Add plot items to dialog

doc/locale/fr/LC_MESSAGES/changelog.po

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ msgid ""
77
msgstr ""
88
"Project-Id-Version: DataLab \n"
99
"Report-Msgid-Bugs-To: \n"
10-
"POT-Creation-Date: 2025-10-04 16:52+0200\n"
10+
"POT-Creation-Date: 2025-10-06 18:17+0200\n"
1111
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1212
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1313
"Language: fr\n"
@@ -30,6 +30,24 @@ msgstr "DataLab Version 1.0.0"
3030
msgid "💥 New features and enhancements:"
3131
msgstr "💥 Nouvelles fonctionnalités et améliorations :"
3232

33+
msgid "Multi-object property editing:"
34+
msgstr "Édition des propriétés de plusieurs objets :"
35+
36+
msgid "The properties panel now supports applying property changes to multiple selected objects simultaneously."
37+
msgstr "Le panneau des propriétés prend désormais en charge l'application des modifications de propriétés à plusieurs objets sélectionnés simultanément."
38+
39+
msgid "When multiple signals or images are selected, modifying and applying properties updates all selected objects, not just the current one."
40+
msgstr "Lorsque plusieurs signaux ou images sont sélectionnés, la modification et l'application des propriétés mettent à jour tous les objets sélectionnés, et pas seulement le courant."
41+
42+
msgid "Only the properties that were actually modified are applied to the selected objects, preserving individual object properties that were not changed."
43+
msgstr "Seules les propriétés qui ont été réellement modifiées sont appliquées aux objets sélectionnés, préservant les propriétés individuelles des objets qui n'ont pas été changées."
44+
45+
msgid "Typical use case: changing the LUT boundaries (Z scale bounds) or colormap of multiple images at once, or adjusting display properties for a group of signals."
46+
msgstr "Cas d'utilisation typique : modification des limites de la LUT (bornes de l'échelle Z) ou de la carte des couleurs de plusieurs images à la fois, ou ajustement des propriétés d'affichage d'un groupe de signaux."
47+
48+
msgid "This significantly improves workflow efficiency when working with multiple objects that need similar property adjustments."
49+
msgstr "Cela améliore considérablement l'efficacité du flux de travail lors de la manipulation de plusieurs objets nécessitant des ajustements de propriétés similaires."
50+
3351
msgid "Internal console status indicator added to the status bar:"
3452
msgstr "Indicateur d'état de la console interne ajouté à la barre d'état :"
3553

doc/locale/fr/LC_MESSAGES/features/general/remote.po

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ msgid ""
88
msgstr ""
99
"Project-Id-Version: DataLab \n"
1010
"Report-Msgid-Bugs-To: \n"
11-
"POT-Creation-Date: 2025-10-03 14:57+0200\n"
11+
"POT-Creation-Date: 2025-10-06 18:17+0200\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <[email protected]>\n"
@@ -158,10 +158,10 @@ msgstr ""
158158
msgid "XML-RPC port to connect to. If not specified, the port is automatically retrieved from DataLab configuration."
159159
msgstr ""
160160

161-
msgid "Timeout in seconds. Defaults to 5.0."
161+
msgid "Maximum time to wait for connection in seconds. Defaults to 5.0. This is the total maximum wait time, not per retry."
162162
msgstr ""
163163

164-
msgid "Number of retries. Defaults to 10."
164+
msgid "Number of retries. Defaults to 10. This parameter is deprecated and will be removed in a future version (kept for backward compatibility)."
165165
msgstr ""
166166

167167
msgid "Raises"
@@ -508,3 +508,4 @@ msgstr ""
508508

509509
msgid "Show titles state"
510510
msgstr ""
511+

0 commit comments

Comments
 (0)