Show only the bundle's name in the sensor configuration row

The row carried the origin, the calibration selection and the SDK's verdict, which a 30 px
line cannot explain; it is now the descriptor's display_name, else the folder name, and the
sensor dialog states the rest under the selector — name and folder, profile, shape, root,
verdict, calibration and every finding — with a suffix only where two rows would otherwise
read alike. Verified with the studio suite, the catalog tests in shared/tests, ui_check and
the lint gate.
This commit is contained in:
ykcheungab
2026-10-05 17:01:42 +08:00
parent 92b2f2fcd7
commit d11441cd79
7 changed files with 457 additions and 135 deletions
@@ -22,6 +22,7 @@ from orisys_app_shared.sensors import (
SHAPE_COMPATIBILITY,
SHAPE_DESCRIPTOR,
SHAPE_UNREADABLE,
STATE_UNREADABLE,
)
from PySide6.QtWidgets import (
QDialog,
@@ -42,6 +43,16 @@ _SHAPE_KEYS: dict[str, str] = {
SHAPE_UNREADABLE: "sensor.catalog_shape_unreadable",
}
#: The SDK's verdicts, and the catalog's own "nothing could be read here", as the sentence that
#: says what the verdict means for the operator. Same rule as the shapes: a literal key per state,
#: and an unknown state falls back to words rather than to the identifier the SDK happened to use.
_STATE_KEYS: dict[str, str] = {
"ok": "sensor.catalog_state_ok",
"degraded": "sensor.catalog_state_degraded",
"failed": "sensor.catalog_state_failed",
STATE_UNREADABLE: "sensor.catalog_state_invalid",
}
class SensorDialog(QDialog):
"""Sensor type / config picker plus the calibration facts that follow it."""
@@ -62,6 +73,17 @@ class SensorDialog(QDialog):
self.picker_row.addStretch(1)
root.addLayout(self.picker_row)
#: Everything behind the selected configuration, said in full: the bundle's own name and
#: the folder it lives in, the profile the SDK assigned, how the bundle was read, the
#: root it came from, the verdict with what it means, the calibration selection and every
#: finding — plus, when it must not be started, why. It sits directly under the selector
#: on purpose: the picker row carries the name alone, so this is the surface the rest has
#: to be readable on, without a hover and without hunting for it.
self.profile_label = ShrinkableLabel()
self.profile_label.setObjectName("panelMeta")
self.profile_label.setWordWrap(True)
root.addWidget(self.profile_label)
root.addWidget(self._section_rule())
self.calib_title = QLabel()
self.calib_title.setObjectName("panelTitle")
@@ -72,12 +94,6 @@ class SensorDialog(QDialog):
self.axes_label.setObjectName("panelMeta")
self.config_label = ShrinkableLabel()
self.config_label.setObjectName("panelMeta")
#: What the catalog says the selected configuration *is*: family, profile, origin and
#: the SDK's verdict — plus, when it must not be started, why. The picker can only fit
#: a label on a 30 px rail; the reason needs this room.
self.profile_label = ShrinkableLabel()
self.profile_label.setObjectName("panelMeta")
self.profile_label.setWordWrap(True)
for label in (self.version_label, self.axes_label):
root.addWidget(label)
config_row = QHBoxLayout()
@@ -88,7 +104,6 @@ class SensorDialog(QDialog):
config_row.addWidget(self.btn_reveal)
config_row.addStretch(1)
root.addLayout(config_row)
root.addWidget(self.profile_label)
root.addWidget(self._section_rule())
self.export_title = QLabel()
@@ -203,10 +218,11 @@ class SensorDialog(QDialog):
def _refresh_profile(self) -> None:
"""State the selected catalog entry, and the reason when it cannot be started.
This is the surface that keeps the facts the picker row no longer carries: the identity
the SDK assigned, how the bundle was read and its validation. It is read from the picker
rather than re-derived here, so the window and the dialog can never say two different
things about one bundle.
This is the surface that keeps the facts the picker row no longer carries: the name and
the folder, the identity the SDK assigned, how the bundle was read, the root it was found
in, the verdict with what it means, the calibration selection, and every finding as a
sentence. It is read from the picker rather than re-derived here, so the window and the
dialog can never say two different things about one bundle.
"""
read_entry = getattr(self.picker, "selected_entry", None)
entry = read_entry() if callable(read_entry) else None
@@ -214,19 +230,40 @@ class SensorDialog(QDialog):
self.profile_label.setText("")
self.profile_label.setToolTip("")
return
line = t(
"sensor.catalog_profile",
profile_id=entry.profile_id or entry.folder,
shape=t(_SHAPE_KEYS.get(entry.shape, _SHAPE_KEYS[SHAPE_UNREADABLE])),
origin=entry.origin,
state=entry.state,
)
lines = [
t("sensor.catalog_name", name=entry.name, folder=entry.folder),
t(
"sensor.catalog_profile",
profile_id=entry.profile_id or entry.folder,
shape=t(_SHAPE_KEYS.get(entry.shape, _SHAPE_KEYS[SHAPE_UNREADABLE])),
origin=entry.origin,
),
t("sensor.catalog_root", origin=entry.origin, path=entry.bundle_path),
t(
"sensor.catalog_state",
state=entry.state,
note=t(_STATE_KEYS.get(entry.state, "sensor.catalog_state_unknown")),
),
(
t("sensor.catalog_calibration_selected", name=entry.selected_calibration)
if entry.selected_calibration
else t("sensor.catalog_calibration_none")
),
]
if entry.duplicates:
lines.append(t("sensor.catalog_duplicate", refs=", ".join(entry.duplicates)))
blocked = entry.start_blocked_reason
if blocked:
# In the label, not only the tooltip: a refused configuration has to say why without
# being hovered over, and this is the surface that has room for the sentence.
line = f"{line}\n{t('sensor.catalog_blocked', reason=blocked)}"
self.profile_label.setText(line)
lines.append(t("sensor.catalog_blocked", reason=blocked))
lines.extend(
t("sensor.catalog_error", sentence=text) for text in entry.error_sentences()
)
lines.extend(
t("sensor.catalog_warning", sentence=text) for text in entry.warning_sentences()
)
self.profile_label.setText("\n".join(lines))
self.profile_label.setToolTip(entry.tooltip())
def retranslate(self) -> None:
@@ -1,18 +1,22 @@
"""Sensor type + config picker.
One row, because it lives at the left of the status rail: a badge naming the
selected product, then the product/sensor choice and the config file that
belongs to it. The order matters — picking ``box`` / ``mini`` / ``finger`` is
what decides which configs exist, so the two sit next to each other with their
own labels and tooltips instead of a bare "配置" combo.
selected product, then the product/sensor choice and the configuration bundle
that belongs to it. The order matters — picking ``box`` / ``mini`` / ``finger``
is what decides which configurations exist, so the two sit next to each other
with their own labels and tooltips instead of a bare "配置" combo.
Both combos are filled from the configuration catalog
(:mod:`orisys_app_shared.sensors`), not from a hard-coded product list: the type
combo offers the families bundles actually exist for, and the config combo offers
the catalog's entries for the chosen family — each labelled with what it is, where
it came from and what the calibration selection gives, so the choice is between
products and outcomes rather than between ``profile_id`` strings. An entry the SDK
rejects stays listed, because that is how its reason reaches the operator, and
the catalog's entries for the chosen family. A row is the **bundle's name** — the
descriptor's ``display_name``, else the folder name — and nothing else; the origin,
the calibration selection and the SDK's verdict are read in the dialog, whose
details surface sits directly below this row. The one exception is a collision: two
entries offered together that would otherwise read identically (``shipped/box``
beside ``sensors-user/box``) each carry the suffix that tells them apart, so no
entry is collapsed or shadowed. An entry the SDK rejects stays listed too, because
that is how its reason reaches the operator, and
:meth:`SensorPickerWidget.start_blocked_reason` states why the current selection
must not be started (see also :meth:`selected_entry`).
"""
@@ -186,7 +190,7 @@ class SensorPickerWidget(QWidget):
self.config_combo.blockSignals(True)
self.config_combo.clear()
for entry in entries:
self.config_combo.addItem(_entry_label(entry), entry.key)
self.config_combo.addItem(_entry_label(self._catalog, entry), entry.key)
self.config_combo.setItemData(
self.config_combo.count() - 1,
entry.tooltip(),
@@ -238,12 +242,14 @@ def _family_label(family: str) -> str:
return t("sensor.family_unidentified") if family == FAMILY_UNIDENTIFIED else family
def _entry_label(entry: CatalogEntry) -> str:
"""One combo item: the catalog's operator label, with nothing internal in it.
def _entry_label(catalog: SensorCatalog, entry: CatalogEntry) -> str:
"""One combo item: the catalog's row for an entry, with nothing internal in it.
The catalog names an entry by its display name, so the internal marker for "no product
family" cannot reach the row today; it is still substituted here because a sentinel is
never something an operator should read, and the item text is the one place it could
surface if an entry ever had to fall back to its family.
The row is the bundle's name, and the catalog appends the origin only where two visible
entries would otherwise read alike, so the internal marker for "no product family" cannot
reach it today; it is still substituted here, where translating belongs, because a sentinel
is never something an operator should read and the item text is the one place it could
surface.
"""
return entry.label().replace(FAMILY_UNIDENTIFIED, t("sensor.family_unidentified"))
row = catalog.row_label(entry)
return row.replace(FAMILY_UNIDENTIFIED, t("sensor.family_unidentified"))