1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-01-01 19:41:18 +01:00

refactor(advanced_dock_area): profile tools moved to separate module

This commit is contained in:
2025-08-19 11:20:48 +02:00
committed by Jan Wyzula
parent 66fb0a8816
commit a27f66bbef
3 changed files with 100 additions and 106 deletions

View File

@@ -5,7 +5,7 @@ from typing import Literal, cast
import PySide6QtAds as QtAds
from PySide6QtAds import CDockManager, CDockWidget
from qtpy.QtCore import QSettings, Signal
from qtpy.QtCore import Signal
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
@@ -34,6 +34,16 @@ from bec_widgets.utils.toolbars.actions import (
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.widget_state_manager import WidgetStateManager
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
SETTINGS_KEYS,
is_profile_readonly,
list_profiles,
open_settings,
profile_path,
read_manifest,
set_profile_readonly,
write_manifest,
)
from bec_widgets.widgets.containers.advanced_dock_area.toolbar_components.workspace_actions import (
WorkspaceConnection,
workspace_bundle,
@@ -54,81 +64,6 @@ from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatus
from bec_widgets.widgets.utility.logpanel import LogPanel
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
_DEFAULT_PROFILES_DIR = os.path.join(os.path.dirname(__file__), "states", "default")
_USER_PROFILES_DIR = os.path.join(os.path.dirname(__file__), "states", "user")
def _profiles_dir() -> str:
path = os.environ.get("BECWIDGETS_PROFILE_DIR", _USER_PROFILES_DIR)
os.makedirs(path, exist_ok=True)
return path
def _profile_path(name: str) -> str:
return os.path.join(_profiles_dir(), f"{name}.ini")
SETTINGS_KEYS = {
"geom": "mainWindow/Geometry",
"state": "mainWindow/State",
"ads_state": "mainWindow/DockingState",
"manifest": "manifest/widgets",
"readonly": "profile/readonly",
}
def list_profiles() -> list[str]:
return sorted(os.path.splitext(f)[0] for f in os.listdir(_profiles_dir()) if f.endswith(".ini"))
def is_profile_readonly(name: str) -> bool:
"""Check if a profile is marked as read-only."""
settings = open_settings(name)
return settings.value(SETTINGS_KEYS["readonly"], False, type=bool)
def set_profile_readonly(name: str, readonly: bool) -> None:
"""Set the read-only status of a profile."""
settings = open_settings(name)
settings.setValue(SETTINGS_KEYS["readonly"], readonly)
settings.sync()
def open_settings(name: str) -> QSettings:
return QSettings(_profile_path(name), QSettings.IniFormat)
def write_manifest(settings: QSettings, docks: list[CDockWidget]) -> None:
settings.beginWriteArray(SETTINGS_KEYS["manifest"], len(docks))
for i, dock in enumerate(docks):
settings.setArrayIndex(i)
w = dock.widget()
settings.setValue("object_name", w.objectName())
settings.setValue("widget_class", w.__class__.__name__)
settings.setValue("closable", getattr(dock, "_default_closable", True))
settings.setValue("floatable", getattr(dock, "_default_floatable", True))
settings.setValue("movable", getattr(dock, "_default_movable", True))
settings.endArray()
def read_manifest(settings: QSettings) -> list[dict]:
items: list[dict] = []
count = settings.beginReadArray(SETTINGS_KEYS["manifest"])
for i in range(count):
settings.setArrayIndex(i)
items.append(
{
"object_name": settings.value("object_name"),
"widget_class": settings.value("widget_class"),
"closable": settings.value("closable", type=bool),
"floatable": settings.value("floatable", type=bool),
"movable": settings.value("movable", type=bool),
}
)
settings.endArray()
return items
class DockSettingsDialog(QDialog):
@@ -751,7 +686,7 @@ class AdvancedDockArea(BECWidget, QWidget):
readonly = dialog.is_readonly()
# Check if profile already exists and is read-only
if os.path.exists(_profile_path(name)) and is_profile_readonly(name):
if os.path.exists(profile_path(name)) and is_profile_readonly(name):
suggested_name = f"{name}_custom"
reply = QMessageBox.warning(
self,
@@ -771,14 +706,14 @@ class AdvancedDockArea(BECWidget, QWidget):
readonly = dialog.is_readonly()
# Check again if the new name is also read-only (recursive protection)
if os.path.exists(_profile_path(name)) and is_profile_readonly(name):
if os.path.exists(profile_path(name)) and is_profile_readonly(name):
return self.save_profile()
else:
return
else:
# If name is provided directly, assume not read-only unless already exists
readonly = False
if os.path.exists(_profile_path(name)) and is_profile_readonly(name):
if os.path.exists(profile_path(name)) and is_profile_readonly(name):
QMessageBox.warning(
self,
"Read-only Profile",
@@ -889,7 +824,7 @@ class AdvancedDockArea(BECWidget, QWidget):
if reply != QMessageBox.Yes:
return
file_path = _profile_path(name)
file_path = profile_path(name)
try:
os.remove(file_path)
except FileNotFoundError:

View File

@@ -0,0 +1,79 @@
import os
from PySide6QtAds import CDockWidget
from qtpy.QtCore import QSettings
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
_DEFAULT_PROFILES_DIR = os.path.join(os.path.dirname(__file__), "states", "default")
_USER_PROFILES_DIR = os.path.join(os.path.dirname(__file__), "states", "user")
def profiles_dir() -> str:
path = os.environ.get("BECWIDGETS_PROFILE_DIR", _USER_PROFILES_DIR)
os.makedirs(path, exist_ok=True)
return path
def profile_path(name: str) -> str:
return os.path.join(profiles_dir(), f"{name}.ini")
SETTINGS_KEYS = {
"geom": "mainWindow/Geometry",
"state": "mainWindow/State",
"ads_state": "mainWindow/DockingState",
"manifest": "manifest/widgets",
"readonly": "profile/readonly",
}
def list_profiles() -> list[str]:
return sorted(os.path.splitext(f)[0] for f in os.listdir(profiles_dir()) if f.endswith(".ini"))
def is_profile_readonly(name: str) -> bool:
"""Check if a profile is marked as read-only."""
settings = open_settings(name)
return settings.value(SETTINGS_KEYS["readonly"], False, type=bool)
def set_profile_readonly(name: str, readonly: bool) -> None:
"""Set the read-only status of a profile."""
settings = open_settings(name)
settings.setValue(SETTINGS_KEYS["readonly"], readonly)
settings.sync()
def open_settings(name: str) -> QSettings:
return QSettings(profile_path(name), QSettings.IniFormat)
def write_manifest(settings: QSettings, docks: list[CDockWidget]) -> None:
settings.beginWriteArray(SETTINGS_KEYS["manifest"], len(docks))
for i, dock in enumerate(docks):
settings.setArrayIndex(i)
w = dock.widget()
settings.setValue("object_name", w.objectName())
settings.setValue("widget_class", w.__class__.__name__)
settings.setValue("closable", getattr(dock, "_default_closable", True))
settings.setValue("floatable", getattr(dock, "_default_floatable", True))
settings.setValue("movable", getattr(dock, "_default_movable", True))
settings.endArray()
def read_manifest(settings: QSettings) -> list[dict]:
items: list[dict] = []
count = settings.beginReadArray(SETTINGS_KEYS["manifest"])
for i in range(count):
settings.setArrayIndex(i)
items.append(
{
"object_name": settings.value("object_name"),
"widget_class": settings.value("widget_class"),
"closable": settings.value("closable", type=bool),
"floatable": settings.value("floatable", type=bool),
"movable": settings.value("movable", type=bool),
}
)
settings.endArray()
return items

View File

@@ -13,11 +13,12 @@ from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import
AdvancedDockArea,
DockSettingsDialog,
SaveProfileDialog,
_profile_path,
_profiles_dir,
)
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
is_profile_readonly,
list_profiles,
open_settings,
profile_path,
read_manifest,
set_profile_readonly,
write_manifest,
@@ -457,15 +458,9 @@ class TestSaveProfileDialog:
class TestProfileManagement:
"""Test profile management functionality."""
def test_profiles_dir_creation(self, temp_profile_dir):
"""Test that profiles directory is created."""
profiles_dir = _profiles_dir()
assert os.path.exists(profiles_dir)
assert profiles_dir == temp_profile_dir
def test_profile_path(self, temp_profile_dir):
"""Test profile path generation."""
path = _profile_path("test_profile")
path = profile_path("test_profile")
expected = os.path.join(temp_profile_dir, "test_profile.ini")
assert path == expected
@@ -539,21 +534,6 @@ class TestProfileManagement:
class TestWorkspaceProfileOperations:
"""Test workspace profile save/load/delete operations."""
def test_save_profile_with_name(self, advanced_dock_area, temp_profile_dir, qtbot):
"""Test saving profile with provided name."""
profile_name = "test_save_profile"
# Create some docks
advanced_dock_area.new("DarkModeButton")
qtbot.wait(200)
# Save profile
advanced_dock_area.save_profile(profile_name)
# Check that profile file was created
profile_path = _profile_path(profile_name)
assert os.path.exists(profile_path)
def test_save_profile_readonly_conflict(self, advanced_dock_area, temp_profile_dir):
"""Test saving profile when read-only profile exists."""
profile_name = "readonly_profile"
@@ -632,7 +612,7 @@ class TestWorkspaceProfileOperations:
mock_warning.assert_called_once()
# Profile should still exist
assert os.path.exists(_profile_path(profile_name))
assert os.path.exists(profile_path(profile_name))
def test_delete_profile_success(self, advanced_dock_area, temp_profile_dir):
"""Test successful profile deletion."""
@@ -659,7 +639,7 @@ class TestWorkspaceProfileOperations:
mock_question.assert_called_once()
mock_refresh.assert_called_once()
# Profile should be deleted
assert not os.path.exists(_profile_path(profile_name))
assert not os.path.exists(profile_path(profile_name))
def test_refresh_workspace_list(self, advanced_dock_area, temp_profile_dir):
"""Test refreshing workspace list."""