Skip to content

Commit 0d37c19

Browse files
authored
Merge pull request #908 from eoyilmaz/901-qt-visual-whitepoint-editor-pin-float
[#901] Add pin/float button to Qt visual whitepoint editor controls panel
2 parents eeeff9e + 3e765a6 commit 0d37c19

4 files changed

Lines changed: 155 additions & 3 deletions

File tree

DisplayCAL/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.10.0.dev61
1+
3.10.0.dev62

DisplayCAL/lang/en.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2662,6 +2662,10 @@
26622662
Visual whitepoint editor
26632663
"whitepoint.visual_editor.display_changed.warning": |-
26642664
Attention: The display configuration has changed and the visual whitepoint editor may no longer be able to correctly restore display profiles. Please check display profile associations carefully after closing the editor and restart DisplayCAL.
2665+
"whitepoint.visual_editor.panel.dock": |-
2666+
Attach panel to editor window
2667+
"whitepoint.visual_editor.panel.float": |-
2668+
Detach panel into its own window
26652669
"whitepoint.xy": |-
26662670
Chromaticity coordinates
26672671
"window.title": |-

DisplayCAL/ui/tools/visual_whitepoint_editor.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
(see ``_visual_whitepoint_editor_measure_handler``), mirroring how wx's button
2323
called back into the parent's ``ambient_measure_handler``. Still deliberately
2424
dropped: the network **pattern-generator** patch output (tracked separately).
25-
The custom wx spinners/sliders/AUI docking are replaced by native Qt widgets.
25+
The custom wx spinners/sliders are replaced by native Qt widgets; wx's
26+
AUI-managed pin/float pane has no docking-framework equivalent under Qt, so
27+
:meth:`VisualWhitepointEditorWindow.float_panel`/``dock_panel`` instead detach
28+
the controls panel into a plain top-level :class:`_FloatingControlsWindow`
29+
and re-embed it, toggled via the header row's pin button.
2630
"""
2731

2832
from __future__ import annotations
@@ -795,6 +799,35 @@ def restore_display_profiles(
795799
thread.join()
796800

797801

802+
class _FloatingControlsWindow(QWidget):
803+
"""Floating window that hosts the detached controls panel.
804+
805+
Qt port of wx's AUI-floated ``mainPanel`` pane (see wx's
806+
``PinButton(True)`` / ``float_pane_handler``). There is no AUI-docking
807+
equivalent under Qt, so this is a plain top-level ``QWidget`` (no modal
808+
dialog semantics, e.g. no Escape-to-close) that clicking its native close
809+
button re-docks rather than destroys, mirroring wx's
810+
``close_pane_handler`` (which vetoes the close and docks the pane
811+
instead).
812+
813+
Args:
814+
editor (VisualWhitepointEditorWindow): The owning editor.
815+
"""
816+
817+
def __init__(self, editor: VisualWhitepointEditorWindow) -> None:
818+
super().__init__(editor, Qt.Tool)
819+
self._editor = editor
820+
821+
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt override)
822+
"""Dock the panel back into the editor instead of closing.
823+
824+
Args:
825+
event (QCloseEvent): The Qt close event.
826+
"""
827+
event.ignore()
828+
self._editor.dock_panel()
829+
830+
798831
class VisualWhitepointEditorWindow(BaseWindow):
799832
"""Standalone visual whitepoint editor window."""
800833

@@ -824,6 +857,7 @@ def __init__(self) -> None:
824857
self.display_size_mm: dict[tuple, list[float]] = {}
825858
self.default_size = 300.0
826859
self._pm: _ProfileManager | None = None
860+
self._float_window: _FloatingControlsWindow | None = None
827861

828862
cfg_x, cfg_y, cfg_scale = (
829863
float(v)
@@ -861,7 +895,16 @@ def _build_ui(self, cfg_x: float, cfg_y: float, cfg_scale: float) -> None:
861895
panel.setContentsMargins(12, 12, 12, 12)
862896
panel.setSpacing(10)
863897

864-
panel.addWidget(_section_label(lang.getstr("whitepoint")))
898+
header_row = QHBoxLayout()
899+
header_row.addWidget(_section_label(lang.getstr("whitepoint")))
900+
header_row.addStretch(1)
901+
self.pin_btn = _icon_button(
902+
"button-pin", lang.getstr("whitepoint.visual_editor.panel.float")
903+
)
904+
self.pin_btn.setCheckable(True)
905+
self.pin_btn.toggled.connect(self._on_pin_toggled)
906+
header_row.addWidget(self.pin_btn)
907+
panel.addLayout(header_row)
865908

866909
self.hsv_wheel = HSVWheel(self)
867910
self.bright_ctrl = BrightCtrl(self, self.colour)
@@ -947,6 +990,8 @@ def _build_ui(self, cfg_x: float, cfg_y: float, cfg_scale: float) -> None:
947990
panel.addStretch(1)
948991

949992
self.bg_area = _BackgroundArea(central)
993+
self.controls = controls
994+
self._root_layout = root
950995
root.addWidget(controls, 0)
951996
root.addWidget(self.bg_area, 1)
952997
self.setCentralWidget(central)
@@ -1093,6 +1138,51 @@ def _center_slider(slider: QSlider) -> None:
10931138
"""
10941139
slider.setValue(500)
10951140

