diff --git a/negpy/desktop/view/styles/templates.py b/negpy/desktop/view/styles/templates.py index e2f120b9..565087c5 100644 --- a/negpy/desktop/view/styles/templates.py +++ b/negpy/desktop/view/styles/templates.py @@ -40,16 +40,20 @@ def default_button_height() -> int: return _default_btn_height -def wrap_tooltip(text: str) -> str: +def wrap_tooltip(text: str, footer: str = "") -> str: """Plain-text tooltips never word-wrap in Qt; rich text does. Wrap in so long tooltips break into lines instead of spanning the screen. Text that already carries markup (e.g. tooltip_with_shortcut's chips) must pass through - unescaped or its tags render as literal text.""" - if text.startswith(""): - return text - if "<" in text and ">" in text: - return f"{text}" - return f"{html.escape(text)}" + unescaped or its tags render as literal text. + + `footer` is trusted markup appended inside the document, so callers adding + a boilerplate line don't have to re-implement the escape/passthrough rule.""" + if not text.startswith(""): + body = text if ("<" in text and ">" in text) else html.escape(text) + text = f"{body}" + if footer: + text = text.removesuffix("") + footer + "" + return text def hint_label(text: str = "", kind: str = "muted") -> QLabel: diff --git a/negpy/desktop/view/widgets/export_settings_form.py b/negpy/desktop/view/widgets/export_settings_form.py index 3e8bfa3e..098cea2e 100644 --- a/negpy/desktop/view/widgets/export_settings_form.py +++ b/negpy/desktop/view/widgets/export_settings_form.py @@ -129,12 +129,12 @@ def _build_jxl(self, root: QVBoxLayout) -> None: jxl_box.addWidget(self.jxl_lossless_check) self.jxl_distance_spin = CompactSlider("Distance", 0.0, 15.0, 1.0, step=0.1) - self.jxl_distance_spin.label.setToolTip("libjxl distance: ~1.0 ≈ visually lossless, higher = more loss") + self.jxl_distance_spin.setToolTip("libjxl distance: ~1.0 ≈ visually lossless, higher = more loss") self.jxl_distance_spin.valueChanged.connect(self._on_changed) jxl_box.addWidget(self.jxl_distance_spin) self.jxl_effort_spin = CompactSlider("Effort", 1, 9, 7, step=1, precision=1) - self.jxl_effort_spin.label.setToolTip("Encoder effort: higher = slower, smaller file") + self.jxl_effort_spin.setToolTip("Encoder effort: higher = slower, smaller file") self.jxl_effort_spin.valueChanged.connect(self._on_changed) jxl_box.addWidget(self.jxl_effort_spin) @@ -154,12 +154,12 @@ def _build_webp(self, root: QVBoxLayout) -> None: webp_box.addWidget(self.webp_lossless_check) self.webp_quality_spin = CompactSlider("Quality", 1, 100, 90, step=1, precision=1) - self.webp_quality_spin.label.setToolTip("Lossy: visual quality. Lossless: compression effort.") + self.webp_quality_spin.setToolTip("Lossy: visual quality. Lossless: compression effort.") self.webp_quality_spin.valueChanged.connect(self._on_changed) webp_box.addWidget(self.webp_quality_spin) self.webp_method_spin = CompactSlider("Method", 0, 6, 4, step=1, precision=1) - self.webp_method_spin.label.setToolTip("Encoder effort: higher = slower, smaller file") + self.webp_method_spin.setToolTip("Encoder effort: higher = slower, smaller file") self.webp_method_spin.valueChanged.connect(self._on_changed) webp_box.addWidget(self.webp_method_spin) diff --git a/negpy/desktop/view/widgets/sliders.py b/negpy/desktop/view/widgets/sliders.py index 7d6ff3f7..bb60569f 100644 --- a/negpy/desktop/view/widgets/sliders.py +++ b/negpy/desktop/view/widgets/sliders.py @@ -15,6 +15,10 @@ from negpy.desktop.view.styles.templates import EditedDot, slider_label_qss, slider_handle_qss, wrap_tooltip +# text_secondary, not text_muted: #555 on the #161616 tooltip background is ~2.4:1. +_RESET_HINT = f'
Double-click to reset
' + + class _NoScrollSlider(QSlider): def __init__(self, *args, default_pos: Optional[float] = None, **kwargs): super().__init__(*args, **kwargs) @@ -108,7 +112,7 @@ class BaseSlider(QWidget): dragEnded = pyqtSignal() def setToolTip(self, text: str) -> None: - super().setToolTip(wrap_tooltip(text)) + super().setToolTip(wrap_tooltip(text, _RESET_HINT)) def __init__( self, @@ -288,7 +292,7 @@ def __init__( header.setSpacing(2) # explicit: the VBox spacing (0) would otherwise collapse the label<->edited-dot gap self.label = QLabel(label) self.label.setStyleSheet(slider_label_qss(self._label_color)) - self.label.setToolTip(f"{label} (double-click to reset)") + self.setToolTip(label) self._edited_dot = EditedDot() @@ -324,6 +328,13 @@ def __init__( layout.addLayout(header) layout.addWidget(self.slider) + def setToolTip(self, text: str) -> None: + """Mirror onto the label: a child with its own tooltip shadows the parent's, and + without one here the label would show nothing on hover. toolTip() is the already + wrapped string, so the reset footer is never appended twice.""" + super().setToolTip(text) + self.label.setToolTip(self.toolTip()) + def enterEvent(self, event) -> None: self.spin.setMaximumWidth(self._spin_full_width) super().enterEvent(event) diff --git a/tests/test_slider_tooltips.py b/tests/test_slider_tooltips.py new file mode 100644 index 00000000..f7b12f31 --- /dev/null +++ b/tests/test_slider_tooltips.py @@ -0,0 +1,54 @@ +"""A CompactSlider's title label must show the same tooltip as its groove: a child +with its own tooltip shadows the parent's, so the label used to repeat only the +control name while the explanatory text sat on the container.""" + +from negpy.desktop.view.shortcut_registry import tooltip_with_shortcut +from negpy.desktop.view.widgets.sliders import CompactSlider + + +def test_label_tooltip_defaults_to_name_plus_reset_hint(): + s = CompactSlider("Density", 0.0, 4.0, 1.0) + + assert s.toolTip().startswith("") + assert "Density" in s.toolTip() + assert "Double-click to reset" in s.toolTip() + assert s.label.toolTip() == s.toolTip() + + +def test_explanatory_tooltip_reaches_both_halves(): + s = CompactSlider("Density", 0.0, 4.0, 1.0) + s.setToolTip("Overall print density — lower = brighter") + + assert "Overall print density" in s.toolTip() + assert s.toolTip().count("Double-click to reset") == 1 + assert s.label.toolTip() == s.toolTip() + + +def test_shortcut_chips_survive_unescaped(): + s = CompactSlider("Density", 0.0, 4.0, 1.0) + s.setToolTip(tooltip_with_shortcut("Print density", ["density_up", "density_down"])) + + assert " document. + assert s.toolTip().index("") + assert s.label.toolTip() == s.toolTip() + + +def test_plain_text_still_escaped(): + s = CompactSlider("Ratio", 0.0, 4.0, 1.0) + s.setToolTip("a < b") + + assert "a < b" in s.toolTip() + + +def test_export_form_sliders_tooltip_the_container(): + """These four used to tooltip only .label, leaving the groove bare.""" + from negpy.desktop.view.widgets.export_settings_form import ExportSettingsForm + + form = ExportSettingsForm() + + assert "libjxl" in form.jxl_distance_spin.toolTip() + assert form.jxl_distance_spin.label.toolTip() == form.jxl_distance_spin.toolTip() + assert "Encoder effort" in form.webp_method_spin.toolTip()