mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-01-01 03:21:19 +01:00
feat(advanced_dock_area): floating docks restore with relative geometry
This commit is contained in:
@@ -1352,6 +1352,7 @@ class DockAreaWidget(RPCBase):
|
||||
floatable: "bool" = True,
|
||||
movable: "bool" = True,
|
||||
start_floating: "bool" = False,
|
||||
floating_state: "Mapping[str, object] | None" = None,
|
||||
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
|
||||
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
|
||||
tab_with: "CDockWidget | QWidget | str | None" = None,
|
||||
@@ -1374,6 +1375,7 @@ class DockAreaWidget(RPCBase):
|
||||
floatable(bool): Whether the dock is floatable.
|
||||
movable(bool): Whether the dock is movable.
|
||||
start_floating(bool): Whether to start the dock floating.
|
||||
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
|
||||
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
|
||||
``relative_to`` is provided without an explicit value).
|
||||
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
|
||||
@@ -2901,6 +2903,7 @@ class MonacoDock(RPCBase):
|
||||
floatable: "bool" = True,
|
||||
movable: "bool" = True,
|
||||
start_floating: "bool" = False,
|
||||
floating_state: "Mapping[str, object] | None" = None,
|
||||
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
|
||||
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
|
||||
tab_with: "CDockWidget | QWidget | str | None" = None,
|
||||
@@ -2923,6 +2926,7 @@ class MonacoDock(RPCBase):
|
||||
floatable(bool): Whether the dock is floatable.
|
||||
movable(bool): Whether the dock is movable.
|
||||
start_floating(bool): Whether to start the dock floating.
|
||||
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
|
||||
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
|
||||
``relative_to`` is provided without an explicit value).
|
||||
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
|
||||
|
||||
@@ -551,6 +551,13 @@ class AdvancedDockArea(DockAreaWidget):
|
||||
) or any(os.path.exists(path) for path in default_profile_candidates(name, namespace))
|
||||
|
||||
def _write_snapshot_to_settings(self, settings, save_preview: bool = True) -> None:
|
||||
"""
|
||||
Write the current workspace snapshot to the provided settings object.
|
||||
|
||||
Args:
|
||||
settings(QSettings): The settings object to write to.
|
||||
save_preview(bool): Whether to save a screenshot preview.
|
||||
"""
|
||||
self.save_to_settings(settings, keys=PROFILE_STATE_KEYS)
|
||||
self.state_manager.save_state(settings=settings)
|
||||
write_manifest(settings, self.dock_list())
|
||||
@@ -688,11 +695,20 @@ class AdvancedDockArea(DockAreaWidget):
|
||||
if obj_name not in self.widget_map():
|
||||
w = widget_handler.create_widget(widget_type=widget_class, parent=self)
|
||||
w.setObjectName(obj_name)
|
||||
floating_state = None
|
||||
if item.get("floating"):
|
||||
floating_state = {
|
||||
"relative": item.get("floating_relative"),
|
||||
"absolute": item.get("floating_absolute"),
|
||||
"screen_name": item.get("floating_screen"),
|
||||
}
|
||||
self._make_dock(
|
||||
w,
|
||||
closable=item["closable"],
|
||||
floatable=item["floatable"],
|
||||
movable=item["movable"],
|
||||
start_floating=item.get("floating", False),
|
||||
floating_state=floating_state,
|
||||
area=QtAds.DockWidgetArea.RightDockWidgetArea,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ import inspect
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Literal, Mapping, Sequence, cast
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QByteArray, QSettings, Qt, QTimer
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QDialog, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QApplication, QDialog, QVBoxLayout, QWidget
|
||||
from shiboken6 import isValid
|
||||
|
||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||
@@ -22,6 +23,8 @@ from bec_widgets.widgets.containers.qt_ads import (
|
||||
CDockWidget,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DockSettingsDialog(QDialog):
|
||||
"""Generic settings editor shown from dock title bar actions."""
|
||||
@@ -64,6 +67,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable: bool = True
|
||||
movable: bool = True
|
||||
start_floating: bool = False
|
||||
floating_state: Mapping[str, Any] | None = None
|
||||
area: QtAds.DockWidgetArea = QtAds.DockWidgetArea.RightDockWidgetArea
|
||||
on_close: Callable[[CDockWidget, QWidget], None] | None = None
|
||||
tab_with: CDockWidget | None = None
|
||||
@@ -258,6 +262,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
movable: bool = True,
|
||||
area: QtAds.DockWidgetArea = QtAds.DockWidgetArea.RightDockWidgetArea,
|
||||
start_floating: bool = False,
|
||||
floating_state: Mapping[str, object] | None = None,
|
||||
on_close: Callable[[CDockWidget, QWidget], None] | None = None,
|
||||
tab_with: CDockWidget | None = None,
|
||||
relative_to: CDockWidget | None = None,
|
||||
@@ -276,6 +281,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
movable(bool): Whether the dock can be moved.
|
||||
area(QtAds.DockWidgetArea): Target dock area.
|
||||
start_floating(bool): Whether the dock should start floating.
|
||||
floating_state(Mapping | None): Optional geometry metadata to apply when floating.
|
||||
on_close(Callable[[CDockWidget, QWidget], None] | None): Custom close handler.
|
||||
tab_with(CDockWidget | None): Optional dock to tab with.
|
||||
relative_to(CDockWidget | None): Optional dock to position relative to.
|
||||
@@ -336,6 +342,8 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
|
||||
if start_floating and tab_with is None and not promote_central:
|
||||
dock.setFloating()
|
||||
if floating_state:
|
||||
self._apply_floating_state_to_dock(dock, floating_state)
|
||||
if resolved_icon is not None:
|
||||
dock.setIcon(resolved_icon)
|
||||
return dock
|
||||
@@ -424,6 +432,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable: bool,
|
||||
movable: bool,
|
||||
start_floating: bool,
|
||||
floating_state: Mapping[str, object] | None,
|
||||
where: Literal["left", "right", "top", "bottom"] | None,
|
||||
on_close: Callable[[CDockWidget, QWidget], None] | None,
|
||||
tab_with: CDockWidget | QWidget | str | None,
|
||||
@@ -444,6 +453,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable(bool): Whether the dock can be floated.
|
||||
movable(bool): Whether the dock can be moved.
|
||||
start_floating(bool): Whether the dock should start floating.
|
||||
floating_state(Mapping | None): Optional floating geometry metadata.
|
||||
where(Literal["left", "right", "top", "bottom"] | None): Target dock area.
|
||||
on_close(Callable[[CDockWidget, QWidget], None] | None): Custom close handler.
|
||||
tab_with(CDockWidget | QWidget | str | None): Optional dock to tab with.
|
||||
@@ -489,6 +499,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable=floatable,
|
||||
movable=movable,
|
||||
start_floating=start_floating,
|
||||
floating_state=floating_state,
|
||||
area=target_area,
|
||||
on_close=on_close,
|
||||
tab_with=resolved_tab,
|
||||
@@ -517,6 +528,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
closable=spec.closable,
|
||||
floatable=spec.floatable,
|
||||
movable=spec.movable,
|
||||
floating_state=spec.floating_state,
|
||||
area=spec.area,
|
||||
start_floating=spec.start_floating,
|
||||
on_close=spec.on_close,
|
||||
@@ -824,6 +836,126 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
defaults[key] = value
|
||||
return defaults
|
||||
|
||||
def _select_screen_for_entry(
|
||||
self, entry: Mapping[str, object], container: QtAds.CFloatingDockContainer | None
|
||||
):
|
||||
"""
|
||||
Pick the best target screen for a saved floating container.
|
||||
|
||||
Args:
|
||||
entry(Mapping[str, object]): Floating window entry.
|
||||
container(QtAds.CFloatingDockContainer | None): Optional container instance.
|
||||
"""
|
||||
screens = QApplication.screens() or []
|
||||
try:
|
||||
name = entry.get("screen_name") or ""
|
||||
except Exception as exc:
|
||||
logger.warning(f"Invalid screen_name in floating window entry: {exc}")
|
||||
name = ""
|
||||
if name:
|
||||
for screen in screens:
|
||||
try:
|
||||
if screen.name() == name:
|
||||
return screen
|
||||
except Exception as exc:
|
||||
logger.warning(f"Error checking screen name '{name}': {exc}")
|
||||
continue
|
||||
if container is not None and hasattr(container, "screen"):
|
||||
screen = container.screen()
|
||||
if screen is not None:
|
||||
return screen
|
||||
return screens[0] if screens else None
|
||||
|
||||
def _apply_saved_floating_geometry(
|
||||
self, container: QtAds.CFloatingDockContainer, entry: Mapping[str, object]
|
||||
) -> None:
|
||||
"""
|
||||
Resize/move a floating container using saved geometry information.
|
||||
|
||||
Args:
|
||||
container(QtAds.CFloatingDockContainer): Target floating container.
|
||||
entry(Mapping[str, object]): Floating window entry.
|
||||
"""
|
||||
abs_geom = entry.get("absolute") if isinstance(entry, Mapping) else None
|
||||
if isinstance(abs_geom, Mapping):
|
||||
try:
|
||||
x = int(abs_geom.get("x"))
|
||||
y = int(abs_geom.get("y"))
|
||||
width = int(abs_geom.get("w"))
|
||||
height = int(abs_geom.get("h"))
|
||||
except Exception as exc:
|
||||
logger.warning(f"Invalid absolute geometry in floating window entry: {exc}")
|
||||
else:
|
||||
if width > 0 and height > 0:
|
||||
container.setGeometry(x, y, max(width, 50), max(height, 50))
|
||||
return
|
||||
|
||||
rel = entry.get("relative") if isinstance(entry, Mapping) else None
|
||||
if not isinstance(rel, Mapping):
|
||||
return
|
||||
try:
|
||||
x_ratio = float(rel.get("x"))
|
||||
y_ratio = float(rel.get("y"))
|
||||
w_ratio = float(rel.get("w"))
|
||||
h_ratio = float(rel.get("h"))
|
||||
except Exception as exc:
|
||||
logger.warning(f"Invalid relative geometry in floating window entry: {exc}")
|
||||
return
|
||||
|
||||
screen = self._select_screen_for_entry(entry, container)
|
||||
if screen is None:
|
||||
return
|
||||
geom = screen.availableGeometry()
|
||||
screen_w = geom.width()
|
||||
screen_h = geom.height()
|
||||
if screen_w <= 0 or screen_h <= 0:
|
||||
return
|
||||
|
||||
min_w = 120
|
||||
min_h = 80
|
||||
width = max(min_w, int(round(screen_w * max(w_ratio, 0.05))))
|
||||
height = max(min_h, int(round(screen_h * max(h_ratio, 0.05))))
|
||||
width = min(width, screen_w)
|
||||
height = min(height, screen_h)
|
||||
|
||||
x = geom.left() + int(round(screen_w * x_ratio))
|
||||
y = geom.top() + int(round(screen_h * y_ratio))
|
||||
x = max(geom.left(), min(x, geom.left() + screen_w - width))
|
||||
y = max(geom.top(), min(y, geom.top() + screen_h - height))
|
||||
|
||||
container.setGeometry(x, y, width, height)
|
||||
|
||||
def _apply_floating_state_to_dock(
|
||||
self, dock: CDockWidget, state: Mapping[str, object], *, attempt: int = 0
|
||||
) -> None:
|
||||
"""
|
||||
Apply saved floating geometry to a dock once its container exists.
|
||||
|
||||
Args:
|
||||
dock(CDockWidget): Target dock widget.
|
||||
state(Mapping[str, object]): Saved floating state.
|
||||
attempt(int): Current attempt count for retries.
|
||||
"""
|
||||
if state is None:
|
||||
return
|
||||
|
||||
def schedule(next_attempt: int):
|
||||
QTimer.singleShot(
|
||||
50, lambda: self._apply_floating_state_to_dock(dock, state, attempt=next_attempt)
|
||||
)
|
||||
|
||||
container = dock.floatingDockContainer()
|
||||
if container is None:
|
||||
if attempt < 10:
|
||||
schedule(attempt + 1)
|
||||
return
|
||||
entry = {
|
||||
"relative": state.get("relative") if isinstance(state, Mapping) else None,
|
||||
"absolute": state.get("absolute") if isinstance(state, Mapping) else None,
|
||||
"screen_name": state.get("screen_name") if isinstance(state, Mapping) else None,
|
||||
}
|
||||
self._apply_saved_floating_geometry(container, entry)
|
||||
|
||||
def save_to_settings(
|
||||
self,
|
||||
settings: QSettings,
|
||||
@@ -1083,6 +1215,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable: bool = True,
|
||||
movable: bool = True,
|
||||
start_floating: bool = False,
|
||||
floating_state: Mapping[str, object] | None = None,
|
||||
where: Literal["left", "right", "top", "bottom"] | None = None,
|
||||
on_close: Callable[[CDockWidget, QWidget], None] | None = None,
|
||||
tab_with: CDockWidget | QWidget | str | None = None,
|
||||
@@ -1105,6 +1238,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable(bool): Whether the dock is floatable.
|
||||
movable(bool): Whether the dock is movable.
|
||||
start_floating(bool): Whether to start the dock floating.
|
||||
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
|
||||
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
|
||||
``relative_to`` is provided without an explicit value).
|
||||
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
|
||||
@@ -1148,6 +1282,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable=floatable,
|
||||
movable=movable,
|
||||
start_floating=start_floating,
|
||||
floating_state=floating_state,
|
||||
where=where,
|
||||
on_close=on_close,
|
||||
tab_with=tab_with,
|
||||
@@ -1173,6 +1308,7 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
floatable=floatable,
|
||||
movable=movable,
|
||||
start_floating=start_floating,
|
||||
floating_state=floating_state,
|
||||
where=where,
|
||||
on_close=on_close,
|
||||
tab_with=tab_with,
|
||||
@@ -1187,13 +1323,29 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
dock = self._create_dock_from_spec(spec)
|
||||
return dock if return_dock else widget
|
||||
|
||||
def _iter_all_docks(self) -> list[CDockWidget]:
|
||||
"""Return all docks, including those hosted in floating containers."""
|
||||
docks = list(self.dock_manager.dockWidgets())
|
||||
seen = {id(d) for d in docks}
|
||||
for container in self.dock_manager.floatingWidgets():
|
||||
if container is None:
|
||||
continue
|
||||
for dock in container.dockWidgets():
|
||||
if dock is None:
|
||||
continue
|
||||
if id(dock) in seen:
|
||||
continue
|
||||
docks.append(dock)
|
||||
seen.add(id(dock))
|
||||
return docks
|
||||
|
||||
def dock_map(self) -> dict[str, CDockWidget]:
|
||||
"""Return the dock widgets map as dictionary with names as keys."""
|
||||
return self.dock_manager.dockWidgetsMap()
|
||||
return {dock.objectName(): dock for dock in self._iter_all_docks() if dock.objectName()}
|
||||
|
||||
def dock_list(self) -> list[CDockWidget]:
|
||||
"""Return the list of dock widgets."""
|
||||
return self.dock_manager.dockWidgets()
|
||||
return self._iter_all_docks()
|
||||
|
||||
def widget_map(self) -> dict[str, QWidget]:
|
||||
"""Return a dictionary mapping widget names to their corresponding widgets."""
|
||||
|
||||
@@ -22,6 +22,7 @@ from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import QByteArray, QDateTime, QSettings, Qt
|
||||
from qtpy.QtGui import QPixmap
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.widgets.containers.qt_ads import CDockWidget
|
||||
|
||||
@@ -655,8 +656,44 @@ def write_manifest(settings: QSettings, docks: list[CDockWidget]) -> None:
|
||||
settings(QSettings): Settings object to write to.
|
||||
docks(list[CDockWidget]): List of dock widgets to serialize.
|
||||
"""
|
||||
settings.beginWriteArray(SETTINGS_KEYS["manifest"], len(docks))
|
||||
for i, dock in enumerate(docks):
|
||||
|
||||
def _floating_snapshot(dock: CDockWidget) -> dict | None:
|
||||
if not hasattr(dock, "isFloating") or not dock.isFloating():
|
||||
return None
|
||||
container = dock.floatingDockContainer() if hasattr(dock, "floatingDockContainer") else None
|
||||
if container is None:
|
||||
return None
|
||||
geom = container.frameGeometry()
|
||||
if geom.isNull():
|
||||
return None
|
||||
absolute = {"x": geom.x(), "y": geom.y(), "w": geom.width(), "h": geom.height()}
|
||||
screen = container.screen() if hasattr(container, "screen") else None
|
||||
if screen is None:
|
||||
screen = QApplication.screenAt(geom.center()) if QApplication.instance() else None
|
||||
screen_name = ""
|
||||
relative = None
|
||||
if screen is not None:
|
||||
if hasattr(screen, "name"):
|
||||
try:
|
||||
screen_name = screen.name()
|
||||
except Exception:
|
||||
screen_name = ""
|
||||
avail = screen.availableGeometry()
|
||||
width = max(1, avail.width())
|
||||
height = max(1, avail.height())
|
||||
relative = {
|
||||
"x": (geom.left() - avail.left()) / float(width),
|
||||
"y": (geom.top() - avail.top()) / float(height),
|
||||
"w": geom.width() / float(width),
|
||||
"h": geom.height() / float(height),
|
||||
}
|
||||
return {"screen_name": screen_name, "relative": relative, "absolute": absolute}
|
||||
|
||||
ordered_docks = [dock for dock in docks if dock.isFloating()] + [
|
||||
dock for dock in docks if not dock.isFloating()
|
||||
]
|
||||
settings.beginWriteArray(SETTINGS_KEYS["manifest"], len(ordered_docks))
|
||||
for i, dock in enumerate(ordered_docks):
|
||||
settings.setArrayIndex(i)
|
||||
w = dock.widget()
|
||||
settings.setValue("object_name", w.objectName())
|
||||
@@ -664,6 +701,32 @@ def write_manifest(settings: QSettings, docks: list[CDockWidget]) -> None:
|
||||
settings.setValue("closable", getattr(dock, "_default_closable", True))
|
||||
settings.setValue("floatable", getattr(dock, "_default_floatable", True))
|
||||
settings.setValue("movable", getattr(dock, "_default_movable", True))
|
||||
is_floating = bool(dock.isFloating())
|
||||
settings.setValue("floating", is_floating)
|
||||
if is_floating:
|
||||
snapshot = _floating_snapshot(dock)
|
||||
if snapshot:
|
||||
relative = snapshot.get("relative") or {}
|
||||
absolute = snapshot.get("absolute") or {}
|
||||
settings.setValue("floating_screen", snapshot.get("screen_name", ""))
|
||||
settings.setValue("floating_rel_x", relative.get("x", 0.0))
|
||||
settings.setValue("floating_rel_y", relative.get("y", 0.0))
|
||||
settings.setValue("floating_rel_w", relative.get("w", 0.0))
|
||||
settings.setValue("floating_rel_h", relative.get("h", 0.0))
|
||||
settings.setValue("floating_abs_x", absolute.get("x", 0))
|
||||
settings.setValue("floating_abs_y", absolute.get("y", 0))
|
||||
settings.setValue("floating_abs_w", absolute.get("w", 0))
|
||||
settings.setValue("floating_abs_h", absolute.get("h", 0))
|
||||
else:
|
||||
settings.setValue("floating_screen", "")
|
||||
settings.setValue("floating_rel_x", 0.0)
|
||||
settings.setValue("floating_rel_y", 0.0)
|
||||
settings.setValue("floating_rel_w", 0.0)
|
||||
settings.setValue("floating_rel_h", 0.0)
|
||||
settings.setValue("floating_abs_x", 0)
|
||||
settings.setValue("floating_abs_y", 0)
|
||||
settings.setValue("floating_abs_w", 0)
|
||||
settings.setValue("floating_abs_h", 0)
|
||||
settings.endArray()
|
||||
|
||||
|
||||
@@ -681,6 +744,22 @@ def read_manifest(settings: QSettings) -> list[dict]:
|
||||
count = settings.beginReadArray(SETTINGS_KEYS["manifest"])
|
||||
for i in range(count):
|
||||
settings.setArrayIndex(i)
|
||||
floating = settings.value("floating", False, type=bool)
|
||||
rel = {
|
||||
"x": float(settings.value("floating_rel_x", 0.0)),
|
||||
"y": float(settings.value("floating_rel_y", 0.0)),
|
||||
"w": float(settings.value("floating_rel_w", 0.0)),
|
||||
"h": float(settings.value("floating_rel_h", 0.0)),
|
||||
}
|
||||
abs_geom = {
|
||||
"x": int(settings.value("floating_abs_x", 0)),
|
||||
"y": int(settings.value("floating_abs_y", 0)),
|
||||
"w": int(settings.value("floating_abs_w", 0)),
|
||||
"h": int(settings.value("floating_abs_h", 0)),
|
||||
}
|
||||
if not floating:
|
||||
rel = None
|
||||
abs_geom = None
|
||||
items.append(
|
||||
{
|
||||
"object_name": settings.value("object_name"),
|
||||
@@ -688,6 +767,10 @@ def read_manifest(settings: QSettings) -> list[dict]:
|
||||
"closable": settings.value("closable", type=bool),
|
||||
"floatable": settings.value("floatable", type=bool),
|
||||
"movable": settings.value("movable", type=bool),
|
||||
"floating": floating,
|
||||
"floating_screen": settings.value("floating_screen", ""),
|
||||
"floating_relative": rel,
|
||||
"floating_absolute": abs_geom,
|
||||
}
|
||||
)
|
||||
settings.endArray()
|
||||
|
||||
@@ -21,6 +21,7 @@ from bec_widgets.widgets.containers.advanced_dock_area.basic_dock_area import (
|
||||
DockSettingsDialog,
|
||||
)
|
||||
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
|
||||
SETTINGS_KEYS,
|
||||
default_profile_path,
|
||||
get_profile_info,
|
||||
is_profile_read_only,
|
||||
@@ -249,6 +250,31 @@ class TestBasicDockArea:
|
||||
basic_dock_area.delete_all()
|
||||
assert basic_dock_area.dock_list() == []
|
||||
|
||||
def test_manifest_serialization_includes_floating_geometry(
|
||||
self, basic_dock_area, qtbot, tmp_path
|
||||
):
|
||||
anchored = QWidget(parent=basic_dock_area)
|
||||
anchored.setObjectName("anchored_widget")
|
||||
floating = QWidget(parent=basic_dock_area)
|
||||
floating.setObjectName("floating_widget")
|
||||
|
||||
basic_dock_area.new(anchored, return_dock=True)
|
||||
dock_floating = basic_dock_area.new(floating, return_dock=True, start_floating=True)
|
||||
qtbot.waitUntil(lambda: dock_floating.isFloating(), timeout=2000)
|
||||
|
||||
settings_path = tmp_path / "manifest.ini"
|
||||
settings = QSettings(str(settings_path), QSettings.IniFormat)
|
||||
write_manifest(settings, basic_dock_area.dock_list())
|
||||
settings.sync()
|
||||
|
||||
manifest_entries = read_manifest(settings)
|
||||
assert len(manifest_entries) == 2
|
||||
assert manifest_entries[0]["object_name"] == "floating_widget"
|
||||
assert manifest_entries[0]["floating"] is True
|
||||
assert manifest_entries[0]["floating_relative"] is not None
|
||||
assert manifest_entries[1]["object_name"] == "anchored_widget"
|
||||
assert manifest_entries[1]["floating"] is False
|
||||
|
||||
def test_splitter_weight_coercion_supports_aliases(self, basic_dock_area):
|
||||
weights = {"default": 0.5, "left": 2, "center": 3, "right": 4}
|
||||
|
||||
@@ -837,6 +863,59 @@ class TestToolbarFunctionality:
|
||||
final_floating = len(advanced_dock_area.dock_manager.floatingWidgets())
|
||||
assert final_floating <= initial_floating
|
||||
|
||||
def test_load_profile_restores_floating_dock(self, advanced_dock_area, qtbot):
|
||||
helper = profile_helper(advanced_dock_area)
|
||||
settings = helper.open_user("floating_profile")
|
||||
settings.clear()
|
||||
|
||||
settings.setValue("profile/created_at", "2025-11-23T00:00:00Z")
|
||||
settings.beginWriteArray(SETTINGS_KEYS["manifest"], 2)
|
||||
|
||||
# Floating entry
|
||||
settings.setArrayIndex(0)
|
||||
settings.setValue("object_name", "FloatingWaveform")
|
||||
settings.setValue("widget_class", "DarkModeButton")
|
||||
settings.setValue("closable", True)
|
||||
settings.setValue("floatable", True)
|
||||
settings.setValue("movable", True)
|
||||
settings.setValue("floating", True)
|
||||
settings.setValue("floating_screen", "")
|
||||
settings.setValue("floating_rel_x", 0.1)
|
||||
settings.setValue("floating_rel_y", 0.1)
|
||||
settings.setValue("floating_rel_w", 0.2)
|
||||
settings.setValue("floating_rel_h", 0.2)
|
||||
settings.setValue("floating_abs_x", 50)
|
||||
settings.setValue("floating_abs_y", 50)
|
||||
settings.setValue("floating_abs_w", 200)
|
||||
settings.setValue("floating_abs_h", 150)
|
||||
|
||||
# Anchored entry
|
||||
settings.setArrayIndex(1)
|
||||
settings.setValue("object_name", "EmbeddedWaveform")
|
||||
settings.setValue("widget_class", "DarkModeButton")
|
||||
settings.setValue("closable", True)
|
||||
settings.setValue("floatable", True)
|
||||
settings.setValue("movable", True)
|
||||
settings.setValue("floating", False)
|
||||
settings.setValue("floating_screen", "")
|
||||
settings.setValue("floating_rel_x", 0.0)
|
||||
settings.setValue("floating_rel_y", 0.0)
|
||||
settings.setValue("floating_rel_w", 0.0)
|
||||
settings.setValue("floating_rel_h", 0.0)
|
||||
settings.setValue("floating_abs_x", 0)
|
||||
settings.setValue("floating_abs_y", 0)
|
||||
settings.setValue("floating_abs_w", 0)
|
||||
settings.setValue("floating_abs_h", 0)
|
||||
settings.endArray()
|
||||
settings.sync()
|
||||
|
||||
advanced_dock_area.delete_all()
|
||||
advanced_dock_area.load_profile("floating_profile")
|
||||
|
||||
qtbot.waitUntil(lambda: "FloatingWaveform" in advanced_dock_area.dock_map(), timeout=3000)
|
||||
floating_dock = advanced_dock_area.dock_map()["FloatingWaveform"]
|
||||
assert floating_dock.isFloating()
|
||||
|
||||
def test_screenshot_action(self, advanced_dock_area, tmpdir):
|
||||
"""Test screenshot toolbar action."""
|
||||
# Create a test screenshot file path in tmpdir
|
||||
|
||||
Reference in New Issue
Block a user