From 5f57d3ce0feccb431b6d38fc557369fa32666090 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:38:54 +0100 Subject: [PATCH] feat(plugins): add "New group" option to instrument windows Acquisitions of plugin instruments always created a new group. A "New group" check box next to Acquire now lets the user add them to the current group of the panel instead (a group is still created when the panel has no current group). PluginInstrument.new_group sets the default state, and the user's choice is remembered per instrument. Closes #442 Assisted-by: Claude Opus 5.5 --- datalab/config/options.py | 11 +++ datalab/gui/main.py | 6 +- datalab/gui/plugins/instrument.py | 91 ++++++++++++++----- datalab/locale/fr/LC_MESSAGES/datalab.po | 7 +- datalab/plugins/instruments.py | 13 ++- .../backbone/config_persistence_unit_test.py | 2 + .../plugins/plugin_tiles_unit_test.py | 1 + .../features/plugins/plugin_tools_app_test.py | 77 ++++++++++++++++ doc/features/advanced/plugins.rst | 6 +- .../LC_MESSAGES/features/advanced/plugins.po | 25 +++-- .../LC_MESSAGES/release_notes/release_1.04.po | 3 + doc/release_notes/release_1.04.md | 1 + 12 files changed, 206 insertions(+), 37 deletions(-) diff --git a/datalab/config/options.py b/datalab/config/options.py index 022545bd4..ffe931059 100644 --- a/datalab/config/options.py +++ b/datalab/config/options.py @@ -223,6 +223,17 @@ def __init__(self) -> None: expected_type=bool, description="If True, hide the application list of the catalog.", ) + self.instrument_new_group = TypedOptionField( + self, + "instrument_new_group", + category="main", + default={}, + expected_type=dict, + description=( + "State of the 'New group' option of each instrument window, chosen " + "by the user (key: 'plugin_id/tool_id')." + ), + ) self.v020_plugins_warning_ignore = TypedOptionField( self, "v020_plugins_warning_ignore", diff --git a/datalab/gui/main.py b/datalab/gui/main.py index 9c114a0a0..2eb91b9d6 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -1177,7 +1177,11 @@ def open_plugin_instrument( if worker is None: worker = self.__instrument_workers[key] = SerialWorker() window = InstrumentWindow( - self, plugin.get_instrument(tool.id), tool.title, worker + self, + plugin.get_instrument(tool.id), + tool.title, + worker, + option_key=f"{plugin.plugin_id}/{tool.id}", ) window.finished.connect( functools.partial(self.__forget_instrument_window, key) diff --git a/datalab/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index 0d271e5b3..660b264c9 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -23,7 +23,7 @@ from sigimax.adapters_plotpy.objects.signal import CURVESTYLES from datalab.adapters_plotpy import create_adapter_from_object -from datalab.config import _ +from datalab.config import Conf, _ from datalab.objectmodel import get_uuid if TYPE_CHECKING: @@ -94,12 +94,13 @@ class InstrumentWindow(QW.QDialog): """Live view of a plugin instrument, next to its settings. The view is refreshed when settings change, and periodically in live mode. - Each acquisition is added to the workspace in a new group. The instrument is - opened with the window and closed with it; if opening it fails, the reason - is shown, and it is opened again before the next call. Its methods are - called one at a time in a worker thread: a request made while a call is - running is done next (an acquisition before a refresh, repeated refreshes - only once). + Each acquisition is added to the workspace in a new group or, when the + **New group** option is off, in the current group of the panel. The + instrument is opened with the window and closed with it; if opening it + fails, the reason is shown, and it is opened again before the next call. Its + methods are called one at a time in a worker thread: a request made while a + call is running is done next (an acquisition before a refresh, repeated + refreshes only once). Args: main: DataLab main window @@ -109,6 +110,9 @@ class InstrumentWindow(QW.QDialog): successive windows of the instrument so that a window opened again runs its calls after those of the closed one (default: a worker of its own, stopped when the window is closed) + option_key: key under which the state of the **New group** option is + remembered in the configuration (default: not remembered, the option + starts from :attr:`PluginInstrument.new_group`) """ #: Delay between a settings change and the refresh of the view (ms) @@ -125,11 +129,13 @@ def __init__( instrument: PluginInstrument, title: str, executor: SerialWorker | None = None, + option_key: str | None = None, ) -> None: super().__init__(main) win32_fix_title_bar_background(self) self.main = main self.instrument = instrument + self.option_key = option_key self.setWindowTitle(title.rstrip(".…")) self.setModal(False) self.plotwidget: PlotWidget | None = None @@ -180,10 +186,18 @@ def __init__( self.live_button.toggled.connect(self.set_live) commands.addWidget(self.live_button) commands.addStretch() - self.acquire_button = QW.QPushButton(get_icon("record.svg"), _("Acquire")) - self.acquire_button.setToolTip( - _("Add an acquisition to the workspace, in a new group") + self.new_group_box = QW.QCheckBox(_("New group")) + self.new_group_box.setToolTip( + _( + "Add each acquisition to a new group, " + "otherwise to the current group of the panel" + ) ) + self.new_group_box.setChecked(self._stored_new_group()) + self.new_group_box.toggled.connect(self._store_new_group) + commands.addWidget(self.new_group_box) + self.acquire_button = QW.QPushButton(get_icon("record.svg"), _("Acquire")) + self.acquire_button.setToolTip(_("Add an acquisition to the workspace")) self.acquire_button.clicked.connect(self.acquire) commands.addWidget(self.acquire_button) settings_layout.addLayout(commands) @@ -348,6 +362,21 @@ def set_live(self, live: bool) -> None: else: self._live_timer.stop() + def _stored_new_group(self) -> bool: + """Return the remembered state of the **New group** option, or the + default state set by the instrument.""" + default = bool(self.instrument.new_group) + if self.option_key is None: + return default + return bool(Conf.instrument_new_group.get().get(self.option_key, default)) + + def _store_new_group(self, new_group: bool) -> None: + """Remember the state of the **New group** option chosen by the user.""" + if self.option_key is not None: + states = dict(Conf.instrument_new_group.get()) + states[self.option_key] = bool(new_group) + Conf.instrument_new_group.set(states) + def stop(self) -> None: """Stop refreshing the view and close the instrument.""" self._refresh_timer.stop() @@ -449,36 +478,54 @@ def render(self, frame: InstrumentFrame) -> None: plot.replot() def acquire(self) -> None: - """Add an acquisition to the workspace, in a new group.""" + """Add an acquisition to the workspace, in a new group or in the current + group of the panel, depending on the **New group** option.""" self._request("acquire") def _add_acquired(self, acquisition: InstrumentAcquisition) -> None: """Add the objects acquired by the instrument to the workspace.""" try: - self.add_acquisition(acquisition) + group_title = self.add_acquisition(acquisition) except Exception as exc: # pylint: disable=broad-except self.status_label.setText(str(exc)) return self.status_label.setText( _("%d objects added to group '%s'") - % (len(acquisition.objects), acquisition.group_title) + % (len(acquisition.objects), group_title) ) - def add_acquisition(self, acquisition: InstrumentAcquisition) -> None: - """Add acquired objects to their panel, in a new group.""" + def add_acquisition(self, acquisition: InstrumentAcquisition) -> str: + """Add acquired objects to their panel. + + The objects are added to the current group of the panel if the **New + group** option is off and the panel has a current group, otherwise to a + new group. + + Returns: + Title of the group where the objects were added + """ objects = acquisition.objects panel_name = "signal" if isinstance(objects[0], SignalObj) else "image" panel = ( self.main.signalpanel if panel_name == "signal" else self.main.imagepanel ) with self.main.context_no_refresh(): - group = panel.add_group(acquisition.group_title) - try: + group_id = None + if not self.new_group_box.isChecked(): + group_id = panel.objview.get_current_group_id() + if group_id is None: + group = panel.add_group(acquisition.group_title) + try: + # pylint: disable=protected-access + panel._add_objects(objects, get_uuid(group)) + except Exception: + panel.objview.remove_item(get_uuid(group), refresh=False) + panel.objmodel.remove_group(group) + raise + else: + group = panel.objmodel.get_group(group_id) # pylint: disable=protected-access - panel._add_objects(objects, get_uuid(group)) - except Exception: - panel.objview.remove_item(get_uuid(group), refresh=False) - panel.objmodel.remove_group(group) - raise + panel._add_objects(objects, group_id) self.main.set_current_panel(panel_name) panel.objview.select_objects(objects) + return group.title diff --git a/datalab/locale/fr/LC_MESSAGES/datalab.po b/datalab/locale/fr/LC_MESSAGES/datalab.po index c811b193e..5ca885e5d 100644 --- a/datalab/locale/fr/LC_MESSAGES/datalab.po +++ b/datalab/locale/fr/LC_MESSAGES/datalab.po @@ -2591,8 +2591,11 @@ msgstr "Rafraîchir la vue en continu" msgid "Acquire" msgstr "Acquérir" -msgid "Add an acquisition to the workspace, in a new group" -msgstr "Ajouter une acquisition à l'espace de travail, dans un nouveau groupe" +msgid "Add each acquisition to a new group, otherwise to the current group of the panel" +msgstr "Ajouter chaque acquisition à un nouveau groupe, sinon au groupe courant du panneau" + +msgid "Add an acquisition to the workspace" +msgstr "Ajouter une acquisition à l'espace de travail" msgid "Invalid settings" msgstr "Réglages non valides" diff --git a/datalab/plugins/instruments.py b/datalab/plugins/instruments.py index 0b38ca366..8185dc2b0 100644 --- a/datalab/plugins/instruments.py +++ b/datalab/plugins/instruments.py @@ -66,10 +66,14 @@ def __post_init__(self) -> None: @dataclasses.dataclass(frozen=True) class InstrumentAcquisition: - """Objects acquired by an instrument, added by DataLab in a new group. + """Objects acquired by an instrument, added by DataLab to the workspace. + + DataLab adds them to a new group, or to the current group of the panel when + the user turns off the **New group** option of the instrument window (see + :attr:`PluginInstrument.new_group`). Args: - group_title: title of the group created for the acquisition + group_title: title of the group created for the acquisition, if any objects: acquired signals, or acquired images """ @@ -110,6 +114,11 @@ class PluginInstrument(abc.ABC): #: Interval between two frames in live mode, in milliseconds live_interval_ms: int = 500 + #: Default state of the **New group** option of the instrument window (if + #: True, each acquisition is added to a new group, otherwise to the current + #: group of the panel). The choice of the user, once made, prevails. + new_group: bool = True + def __init__(self, settings: gds.DataSet) -> None: if not isinstance(settings, gds.DataSet): raise TypeError("Instrument settings must be a guidata DataSet") diff --git a/datalab/tests/backbone/config_persistence_unit_test.py b/datalab/tests/backbone/config_persistence_unit_test.py index 120778556..60fe1b355 100644 --- a/datalab/tests/backbone/config_persistence_unit_test.py +++ b/datalab/tests/backbone/config_persistence_unit_test.py @@ -155,6 +155,7 @@ def test_round_trip_across_types() -> None: src.window_size.set((1234, 567)) src.plugins_path_list.set(["/tmp/plugins_a", "/tmp/plugins_b"]) src.plugins_enabled_list.set(["Test Plugin 1"]) + src.instrument_new_group.set({"org.example/tool": False}) src.macro_console_max_lines.set(4242) src.ai_temperature.set(0.9) @@ -173,6 +174,7 @@ def test_round_trip_across_types() -> None: assert dst.window_size.get() == (1234, 567) assert dst.plugins_path_list.get() == ["/tmp/plugins_a", "/tmp/plugins_b"] assert dst.plugins_enabled_list.get() == ["Test Plugin 1"] + assert dst.instrument_new_group.get() == {"org.example/tool": False} assert dst.macro_console_max_lines.get() == 4242 assert abs(dst.ai_temperature.get() - 0.9) < 1e-9 assert dst.traceback_log_path.to_storage() == ".DataLab_custom.log" diff --git a/datalab/tests/backbone/plugins/plugin_tiles_unit_test.py b/datalab/tests/backbone/plugins/plugin_tiles_unit_test.py index 132adb0aa..9d440d331 100644 --- a/datalab/tests/backbone/plugins/plugin_tiles_unit_test.py +++ b/datalab/tests/backbone/plugins/plugin_tiles_unit_test.py @@ -298,6 +298,7 @@ def create_actions(self) -> None: assert opened == [(plugin, generator)] instrument = plugin.get_instrument("generator") assert plugin.get_instrument("generator") is instrument + assert instrument.new_group is True with pytest.raises(ValueError, match="no instrument"): plugin.get_instrument("annotate") plugin._reset_registration_state() # pylint: disable=protected-access diff --git a/datalab/tests/features/plugins/plugin_tools_app_test.py b/datalab/tests/features/plugins/plugin_tools_app_test.py index 9e45d386c..0d74d49d1 100644 --- a/datalab/tests/features/plugins/plugin_tools_app_test.py +++ b/datalab/tests/features/plugins/plugin_tools_app_test.py @@ -251,3 +251,80 @@ def failing_preview(): reloaded = PluginRegistry.get_plugin("org.example.tools-app") assert reloaded.launch_tool("generator") is not window win.close_plugin_instruments() + + +def test_instrument_new_group_option() -> None: + """Acquisitions go to a new group, or to the current group of the panel when + the **New group** option is off; the option is remembered per instrument.""" + with ( + Conf.plugins_enabled.context(True), + Conf.plugins_enabled_list.context(None), + Conf.instrument_new_group.context({}), + temporary_plugin_dir() as plugin_dir, + ): + with open( + f"{plugin_dir}/datalab_test_plugin_tools.py", "w", encoding="utf-8" + ) as handle: + handle.write(PLUGIN_SOURCE) + with datalab_test_app_context(console=False) as win: + plugin = PluginRegistry.get_plugin("org.example.tools-app") + instrument = plugin.get_instrument("generator") + key = "org.example.tools-app/generator" + model = win.signalpanel.objmodel + + def acquire(window: InstrumentWindow) -> None: + window.acquire() + assert window.wait_until_idle() + + # Default state set by the instrument, nothing remembered yet + window = plugin.launch_tool("generator") + assert window.wait_until_idle() + assert window.new_group_box.isChecked() + assert key not in Conf.instrument_new_group.get() + + # Option off, panel without current group: a group is created anyway + window.new_group_box.setChecked(False) + assert Conf.instrument_new_group.get()[key] is False + assert not model.get_groups() + acquire(window) + groups = model.get_groups() + assert [group.title for group in groups] == ["Generator acquisition"] + assert len(groups[0]) == 3 + + # Option off: the next acquisitions go to the current group + acquire(window) + assert len(model.get_groups()) == 1 + assert len(groups[0]) == 6 + assert "group 'Generator acquisition'" in window.status_label.text() + target = win.signalpanel.add_group("Target", select=True) + acquire(window) + assert len(model.get_groups()) == 2 + assert len(groups[0]) == 6 and len(target) == 3 + assert "group 'Target'" in window.status_label.text() + acquire(window) + assert len(target) == 6 + + # Option on: each acquisition goes to a new group + window.new_group_box.setChecked(True) + assert Conf.instrument_new_group.get()[key] is True + acquire(window) + groups = model.get_groups() + assert len(groups) == 3 and len(groups[-1]) == 3 + assert len(target) == 6 + + # The choice of the user is remembered, and prevails over the default + window.new_group_box.setChecked(False) + window.close() + QW.QApplication.processEvents() + window = plugin.launch_tool("generator") + assert not window.new_group_box.isChecked() + window.close() + QW.QApplication.processEvents() + + # Without a remembered choice, the instrument sets the default state + Conf.instrument_new_group.set({}) + instrument.new_group = False + window = plugin.launch_tool("generator") + assert not window.new_group_box.isChecked() + assert key not in Conf.instrument_new_group.get() + win.close_plugin_instruments() diff --git a/doc/features/advanced/plugins.rst b/doc/features/advanced/plugins.rst index e12a1f3ff..1d989fe0e 100644 --- a/doc/features/advanced/plugins.rst +++ b/doc/features/advanced/plugins.rst @@ -746,7 +746,7 @@ The launcher is called without arguments. :meth:`datalab.plugins.PluginBase.get_ Instruments ^^^^^^^^^^^ -A tool may name an ``instrument`` method instead of a ``launcher``. This method returns a :class:`datalab.plugins.instruments.PluginInstrument`, and DataLab opens a window for it: a live view on the left, the instrument settings on the right, a **Live** button that refreshes the view continuously, and an **Acquire** button. The plugin writes no user interface code: +A tool may name an ``instrument`` method instead of a ``launcher``. This method returns a :class:`datalab.plugins.instruments.PluginInstrument`, and DataLab opens a window for it: a live view on the left, the instrument settings on the right, a **Live** button that refreshes the view continuously, a **New group** option and an **Acquire** button. The plugin writes no user interface code: .. code-block:: python @@ -788,7 +788,9 @@ A tool may name an ``instrument`` method instead of a ``launcher``. This method def signal_source(self): return Source() -DataLab edits :attr:`~datalab.plugins.instruments.PluginInstrument.settings` in place, with the active state, groups and tabs of the guidata DataSet, then calls :meth:`~datalab.plugins.instruments.PluginInstrument.preview` after each change and, in live mode, every ``live_interval_ms`` milliseconds. An :class:`~datalab.plugins.instruments.InstrumentFrame` holds signals drawn together or a single image, a short summary shown below the view, and an optional fixed Y range (signals) or color range (image). :meth:`~datalab.plugins.instruments.PluginInstrument.acquire` returns an :class:`~datalab.plugins.instruments.InstrumentAcquisition`: DataLab adds its objects to a new group of the matching panel and selects them, so that a recipe can run on them at once. Both methods raise ``ValueError`` with a user-facing message when the settings cannot be used; DataLab shows the message in the window. +DataLab edits :attr:`~datalab.plugins.instruments.PluginInstrument.settings` in place, with the active state, groups and tabs of the guidata DataSet, then calls :meth:`~datalab.plugins.instruments.PluginInstrument.preview` after each change and, in live mode, every ``live_interval_ms`` milliseconds. An :class:`~datalab.plugins.instruments.InstrumentFrame` holds signals drawn together or a single image, a short summary shown below the view, and an optional fixed Y range (signals) or color range (image). :meth:`~datalab.plugins.instruments.PluginInstrument.acquire` returns an :class:`~datalab.plugins.instruments.InstrumentAcquisition`: DataLab adds its objects to a new group of the matching panel, titled after the acquisition, and selects them, so that a recipe can run on them at once. Both methods raise ``ValueError`` with a user-facing message when the settings cannot be used; DataLab shows the message in the window. + +When the user turns off the **New group** option of the window, DataLab adds the acquired objects to the current group of the panel instead: as they are selected, the next acquisitions go to the same group, until the user selects another group. A new group is still created when the panel has no current group, for example when it is empty. The :attr:`~datalab.plugins.instruments.PluginInstrument.new_group` class attribute sets the default state of the option (``True`` by default): an instrument whose acquisitions are usually single objects, such as a device attribute read on demand, may set it to ``False``. The choice of the user, once made, is remembered for each instrument and prevails over this default. DataLab creates the instrument once per tool and keeps it until the plugins are reloaded, so the settings remain from one opening to the next. diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po index cd638a0f4..55f67bd48 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po @@ -617,11 +617,14 @@ msgstr "Le lanceur est appelé sans argument. :meth:`datalab.plugins.PluginBase. msgid "Instruments" msgstr "Instruments" -msgid "A tool may name an ``instrument`` method instead of a ``launcher``. This method returns a :class:`datalab.plugins.instruments.PluginInstrument`, and DataLab opens a window for it: a live view on the left, the instrument settings on the right, a **Live** button that refreshes the view continuously, and an **Acquire** button. The plugin writes no user interface code:" -msgstr "Un outil peut désigner une méthode ``instrument`` au lieu d'un ``launcher``. Cette méthode renvoie un :class:`datalab.plugins.instruments.PluginInstrument`, et DataLab ouvre une fenêtre pour lui : une vue en direct à gauche, les réglages de l'instrument à droite, un bouton **En direct** qui rafraîchit la vue en continu et un bouton **Acquérir**. Le plugin n'écrit aucun code d'interface utilisateur :" +msgid "A tool may name an ``instrument`` method instead of a ``launcher``. This method returns a :class:`datalab.plugins.instruments.PluginInstrument`, and DataLab opens a window for it: a live view on the left, the instrument settings on the right, a **Live** button that refreshes the view continuously, a **New group** option and an **Acquire** button. The plugin writes no user interface code:" +msgstr "Un outil peut désigner une méthode ``instrument`` au lieu d'un ``launcher``. Cette méthode renvoie un :class:`datalab.plugins.instruments.PluginInstrument`, et DataLab ouvre une fenêtre pour lui : une vue en direct à gauche, les réglages de l'instrument à droite, un bouton **En direct** qui rafraîchit la vue en continu, une option **Nouveau groupe** et un bouton **Acquérir**. Le plugin n'écrit aucun code d'interface utilisateur :" -msgid "DataLab edits :attr:`~datalab.plugins.instruments.PluginInstrument.settings` in place, with the active state, groups and tabs of the guidata DataSet, then calls :meth:`~datalab.plugins.instruments.PluginInstrument.preview` after each change and, in live mode, every ``live_interval_ms`` milliseconds. An :class:`~datalab.plugins.instruments.InstrumentFrame` holds signals drawn together or a single image, a short summary shown below the view, and an optional fixed Y range (signals) or color range (image). :meth:`~datalab.plugins.instruments.PluginInstrument.acquire` returns an :class:`~datalab.plugins.instruments.InstrumentAcquisition`: DataLab adds its objects to a new group of the matching panel and selects them, so that a recipe can run on them at once. Both methods raise ``ValueError`` with a user-facing message when the settings cannot be used; DataLab shows the message in the window." -msgstr "DataLab modifie :attr:`~datalab.plugins.instruments.PluginInstrument.settings` sur place, en respectant l'état actif, les groupes et les onglets du DataSet guidata, puis appelle :meth:`~datalab.plugins.instruments.PluginInstrument.preview` après chaque modification et, en mode direct, toutes les ``live_interval_ms`` millisecondes. Un :class:`~datalab.plugins.instruments.InstrumentFrame` contient des signaux tracés ensemble ou une seule image, un court résumé affiché sous la vue, et une plage fixe facultative en Y (signaux) ou de couleurs (image). :meth:`~datalab.plugins.instruments.PluginInstrument.acquire` renvoie un :class:`~datalab.plugins.instruments.InstrumentAcquisition` : DataLab ajoute ses objets dans un nouveau groupe du panneau correspondant et les sélectionne, afin qu'une recette puisse s'exécuter aussitôt sur eux. Les deux méthodes lèvent ``ValueError`` avec un message destiné à l'utilisateur lorsque les réglages ne peuvent pas être utilisés ; DataLab affiche ce message dans la fenêtre." +msgid "DataLab edits :attr:`~datalab.plugins.instruments.PluginInstrument.settings` in place, with the active state, groups and tabs of the guidata DataSet, then calls :meth:`~datalab.plugins.instruments.PluginInstrument.preview` after each change and, in live mode, every ``live_interval_ms`` milliseconds. An :class:`~datalab.plugins.instruments.InstrumentFrame` holds signals drawn together or a single image, a short summary shown below the view, and an optional fixed Y range (signals) or color range (image). :meth:`~datalab.plugins.instruments.PluginInstrument.acquire` returns an :class:`~datalab.plugins.instruments.InstrumentAcquisition`: DataLab adds its objects to a new group of the matching panel, titled after the acquisition, and selects them, so that a recipe can run on them at once. Both methods raise ``ValueError`` with a user-facing message when the settings cannot be used; DataLab shows the message in the window." +msgstr "DataLab modifie :attr:`~datalab.plugins.instruments.PluginInstrument.settings` sur place, en respectant l'état actif, les groupes et les onglets du DataSet guidata, puis appelle :meth:`~datalab.plugins.instruments.PluginInstrument.preview` après chaque modification et, en mode direct, toutes les ``live_interval_ms`` millisecondes. Un :class:`~datalab.plugins.instruments.InstrumentFrame` contient des signaux tracés ensemble ou une seule image, un court résumé affiché sous la vue, et une plage fixe facultative en Y (signaux) ou de couleurs (image). :meth:`~datalab.plugins.instruments.PluginInstrument.acquire` renvoie un :class:`~datalab.plugins.instruments.InstrumentAcquisition` : DataLab ajoute ses objets dans un nouveau groupe du panneau correspondant, portant le titre de l'acquisition, et les sélectionne, afin qu'une recette puisse s'exécuter aussitôt sur eux. Les deux méthodes lèvent ``ValueError`` avec un message destiné à l'utilisateur lorsque les réglages ne peuvent pas être utilisés ; DataLab affiche ce message dans la fenêtre." + +msgid "When the user turns off the **New group** option of the window, DataLab adds the acquired objects to the current group of the panel instead: as they are selected, the next acquisitions go to the same group, until the user selects another group. A new group is still created when the panel has no current group, for example when it is empty. The :attr:`~datalab.plugins.instruments.PluginInstrument.new_group` class attribute sets the default state of the option (``True`` by default): an instrument whose acquisitions are usually single objects, such as a device attribute read on demand, may set it to ``False``. The choice of the user, once made, is remembered for each instrument and prevails over this default." +msgstr "Lorsque l'utilisateur désactive l'option **Nouveau groupe** de la fenêtre, DataLab ajoute plutôt les objets acquis au groupe courant du panneau : comme ils sont sélectionnés, les acquisitions suivantes vont dans le même groupe, jusqu'à ce que l'utilisateur sélectionne un autre groupe. Un nouveau groupe est tout de même créé lorsque le panneau n'a pas de groupe courant, par exemple lorsqu'il est vide. L'attribut de classe :attr:`~datalab.plugins.instruments.PluginInstrument.new_group` définit l'état par défaut de l'option (``True`` par défaut) : un instrument dont les acquisitions sont généralement des objets uniques, comme un attribut d'appareil lu à la demande, peut le mettre à ``False``. Une fois fait, le choix de l'utilisateur est mémorisé pour chaque instrument et prévaut sur cette valeur par défaut." msgid "DataLab creates the instrument once per tool and keeps it until the plugins are reloaded, so the settings remain from one opening to the next." msgstr "DataLab crée l'instrument une seule fois par outil et le conserve jusqu'au rechargement des plugins : les réglages sont ainsi conservés d'une ouverture à l'autre." @@ -1551,11 +1554,14 @@ msgstr "Instruments exposés comme outils par les plugins d'application." msgid "An instrument is a headless object: it holds settings (a guidata DataSet), returns a live frame for these settings, and acquires objects. DataLab shows it in a window with the live view on the left and the settings on the right. DataLab Desktop and DataLab-Web share this module." msgstr "Un instrument est un objet sans interface : il porte des réglages (un DataSet guidata), renvoie une image en direct pour ces réglages et acquiert des objets. DataLab l'affiche dans une fenêtre avec la vue en direct à gauche et les réglages à droite. DataLab Desktop et DataLab-Web partagent ce module." -msgid "Objects acquired by an instrument, added by DataLab in a new group." -msgstr "Objets acquis par un instrument, ajoutés par DataLab dans un nouveau groupe." +msgid "Objects acquired by an instrument, added by DataLab to the workspace." +msgstr "Objets acquis par un instrument, ajoutés par DataLab à l'espace de travail." + +msgid "DataLab adds them to a new group, or to the current group of the panel when the user turns off the **New group** option of the instrument window (see :attr:`PluginInstrument.new_group`)." +msgstr "DataLab les ajoute à un nouveau groupe, ou au groupe courant du panneau lorsque l'utilisateur désactive l'option **Nouveau groupe** de la fenêtre de l'instrument (voir :attr:`PluginInstrument.new_group`)." -msgid "title of the group created for the acquisition" -msgstr "titre du groupe créé pour l'acquisition" +msgid "title of the group created for the acquisition, if any" +msgstr "titre du groupe créé pour l'acquisition, le cas échéant" msgid "acquired signals, or acquired images" msgstr "signaux acquis, ou images acquises" @@ -1587,6 +1593,9 @@ msgstr "réglages de l'instrument, modifiés par DataLab" msgid "Interval between two frames in live mode, in milliseconds" msgstr "Intervalle entre deux images en mode direct, en millisecondes" +msgid "Default state of the **New group** option of the instrument window (if True, each acquisition is added to a new group, otherwise to the current group of the panel). The choice of the user, once made, prevails." +msgstr "État par défaut de l'option **Nouveau groupe** de la fenêtre de l'instrument (si True, chaque acquisition est ajoutée à un nouveau groupe, sinon au groupe courant du panneau). Une fois fait, le choix de l'utilisateur prévaut." + msgid "Prepare the instrument when its window opens (e.g. connect to it)." msgstr "Préparer l'instrument à l'ouverture de sa fenêtre (par exemple s'y connecter)." diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index cd38b735b..b62c6f3ac 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -92,6 +92,9 @@ msgstr "Les outils de plugin apparaissent aussi dans le sous-menu du plugin du m msgid "Application plugins may provide instruments, such as simulated cameras or oscilloscopes, without writing any user interface: DataLab shows a window with a live view and the instrument settings, refreshed as settings change or continuously in **Live** mode, and **Acquire** adds the acquired objects to a new group, ready for the application's methods" msgstr "Les plugins d'application peuvent fournir des instruments, comme des caméras ou des oscilloscopes simulés, sans écrire d'interface utilisateur : DataLab affiche une fenêtre avec une vue en direct et les réglages de l'instrument, rafraîchie à chaque modification des réglages ou en continu en mode **En direct**, et **Acquérir** ajoute les objets acquis dans un nouveau groupe, prêts pour les méthodes de l'application" +msgid "The **New group** option of the instrument window may be turned off to add the next acquisitions to the current group of the panel instead of creating a group for each of them. Each instrument sets the default state of the option, and the choice of the user is remembered for each instrument" +msgstr "L'option **Nouveau groupe** de la fenêtre d'instrument peut être désactivée pour ajouter les acquisitions suivantes au groupe courant du panneau, au lieu de créer un groupe pour chacune d'elles. Chaque instrument définit l'état par défaut de l'option, et le choix de l'utilisateur est mémorisé pour chaque instrument" + msgid "Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it. Calls stay one at a time when a window is closed and opened again while a call is running. When `open` fails, its message stays in the window and the instrument is opened again before the next refresh or acquisition, for example after a settings change" msgstr "Les instruments sont appelés en dehors du fil d'exécution de l'interface utilisateur, un appel à la fois : un instrument lent, comme un appareil réel attendant ses données, ne fige plus DataLab, et les demandes de rafraîchissement répétées entre-temps sont regroupées. Les instruments peuvent aussi déclarer des méthodes `open` et `close`, appelées à l'ouverture et à la fermeture de leur fenêtre (y compris lors du rechargement des plugins ou de la fermeture de DataLab), par exemple pour se connecter à un appareil puis le libérer. Les appels restent successifs lorsqu'une fenêtre est fermée puis rouverte pendant un appel. Lorsque `open` échoue, son message reste affiché dans la fenêtre et l'instrument est ouvert à nouveau avant le rafraîchissement ou l'acquisition suivant, par exemple après une modification des réglages" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 32f5f2b58..53a4b87d4 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -37,6 +37,7 @@ * Application plugins may also list tools, such as wizards or editors, in the catalog * Plugin tools also appear in the plugin's submenu of the **Plugins** menu, in the Signal or Image panel they work on. A tool that needs a selection is disabled until the right objects are selected, and its catalog button tells what to select * Application plugins may provide instruments, such as simulated cameras or oscilloscopes, without writing any user interface: DataLab shows a window with a live view and the instrument settings, refreshed as settings change or continuously in **Live** mode, and **Acquire** adds the acquired objects to a new group, ready for the application's methods +* The **New group** option of the instrument window may be turned off to add the next acquisitions to the current group of the panel instead of creating a group for each of them. Each instrument sets the default state of the option, and the choice of the user is remembered for each instrument * Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it. Calls stay one at a time when a window is closed and opened again while a call is running. When `open` fails, its message stays in the window and the instrument is opened again before the next refresh or acquisition, for example after a settings change * Plugin developers describe recipe inputs declaratively (titles, minimum counts, metadata requirements) and may add binding suggestions and fast input checks; every recipe then runs through DataLab's generic launcher, without plugin-specific input dialogs * The catalog is non-modal and remains open after starting a method or opening an example, so users may continue interacting with the DataLab workspace