Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions negpy/desktop/view/styles/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <qt> 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("<qt>"):
return text
if "<" in text and ">" in text:
return f"<qt>{text}</qt>"
return f"<qt>{html.escape(text)}</qt>"
unescaped or its tags render as literal text.

`footer` is trusted markup appended inside the <qt> document, so callers adding
a boilerplate line don't have to re-implement the escape/passthrough rule."""
if not text.startswith("<qt>"):
body = text if ("<" in text and ">" in text) else html.escape(text)
text = f"<qt>{body}</qt>"
if footer:
text = text.removesuffix("</qt>") + footer + "</qt>"
return text


def hint_label(text: str = "", kind: str = "muted") -> QLabel:
Expand Down
8 changes: 4 additions & 4 deletions negpy/desktop/view/widgets/export_settings_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down
15 changes: 13 additions & 2 deletions negpy/desktop/view/widgets/sliders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'<div style="color:{THEME.text_secondary};">Double-click to reset</div>'


class _NoScrollSlider(QSlider):
def __init__(self, *args, default_pos: Optional[float] = None, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions tests/test_slider_tooltips.py
Original file line number Diff line number Diff line change
@@ -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("<qt>")
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 "<table" in s.toolTip()
assert "&lt;table" not in s.toolTip()
# The hint follows the chips, inside the <qt> document.
assert s.toolTip().index("<table") < s.toolTip().index("Double-click to reset")
assert s.toolTip().endswith("</qt>")
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 &lt; 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()
Loading