1141+
# -- controls panel float/dock ------------------------------------------
1142+
1143+
def _on_pin_toggled(self, checked: bool) -> None:
1144+
"""Float or dock the controls panel in response to the pin button.
1145+
1146+
Args:
1147+
checked (bool): True to float the panel, False to dock it.
1148+
"""
1149+
if checked:
1150+
self.float_panel()
1151+
else:
1152+
self.dock_panel()
1153+
1154+
def float_panel(self) -> None:
1155+
"""Detach the controls panel into its own top-level window."""
1156+
if self._float_window is not None:
1157+
return
1158+
self._root_layout.removeWidget(self.controls)
1159+
float_window = _FloatingControlsWindow(self)
1160+
layout = QVBoxLayout(float_window)
1161+
layout.setContentsMargins(0, 0, 0, 0)
1162+
layout.addWidget(self.controls)
1163+
self.controls.show()
1164+
self._float_window = float_window
1165+
top_left = self.mapToGlobal(QPoint(0, 0))
1166+
float_window.move(top_left.x() + 10, top_left.y() + 10)
1167+
float_window.resize(self.controls.sizeHint())
1168+
float_window.show()
1169+
self.pin_btn.setToolTip(lang.getstr("whitepoint.visual_editor.panel.dock"))
1170+
1171+
def dock_panel(self) -> None:
1172+
"""Re-embed a floating controls panel back into the editor window."""
1173+
if self._float_window is None:
1174+
return
1175+
float_window = self._float_window
1176+
self._float_window = None
1177+
self.controls.setParent(None)
1178+
self._root_layout.insertWidget(0, self.controls, 0)
1179+
self.controls.show()
1180+
float_window.deleteLater()
1181+
self.pin_btn.blockSignals(True)
1182+
self.pin_btn.setChecked(False)
1183+
self.pin_btn.blockSignals(False)
1184+
self.pin_btn.setToolTip(lang.getstr("whitepoint.visual_editor.panel.float"))
1185+
10961186
def apply_initial_whitepoint(self, r: int, g: int, b: int) -> None:
10971187
"""Seed the editor from a display profile's ``vcgt`` whitepoint.
10981188
@@ -1197,6 +1287,7 @@ def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt override)
11971287
Args:
11981288
event (QCloseEvent): The Qt close event.
11991289
"""
1290+
self.dock_panel()
12001291
self._save_cfg()
12011292
if self._pm is not None:
12021293
self._pm.restore_display_profiles(wrapup=True, wait=True)

tests/test_ui_main_window.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,63 @@ def test_visual_whitepoint_editor_measure_btn_missing_argyll_reenables_button(
11151115
assert editor.measure_btn.isEnabled() is True
11161116

11171117

1118+
def test_visual_whitepoint_editor_pin_btn_floats_and_docks_controls_panel(window):
1119+
# Issue #901: wx's AUI-managed controls pane can be pinned/floated into
1120+
# its own draggable window; Qt has no AUI-docking equivalent, so the
1121+
# ported pin button instead detaches the panel into a plain top-level
1122+
# window and re-embeds it on toggle.
1123+
window._visual_whitepoint_editor_btn_handler()
1124+
editor = window._visual_whitepoint_editor_window
1125+
assert editor.controls.window() is editor
1126+
assert editor._float_window is None
1127+
1128+
editor.pin_btn.setChecked(True)
1129+
assert editor._float_window is not None
1130+
assert editor.controls.window() is editor._float_window
1131+
assert editor.pin_btn.toolTip() == lang.getstr(
1132+
"whitepoint.visual_editor.panel.dock"
1133+
)
1134+
1135+
editor.pin_btn.setChecked(False)
1136+
assert editor._float_window is None
1137+
assert editor.controls.window() is editor
1138+
assert editor.pin_btn.toolTip() == lang.getstr(
1139+
"whitepoint.visual_editor.panel.float"
1140+
)
1141+
1142+
1143+
def test_visual_whitepoint_editor_float_window_close_docks_instead_of_destroying(
1144+
window,
1145+
):
1146+
# wx's close_pane_handler vetoes the floating pane's close and docks it
1147+
# back instead of destroying it; the Qt port's _FloatingControlsWindow
1148+
# mirrors that via its own closeEvent override.
1149+
window._visual_whitepoint_editor_btn_handler()
1150+
editor = window._visual_whitepoint_editor_window
1151+
editor.float_panel()
1152+
float_window = editor._float_window
1153+
assert float_window is not None
1154+
1155+
float_window.close()
1156+
1157+
assert editor._float_window is None
1158+
assert editor.controls.window() is editor
1159+
assert editor.pin_btn.isChecked() is False
1160+
1161+
1162+
def test_visual_whitepoint_editor_close_docks_floating_panel(window):
1163+
window._visual_whitepoint_editor_btn_handler()
1164+
editor = window._visual_whitepoint_editor_window
1165+
editor.float_panel()
1166+
assert editor._float_window is not None
1167+
1168+
editor.close()
1169+
1170+
assert editor._float_window is None
1171+
# Re-open so later tests in this module still find a live singleton.
1172+
window._visual_whitepoint_editor_window = None
1173+
1174+
11181175
def test_visual_whitepoint_editor_measure_consumer_sets_colortemp_whitepoint(window):
11191176
window._visual_whitepoint_editor_btn_handler()
11201177
editor = window._visual_whitepoint_editor_window

0 commit comments

Comments
 (0)