1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-05 00:12:49 +01:00

feat(advanced_dock_area): instance lock for multiple ads in same session

This commit is contained in:
2025-11-20 11:32:27 +01:00
committed by Klaus Wakonig
parent 0f392efdce
commit fd2b8918f5
3 changed files with 97 additions and 32 deletions

View File

@@ -92,7 +92,7 @@ class DeveloperWidget(DockAreaWidget):
self.terminal = WebConsole(self, startup_cmd="")
self.terminal.setObjectName("Terminal")
self.monaco = MonacoDock(self)
self.monaco.setObjectName("Monaco Editor")
self.monaco.setObjectName("MonacoEditor")
self.monaco.save_enabled.connect(self._on_save_enabled_update)
self.plotting_ads = AdvancedDockArea(
self,
@@ -103,7 +103,7 @@ class DeveloperWidget(DockAreaWidget):
enable_profile_management=False,
variant="compact",
)
self.plotting_ads.setObjectName("Plotting Area")
self.plotting_ads.setObjectName("PlottingArea")
self.signature_help = QTextEdit(self)
self.signature_help.setObjectName("Signature Help")
self.signature_help.setAcceptRichText(True)

View File

@@ -118,6 +118,7 @@ class AdvancedDockArea(DockAreaWidget):
default_add_direction: Literal["left", "right", "top", "bottom"] = "right",
profile_namespace: str | None = None,
auto_profile_namespace: bool = True,
instance_id: str | None = None,
auto_save_upon_exit: bool = True,
enable_profile_management: bool = True,
restore_initial_profile: bool = True,
@@ -126,6 +127,7 @@ class AdvancedDockArea(DockAreaWidget):
self._profile_namespace_hint = profile_namespace
self._profile_namespace_auto = auto_profile_namespace
self._profile_namespace_resolved: str | None | object = _PROFILE_NAMESPACE_UNSET
self._instance_id = sanitize_namespace(instance_id) if instance_id else None
self._auto_save_upon_exit = auto_save_upon_exit
self._profile_management_enabled = enable_profile_management
self._restore_initial_profile = restore_initial_profile
@@ -181,24 +183,23 @@ class AdvancedDockArea(DockAreaWidget):
# Restore last-used profile if available; otherwise fall back to combo selection
combo = self.toolbar.components.get_action("workspace_combo").widget
namespace = self.profile_namespace
last = get_last_profile(namespace)
if last:
user_exists = any(
os.path.exists(path) for path in user_profile_candidates(last, namespace)
init_profile = None
instance_id = self._last_profile_instance_id()
if instance_id:
inst_profile = get_last_profile(
namespace=namespace, instance=instance_id, allow_namespace_fallback=False
)
default_exists = any(
os.path.exists(path) for path in default_profile_candidates(last, namespace)
)
init_profile = last if (user_exists or default_exists) else None
else:
init_profile = combo.currentText()
if inst_profile and self._profile_exists(inst_profile, namespace):
init_profile = inst_profile
if not init_profile:
general_exists = any(
os.path.exists(path) for path in user_profile_candidates("general", namespace)
) or any(
os.path.exists(path) for path in default_profile_candidates("general", namespace)
)
if general_exists:
last = get_last_profile(namespace=namespace)
if last and self._profile_exists(last, namespace):
init_profile = last
else:
text = combo.currentText()
init_profile = text if text else None
if not init_profile:
if self._profile_exists("general", namespace):
init_profile = "general"
if init_profile:
# Defer initial load to the event loop so child widgets exist before state restore.
@@ -500,6 +501,14 @@ class AdvancedDockArea(DockAreaWidget):
for dock in self.dock_list():
dock.setting_action.setVisible(not value)
def _last_profile_instance_id(self) -> str | None:
"""
Identifier used to scope the last-profile entry for this dock area.
When unset, profiles are scoped only by namespace.
"""
return self._instance_id
def _resolve_profile_namespace(self) -> str | None:
if self._profile_namespace_resolved is not _PROFILE_NAMESPACE_UNSET:
return self._profile_namespace_resolved # type: ignore[return-value]
@@ -536,6 +545,11 @@ class AdvancedDockArea(DockAreaWidget):
self._current_profile_name = name
return name
def _profile_exists(self, name: str, namespace: str | None) -> bool:
return any(
os.path.exists(path) for path in user_profile_candidates(name, namespace)
) 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:
self.save_to_settings(settings, keys=PROFILE_STATE_KEYS)
self.state_manager.save_state(settings=settings)
@@ -630,7 +644,7 @@ class AdvancedDockArea(DockAreaWidget):
workspace_combo.setCurrentText(name)
self._current_profile_name = name
self.profile_changed.emit(name)
set_last_profile(name, namespace=namespace)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
combo = self.toolbar.components.get_action("workspace_combo").widget
combo.refresh_profiles(active_profile=name)
@@ -688,7 +702,7 @@ class AdvancedDockArea(DockAreaWidget):
self._current_profile_name = name
self.profile_changed.emit(name)
set_last_profile(name, namespace=namespace)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
combo = self.toolbar.components.get_action("workspace_combo").widget
combo.refresh_profiles(active_profile=name)
@@ -889,7 +903,7 @@ class AdvancedDockArea(DockAreaWidget):
namespace = self.profile_namespace
settings = open_user_settings(name, namespace=namespace)
self._write_snapshot_to_settings(settings)
set_last_profile(name, namespace=namespace)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
self._exit_snapshot_written = True
def cleanup(self):
@@ -910,13 +924,31 @@ class AdvancedDockArea(DockAreaWidget):
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QTabWidget
app = QApplication(sys.argv)
apply_theme("dark")
dispatcher = BECDispatcher(gui_id="ads")
window = BECMainWindowNoRPC()
ads = AdvancedDockArea(mode="creator", root_widget=True, enable_profile_management=True)
window.setCentralWidget(ads)
central = QWidget()
layout = QVBoxLayout(central)
window.setCentralWidget(central)
# two dock areas stacked vertically no instance ids
ads = AdvancedDockArea(mode="creator", enable_profile_management=True)
ads2 = AdvancedDockArea(mode="creator", enable_profile_management=True)
layout.addWidget(ads, 1)
layout.addWidget(ads2, 1)
# two dock areas inside a tab widget
tabs = QTabWidget(parent=central)
ads3 = AdvancedDockArea(mode="creator", enable_profile_management=True, instance_id="AdsTab3")
ads4 = AdvancedDockArea(mode="creator", enable_profile_management=True, instance_id="AdsTab4")
tabs.addTab(ads3, "Workspace 3")
tabs.addTab(ads4, "Workspace 4")
layout.addWidget(tabs, 1)
window.show()
window.resize(800, 600)
window.resize(800, 1000)
sys.exit(app.exec())

View File

@@ -559,9 +559,10 @@ def _app_settings() -> QSettings:
return QSettings(os.path.join(_settings_profiles_root(), "_meta.ini"), QSettings.IniFormat)
def _last_profile_key(namespace: str | None) -> str:
def _last_profile_key(namespace: str | None, instance: str | None = None) -> str:
"""
Build the QSettings key used to store the last profile per namespace.
Build the QSettings key used to store the last profile per namespace and
optional instance id.
Args:
namespace (str | None): Namespace label.
@@ -571,37 +572,69 @@ def _last_profile_key(namespace: str | None) -> str:
"""
ns = sanitize_namespace(namespace)
key = SETTINGS_KEYS["last_profile"]
return f"{key}/{ns}" if ns else key
if ns:
key = f"{key}/{ns}"
inst = sanitize_namespace(instance) if instance else ""
if inst:
key = f"{key}@{inst}"
return key
def get_last_profile(namespace: str | None = None) -> str | None:
def get_last_profile(
namespace: str | None = None,
instance: str | None = None,
*,
allow_namespace_fallback: bool = True,
) -> str | None:
"""
Retrieve the last-used profile name persisted in app settings.
When *instance* is provided, the lookup is scoped to that particular dock
area instance. If the instance-specific entry is missing and
``allow_namespace_fallback`` is True, the namespace-wide entry is
consulted next.
Args:
namespace (str | None, optional): Namespace label. Defaults to ``None``.
instance (str | None, optional): Optional instance ID. Defaults to ``None``.
allow_namespace_fallback (bool): Whether to fall back to the namespace
entry when an instance-specific value is not found. Defaults to ``True``.
Returns:
str | None: Profile name or ``None`` if none has been stored.
"""
s = _app_settings()
name = s.value(_last_profile_key(namespace), "", type=str)
inst = instance or None
if inst:
name = s.value(_last_profile_key(namespace, inst), "", type=str)
if name:
return name
if not allow_namespace_fallback:
return None
name = s.value(_last_profile_key(namespace, None), "", type=str)
return name or None
def set_last_profile(name: str | None, namespace: str | None = None) -> None:
def set_last_profile(
name: str | None, namespace: str | None = None, instance: str | None = None
) -> None:
"""
Persist the last-used profile name (or clear the value when ``None``).
When *instance* is provided, the value is stored under a key specific to
that dock area instance; otherwise it is stored under the namespace-wide key.
Args:
name (str | None): Profile name to store.
namespace (str | None, optional): Namespace label. Defaults to ``None``.
instance (str | None, optional): Optional instance ID. Defaults to ``None``.
"""
s = _app_settings()
key = _last_profile_key(namespace, instance)
if name:
s.setValue(_last_profile_key(namespace), name)
s.setValue(key, name)
else:
s.remove(_last_profile_key(namespace))
s.remove(key)
def now_iso_utc() -> str: