mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-09 16:22:08 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0695716e5 |
@@ -1,7 +1,4 @@
|
|||||||
##########################
|
#!/usr/bin/env python3
|
||||||
### AI-generated file. ###
|
|
||||||
##########################
|
|
||||||
|
|
||||||
"""Aggregate and merge benchmark JSON files.
|
"""Aggregate and merge benchmark JSON files.
|
||||||
|
|
||||||
The workflow runs the same benchmark suite on multiple independent runners.
|
The workflow runs the same benchmark suite on multiple independent runners.
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
##########################
|
#!/usr/bin/env python3
|
||||||
### AI-generated file. ###
|
|
||||||
##########################
|
|
||||||
|
|
||||||
"""Compare benchmark JSON files and write a GitHub Actions summary.
|
"""Compare benchmark JSON files and write a GitHub Actions summary.
|
||||||
|
|
||||||
The script supports JSON emitted by hyperfine, JSON emitted by pytest-benchmark,
|
The script supports JSON emitted by hyperfine, JSON emitted by pytest-benchmark,
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
##########################
|
|
||||||
### AI-generated file. ###
|
|
||||||
##########################
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p benchmark-results
|
mkdir -p benchmark-results
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
##########################
|
#!/usr/bin/env python3
|
||||||
### AI-generated file. ###
|
|
||||||
##########################
|
|
||||||
|
|
||||||
"""Run a command with BEC e2e services available."""
|
"""Run a command with BEC e2e services available."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: BW Benchmarks
|
name: BW Benchmarks
|
||||||
|
|
||||||
on: [ workflow_call ]
|
on: [workflow_call]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -10,7 +10,7 @@ env:
|
|||||||
BENCHMARK_BASELINE_JSON: gh-pages-benchmark-data/benchmarks/latest.json
|
BENCHMARK_BASELINE_JSON: gh-pages-benchmark-data/benchmarks/latest.json
|
||||||
BENCHMARK_SUMMARY: benchmark-results/summary.md
|
BENCHMARK_SUMMARY: benchmark-results/summary.md
|
||||||
BENCHMARK_COMMAND: "bash .github/scripts/run_benchmarks.sh"
|
BENCHMARK_COMMAND: "bash .github/scripts/run_benchmarks.sh"
|
||||||
BENCHMARK_THRESHOLD_PERCENT: 20
|
BENCHMARK_THRESHOLD_PERCENT: 10
|
||||||
BENCHMARK_HIGHER_IS_BETTER: false
|
BENCHMARK_HIGHER_IS_BETTER: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
attempt: [ 1, 2, 3 ]
|
attempt: [1, 2, 3]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BENCHMARK_JSON: benchmark-results/current-${{ matrix.attempt }}.json
|
BENCHMARK_JSON: benchmark-results/current-${{ matrix.attempt }}.json
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
path: ${{ env.BENCHMARK_JSON }}
|
path: ${{ env.BENCHMARK_JSON }}
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
needs: [ benchmark_attempt ]
|
needs: [benchmark_attempt]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: [ benchmark ]
|
needs: [benchmark]
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -208,10 +208,7 @@ jobs:
|
|||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: bw-benchmark-json
|
name: bw-benchmark-json
|
||||||
path: benchmark-results
|
path: .
|
||||||
|
|
||||||
- name: Verify aggregate benchmark artifact
|
|
||||||
run: test -s "$BENCHMARK_JSON"
|
|
||||||
|
|
||||||
- name: Prepare gh-pages for publishing
|
- name: Prepare gh-pages for publishing
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,82 +1,6 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
|
||||||
## v3.8.0 (2026-05-01)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- **dock_area**: Change to show_dialo=False for CLI profile baseline restore
|
|
||||||
([`0b1f0b4`](https://github.com/bec-project/bec_widgets/commit/0b1f0b4c262ff31469b7114b9f00bf0a7b85e8f2))
|
|
||||||
|
|
||||||
- **dock_area**: Cli call load_profile has restore_baseline kwarg
|
|
||||||
([`cc82597`](https://github.com/bec-project/bec_widgets/commit/cc825972c202cd9ded32f8b2d1ce5f822c2ebdba))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- **dock_area**: Add CLI restore current profile from baseline with optional confirmation dialog
|
|
||||||
([`17865a2`](https://github.com/bec-project/bec_widgets/commit/17865a2c338a4a1f944659dde4ec05c25a8dd963))
|
|
||||||
|
|
||||||
|
|
||||||
## v3.7.3 (2026-05-01)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- **dock_area**: Profile names changed, default->baseline, user->runtime
|
|
||||||
([`dd32caf`](https://github.com/bec-project/bec_widgets/commit/dd32caf6e815fd1922b6aae84d00decad9dbf869))
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
- **dock_area**: Remove low-value tests
|
|
||||||
([`717d74b`](https://github.com/bec-project/bec_widgets/commit/717d74b19e8c6960209190c47ba32732ffaa0094))
|
|
||||||
|
|
||||||
|
|
||||||
## v3.7.2 (2026-04-29)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- **dock-area**: Avoid switching profile when saving new profile
|
|
||||||
([`73b44cf`](https://github.com/bec-project/bec_widgets/commit/73b44cffb219347cacb609f3b93068eda6701b42))
|
|
||||||
|
|
||||||
- **workspace-actions**: Use try/finally and restore previous blocked state in refresh_profiles
|
|
||||||
([`30ef255`](https://github.com/bec-project/bec_widgets/commit/30ef25533af9df5a9bd9e69808dc49fdf22f4318))
|
|
||||||
|
|
||||||
Agent-Logs-Url:
|
|
||||||
https://github.com/bec-project/bec_widgets/sessions/004cb4bc-5015-485e-a803-1e63876b7024
|
|
||||||
|
|
||||||
Co-authored-by: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com>
|
|
||||||
|
|
||||||
### Build System
|
|
||||||
|
|
||||||
- Add pytest-benchmark dependency
|
|
||||||
([`551d38d`](https://github.com/bec-project/bec_widgets/commit/551d38d90111361e64bfcf10a1409be71dd298bc))
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
- Update header comments in script files to indicate AI generation
|
|
||||||
([`3f1aa80`](https://github.com/bec-project/bec_widgets/commit/3f1aa80756368454c3631cb6cf9d29db28af177a))
|
|
||||||
|
|
||||||
### Continuous Integration
|
|
||||||
|
|
||||||
- Add benchmark workflow
|
|
||||||
([`999b7a2`](https://github.com/bec-project/bec_widgets/commit/999b7a2321f2f222c04b056a2db4280f66de9c48))
|
|
||||||
|
|
||||||
- Fix benchmark upload
|
|
||||||
([`19b5c8f`](https://github.com/bec-project/bec_widgets/commit/19b5c8f724dbdb7421957c290f9213bf072392df))
|
|
||||||
|
|
||||||
- Increase threshold to 20 percent
|
|
||||||
([`409c9e5`](https://github.com/bec-project/bec_widgets/commit/409c9e5bfacdfc003f1bc8c9944f01798bd3818e))
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
- Fix assertions after updating ophyd devices templates
|
|
||||||
([`a614d66`](https://github.com/bec-project/bec_widgets/commit/a614d662d6da716a49afb4ed3a3903f108210386))
|
|
||||||
|
|
||||||
Co-authored-by: Copilot <copilot@github.com>
|
|
||||||
|
|
||||||
- Remove references to "scan_motors" in tests
|
|
||||||
([`5056ef8`](https://github.com/bec-project/bec_widgets/commit/5056ef8946d03a20e802709d3fe81c84c195fe41))
|
|
||||||
|
|
||||||
|
|
||||||
## v3.7.1 (2026-04-21)
|
## v3.7.1 (2026-04-21)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+14
-46
@@ -340,10 +340,10 @@ class BECDockArea(RPCBase):
|
|||||||
Save the current workspace profile.
|
Save the current workspace profile.
|
||||||
|
|
||||||
On first save of a given name:
|
On first save of a given name:
|
||||||
- writes a baseline copy to profiles/baseline/<name>.ini with created_at
|
- writes a default copy to states/default/<name>.ini with tag=default and created_at
|
||||||
- writes a runtime copy to profiles/runtime/<name>.ini with created_at
|
- writes a user copy to states/user/<name>.ini with tag=user and created_at
|
||||||
On subsequent saves:
|
On subsequent saves of user-owned profiles:
|
||||||
- updates both the baseline and runtime copies so restore uses the latest snapshot.
|
- updates both the default and user copies so restore uses the latest snapshot.
|
||||||
Read-only bundled profiles cannot be overwritten.
|
Read-only bundled profiles cannot be overwritten.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -358,31 +358,15 @@ class BECDockArea(RPCBase):
|
|||||||
|
|
||||||
@rpc_timeout(None)
|
@rpc_timeout(None)
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def load_profile(self, name: "str | None" = None, restore_baseline: "bool" = False):
|
def load_profile(self, name: "str | None" = None):
|
||||||
"""
|
"""
|
||||||
Load a workspace profile.
|
Load a workspace profile.
|
||||||
|
|
||||||
Before switching, persist the current profile to the runtime copy.
|
Before switching, persist the current profile to the user copy.
|
||||||
Prefer loading the runtime copy; fall back to the baseline copy. When
|
Prefer loading the user copy; fall back to the default copy.
|
||||||
``restore_baseline`` is True, first overwrite the runtime copy with the
|
|
||||||
baseline profile and then load it.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str | None): The name of the profile to load. If None, prompts the user.
|
name (str | None): The name of the profile to load. If None, prompts the user.
|
||||||
restore_baseline (bool): If True, restore the runtime copy from the
|
|
||||||
baseline before loading. Defaults to False.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@rpc_timeout(None)
|
|
||||||
@rpc_call
|
|
||||||
def restore_baseline_profile(self, name: "str | None" = None, show_dialog: "bool" = False):
|
|
||||||
"""
|
|
||||||
Overwrite the runtime copy of *name* with the baseline.
|
|
||||||
If *name* is None, target the currently active profile.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str | None): The name of the profile to restore. If None, uses the current profile.
|
|
||||||
show_dialog (bool): If True, ask for confirmation before restoring.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
@@ -1364,10 +1348,10 @@ class DockAreaView(RPCBase):
|
|||||||
Save the current workspace profile.
|
Save the current workspace profile.
|
||||||
|
|
||||||
On first save of a given name:
|
On first save of a given name:
|
||||||
- writes a baseline copy to profiles/baseline/<name>.ini with created_at
|
- writes a default copy to states/default/<name>.ini with tag=default and created_at
|
||||||
- writes a runtime copy to profiles/runtime/<name>.ini with created_at
|
- writes a user copy to states/user/<name>.ini with tag=user and created_at
|
||||||
On subsequent saves:
|
On subsequent saves of user-owned profiles:
|
||||||
- updates both the baseline and runtime copies so restore uses the latest snapshot.
|
- updates both the default and user copies so restore uses the latest snapshot.
|
||||||
Read-only bundled profiles cannot be overwritten.
|
Read-only bundled profiles cannot be overwritten.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1382,31 +1366,15 @@ class DockAreaView(RPCBase):
|
|||||||
|
|
||||||
@rpc_timeout(None)
|
@rpc_timeout(None)
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def load_profile(self, name: "str | None" = None, restore_baseline: "bool" = False):
|
def load_profile(self, name: "str | None" = None):
|
||||||
"""
|
"""
|
||||||
Load a workspace profile.
|
Load a workspace profile.
|
||||||
|
|
||||||
Before switching, persist the current profile to the runtime copy.
|
Before switching, persist the current profile to the user copy.
|
||||||
Prefer loading the runtime copy; fall back to the baseline copy. When
|
Prefer loading the user copy; fall back to the default copy.
|
||||||
``restore_baseline`` is True, first overwrite the runtime copy with the
|
|
||||||
baseline profile and then load it.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str | None): The name of the profile to load. If None, prompts the user.
|
name (str | None): The name of the profile to load. If None, prompts the user.
|
||||||
restore_baseline (bool): If True, restore the runtime copy from the
|
|
||||||
baseline before loading. Defaults to False.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@rpc_timeout(None)
|
|
||||||
@rpc_call
|
|
||||||
def restore_baseline_profile(self, name: "str | None" = None, show_dialog: "bool" = False):
|
|
||||||
"""
|
|
||||||
Overwrite the runtime copy of *name* with the baseline.
|
|
||||||
If *name* is None, target the currently active profile.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str | None): The name of the profile to restore. If None, uses the current profile.
|
|
||||||
show_dialog (bool): If True, ask for confirmation before restoring.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
|
|
||||||
import bec_widgets.cli.client as client
|
import bec_widgets.cli.client as client
|
||||||
else:
|
else:
|
||||||
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
|
GUIRegistryStateMessage = lazy_import_from(
|
||||||
|
"bec_lib.messages", "GUIRegistryStateMessage"
|
||||||
|
)
|
||||||
client = lazy_import("bec_widgets.cli.client")
|
client = lazy_import("bec_widgets.cli.client")
|
||||||
|
|
||||||
|
|
||||||
@@ -199,7 +201,9 @@ class AvailableWidgetsNamespace:
|
|||||||
for attr_name, _ in self.__dict__.items():
|
for attr_name, _ in self.__dict__.items():
|
||||||
docs = getattr(client, attr_name).__doc__
|
docs = getattr(client, attr_name).__doc__
|
||||||
docs = docs if docs else "No description available"
|
docs = docs if docs else "No description available"
|
||||||
table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available")
|
table.add_row(
|
||||||
|
attr_name, docs if len(docs.strip()) > 0 else "No description available"
|
||||||
|
)
|
||||||
console.print(table)
|
console.print(table)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -230,7 +234,9 @@ class BECGuiClient(RPCBase):
|
|||||||
@property
|
@property
|
||||||
def launcher(self) -> RPCBase:
|
def launcher(self) -> RPCBase:
|
||||||
"""The launcher object."""
|
"""The launcher object."""
|
||||||
return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher")
|
return RPCBase(
|
||||||
|
gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher"
|
||||||
|
)
|
||||||
|
|
||||||
def _safe_register_stream(self, endpoint: EndpointInfo, cb: Callable, **kwargs):
|
def _safe_register_stream(self, endpoint: EndpointInfo, cb: Callable, **kwargs):
|
||||||
"""Check if already registered for registration in idempotent functions."""
|
"""Check if already registered for registration in idempotent functions."""
|
||||||
@@ -241,7 +247,8 @@ class BECGuiClient(RPCBase):
|
|||||||
"""Connect to a GUI server"""
|
"""Connect to a GUI server"""
|
||||||
# Unregister the old callback
|
# Unregister the old callback
|
||||||
self._client.connector.unregister(
|
self._client.connector.unregister(
|
||||||
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
MessageEndpoints.gui_registry_state(self._gui_id),
|
||||||
|
cb=self._handle_registry_update,
|
||||||
)
|
)
|
||||||
self._gui_id = gui_id
|
self._gui_id = gui_id
|
||||||
|
|
||||||
@@ -401,7 +408,9 @@ class BECGuiClient(RPCBase):
|
|||||||
and "has no attribute 'system.launch_dock_area'" not in error
|
and "has no attribute 'system.launch_dock_area'" not in error
|
||||||
):
|
):
|
||||||
raise
|
raise
|
||||||
logger.debug("Server does not support system.launch_dock_area; using launcher RPC")
|
logger.debug(
|
||||||
|
"Server does not support system.launch_dock_area; using launcher RPC"
|
||||||
|
)
|
||||||
|
|
||||||
return self.launcher._run_rpc(
|
return self.launcher._run_rpc(
|
||||||
"launch",
|
"launch",
|
||||||
@@ -462,7 +471,8 @@ class BECGuiClient(RPCBase):
|
|||||||
|
|
||||||
# Unregister the registry state
|
# Unregister the registry state
|
||||||
self._client.connector.unregister(
|
self._client.connector.unregister(
|
||||||
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
MessageEndpoints.gui_registry_state(self._gui_id),
|
||||||
|
cb=self._handle_registry_update,
|
||||||
)
|
)
|
||||||
# Remove all reference from top level
|
# Remove all reference from top level
|
||||||
self._top_level.clear()
|
self._top_level.clear()
|
||||||
@@ -525,9 +535,15 @@ class BECGuiClient(RPCBase):
|
|||||||
finally:
|
finally:
|
||||||
threading.current_thread().cancel() # type: ignore
|
threading.current_thread().cancel() # type: ignore
|
||||||
|
|
||||||
self._gui_started_timer = RepeatTimer(
|
def check_gui_started_and_continue():
|
||||||
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
|
if self._gui_is_alive():
|
||||||
)
|
gui_started_callback(self._gui_post_startup)
|
||||||
|
elif self._process.poll():
|
||||||
|
logger.error(
|
||||||
|
f"GUI process failed to start with: {self._process.communicate()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._gui_started_timer = RepeatTimer(0.5, check_gui_started_and_continue)
|
||||||
self._gui_started_timer.start()
|
self._gui_started_timer.start()
|
||||||
|
|
||||||
if wait:
|
if wait:
|
||||||
@@ -536,7 +552,8 @@ class BECGuiClient(RPCBase):
|
|||||||
def _start(self, wait: bool = False) -> None:
|
def _start(self, wait: bool = False) -> None:
|
||||||
self._killed = False
|
self._killed = False
|
||||||
self._safe_register_stream(
|
self._safe_register_stream(
|
||||||
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
MessageEndpoints.gui_registry_state(self._gui_id),
|
||||||
|
cb=self._handle_registry_update,
|
||||||
)
|
)
|
||||||
return self._start_server(wait=wait)
|
return self._start_server(wait=wait)
|
||||||
|
|
||||||
@@ -603,7 +620,9 @@ class BECGuiClient(RPCBase):
|
|||||||
self._ipython_registry.pop(gui_id)
|
self._ipython_registry.pop(gui_id)
|
||||||
|
|
||||||
removed_widgets = [
|
removed_widgets = [
|
||||||
widget.object_name for widget in self._top_level.values() if widget._is_deleted()
|
widget.object_name
|
||||||
|
for widget in self._top_level.values()
|
||||||
|
if widget._is_deleted()
|
||||||
]
|
]
|
||||||
|
|
||||||
for widget_name in removed_widgets:
|
for widget_name in removed_widgets:
|
||||||
|
|||||||
@@ -35,25 +35,25 @@ from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
|||||||
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
||||||
from bec_widgets.widgets.containers.dock_area.profile_utils import (
|
from bec_widgets.widgets.containers.dock_area.profile_utils import (
|
||||||
SETTINGS_KEYS,
|
SETTINGS_KEYS,
|
||||||
baseline_profile_candidates,
|
default_profile_candidates,
|
||||||
delete_profile_files,
|
delete_profile_files,
|
||||||
get_last_profile,
|
get_last_profile,
|
||||||
is_profile_read_only,
|
is_profile_read_only,
|
||||||
is_quick_select,
|
is_quick_select,
|
||||||
list_profiles,
|
list_profiles,
|
||||||
list_quick_profiles,
|
list_quick_profiles,
|
||||||
load_baseline_profile_screenshot,
|
load_default_profile_screenshot,
|
||||||
load_runtime_profile_screenshot,
|
load_user_profile_screenshot,
|
||||||
now_iso_utc,
|
now_iso_utc,
|
||||||
open_baseline_settings,
|
open_default_settings,
|
||||||
open_runtime_settings,
|
open_user_settings,
|
||||||
profile_origin,
|
profile_origin,
|
||||||
profile_origin_display,
|
profile_origin_display,
|
||||||
read_manifest,
|
read_manifest,
|
||||||
restore_runtime_from_baseline,
|
restore_user_from_default,
|
||||||
runtime_profile_candidates,
|
|
||||||
set_last_profile,
|
set_last_profile,
|
||||||
set_quick_select,
|
set_quick_select,
|
||||||
|
user_profile_candidates,
|
||||||
write_manifest,
|
write_manifest,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.containers.dock_area.settings.dialogs import (
|
from bec_widgets.widgets.containers.dock_area.settings.dialogs import (
|
||||||
@@ -108,7 +108,6 @@ class BECDockArea(DockAreaWidget):
|
|||||||
"list_profiles",
|
"list_profiles",
|
||||||
"save_profile",
|
"save_profile",
|
||||||
"load_profile",
|
"load_profile",
|
||||||
"restore_baseline_profile",
|
|
||||||
"delete_profile",
|
"delete_profile",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -236,8 +235,11 @@ class BECDockArea(DockAreaWidget):
|
|||||||
def _load_initial_profile(self, name: str) -> None:
|
def _load_initial_profile(self, name: str) -> None:
|
||||||
"""Load the initial profile."""
|
"""Load the initial profile."""
|
||||||
self.load_profile(name)
|
self.load_profile(name)
|
||||||
|
combo = self.toolbar.components.get_action("workspace_combo").widget
|
||||||
|
combo.blockSignals(True)
|
||||||
if not self._empty_profile_active:
|
if not self._empty_profile_active:
|
||||||
self._set_workspace_combo_text_silent(name)
|
combo.setCurrentText(name)
|
||||||
|
combo.blockSignals(False)
|
||||||
|
|
||||||
def _start_empty_workspace(self) -> None:
|
def _start_empty_workspace(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -589,13 +591,13 @@ class BECDockArea(DockAreaWidget):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def profile_namespace(self) -> str | None:
|
def profile_namespace(self) -> str | None:
|
||||||
"""Namespace used to scope runtime/baseline profile files for this dock area."""
|
"""Namespace used to scope user/default profile files for this dock area."""
|
||||||
return self._resolve_profile_namespace()
|
return self._resolve_profile_namespace()
|
||||||
|
|
||||||
def _profile_exists(self, name: str, namespace: str | None) -> bool:
|
def _profile_exists(self, name: str, namespace: str | None) -> bool:
|
||||||
return any(
|
return any(
|
||||||
os.path.exists(path) for path in runtime_profile_candidates(name, namespace)
|
os.path.exists(path) for path in user_profile_candidates(name, namespace)
|
||||||
) or any(os.path.exists(path) for path in baseline_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:
|
def _write_snapshot_to_settings(self, settings, save_preview: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -621,34 +623,35 @@ class BECDockArea(DockAreaWidget):
|
|||||||
name: str,
|
name: str,
|
||||||
namespace: str | None,
|
namespace: str | None,
|
||||||
*,
|
*,
|
||||||
write_baseline: bool = True,
|
write_default: bool = True,
|
||||||
write_runtime: bool = True,
|
write_user: bool = True,
|
||||||
save_preview: bool = True,
|
save_preview: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Write profile settings to baseline and/or runtime settings files.
|
Write profile settings to default and/or user settings files.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The profile name.
|
name: The profile name.
|
||||||
namespace: The profile namespace.
|
namespace: The profile namespace.
|
||||||
write_baseline: Whether to write to the baseline settings file.
|
write_default: Whether to write to the default settings file.
|
||||||
write_runtime: Whether to write to the runtime settings file.
|
write_user: Whether to write to the user settings file.
|
||||||
save_preview: Whether to save a screenshot preview.
|
save_preview: Whether to save a screenshot preview.
|
||||||
"""
|
"""
|
||||||
|
if write_default:
|
||||||
|
ds = open_default_settings(name, namespace=namespace)
|
||||||
|
self._write_snapshot_to_settings(ds, save_preview=save_preview)
|
||||||
|
if not ds.value(SETTINGS_KEYS["created_at"], ""):
|
||||||
|
ds.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
||||||
|
if not ds.value(SETTINGS_KEYS["is_quick_select"], None):
|
||||||
|
ds.setValue(SETTINGS_KEYS["is_quick_select"], True)
|
||||||
|
|
||||||
def _write_settings(open_settings) -> None:
|
if write_user:
|
||||||
settings = open_settings(name, namespace=namespace)
|
us = open_user_settings(name, namespace=namespace)
|
||||||
self._write_snapshot_to_settings(settings, save_preview=save_preview)
|
self._write_snapshot_to_settings(us, save_preview=save_preview)
|
||||||
if not settings.value(SETTINGS_KEYS["created_at"], ""):
|
if not us.value(SETTINGS_KEYS["created_at"], ""):
|
||||||
settings.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
us.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
||||||
if not settings.value(SETTINGS_KEYS["is_quick_select"], None):
|
if not us.value(SETTINGS_KEYS["is_quick_select"], None):
|
||||||
settings.setValue(SETTINGS_KEYS["is_quick_select"], True)
|
us.setValue(SETTINGS_KEYS["is_quick_select"], True)
|
||||||
|
|
||||||
if write_baseline:
|
|
||||||
_write_settings(open_baseline_settings)
|
|
||||||
|
|
||||||
if write_runtime:
|
|
||||||
_write_settings(open_runtime_settings)
|
|
||||||
|
|
||||||
def _finalize_profile_change(self, name: str, namespace: str | None) -> None:
|
def _finalize_profile_change(self, name: str, namespace: str | None) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -666,14 +669,6 @@ class BECDockArea(DockAreaWidget):
|
|||||||
combo = self.toolbar.components.get_action("workspace_combo").widget
|
combo = self.toolbar.components.get_action("workspace_combo").widget
|
||||||
combo.refresh_profiles(active_profile=name)
|
combo.refresh_profiles(active_profile=name)
|
||||||
|
|
||||||
def _set_workspace_combo_text_silent(self, text: str) -> None:
|
|
||||||
combo = self.toolbar.components.get_action("workspace_combo").widget
|
|
||||||
was_blocked = combo.blockSignals(True)
|
|
||||||
try:
|
|
||||||
combo.setCurrentText(text)
|
|
||||||
finally:
|
|
||||||
combo.blockSignals(was_blocked)
|
|
||||||
|
|
||||||
def _enter_empty_profile_state(self) -> None:
|
def _enter_empty_profile_state(self) -> None:
|
||||||
"""
|
"""
|
||||||
Switch to the transient empty workspace state.
|
Switch to the transient empty workspace state.
|
||||||
@@ -710,10 +705,10 @@ class BECDockArea(DockAreaWidget):
|
|||||||
Save the current workspace profile.
|
Save the current workspace profile.
|
||||||
|
|
||||||
On first save of a given name:
|
On first save of a given name:
|
||||||
- writes a baseline copy to profiles/baseline/<name>.ini with created_at
|
- writes a default copy to states/default/<name>.ini with tag=default and created_at
|
||||||
- writes a runtime copy to profiles/runtime/<name>.ini with created_at
|
- writes a user copy to states/user/<name>.ini with tag=user and created_at
|
||||||
On subsequent saves:
|
On subsequent saves of user-owned profiles:
|
||||||
- updates both the baseline and runtime copies so restore uses the latest snapshot.
|
- updates both the default and user copies so restore uses the latest snapshot.
|
||||||
Read-only bundled profiles cannot be overwritten.
|
Read-only bundled profiles cannot be overwritten.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -777,7 +772,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
overwrite_existing = origin == "settings"
|
overwrite_existing = origin == "settings"
|
||||||
|
|
||||||
origin_before_save = profile_origin(name, namespace=namespace)
|
origin_before_save = profile_origin(name, namespace=namespace)
|
||||||
overwrite_baseline = overwrite_existing and origin_before_save == "settings"
|
overwrite_default = overwrite_existing and origin_before_save == "settings"
|
||||||
|
|
||||||
# Display saving placeholder in toolbar
|
# Display saving placeholder in toolbar
|
||||||
workspace_combo = self.toolbar.components.get_action("workspace_combo").widget
|
workspace_combo = self.toolbar.components.get_action("workspace_combo").widget
|
||||||
@@ -786,12 +781,12 @@ class BECDockArea(DockAreaWidget):
|
|||||||
workspace_combo.setCurrentIndex(0)
|
workspace_combo.setCurrentIndex(0)
|
||||||
workspace_combo.blockSignals(False)
|
workspace_combo.blockSignals(False)
|
||||||
|
|
||||||
# Write to baseline and/or runtime settings
|
# Write to default and/or user settings
|
||||||
should_write_baseline = overwrite_baseline or not any(
|
should_write_default = overwrite_default or not any(
|
||||||
os.path.exists(path) for path in baseline_profile_candidates(name, namespace)
|
os.path.exists(path) for path in default_profile_candidates(name, namespace)
|
||||||
)
|
)
|
||||||
self._write_profile_settings(
|
self._write_profile_settings(
|
||||||
name, namespace, write_baseline=should_write_baseline, write_runtime=True
|
name, namespace, write_default=should_write_default, write_user=True
|
||||||
)
|
)
|
||||||
|
|
||||||
set_quick_select(name, quickselect, namespace=namespace)
|
set_quick_select(name, quickselect, namespace=namespace)
|
||||||
@@ -801,6 +796,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
self._pending_autosave_skip = (current_profile, name)
|
self._pending_autosave_skip = (current_profile, name)
|
||||||
else:
|
else:
|
||||||
self._pending_autosave_skip = None
|
self._pending_autosave_skip = None
|
||||||
|
workspace_combo.setCurrentText(name)
|
||||||
self._finalize_profile_change(name, namespace)
|
self._finalize_profile_change(name, namespace)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
@@ -820,21 +816,16 @@ class BECDockArea(DockAreaWidget):
|
|||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
@SafeSlot(str)
|
@SafeSlot(str)
|
||||||
@SafeSlot(str, bool)
|
|
||||||
@rpc_timeout(None)
|
@rpc_timeout(None)
|
||||||
def load_profile(self, name: str | None = None, restore_baseline: bool = False):
|
def load_profile(self, name: str | None = None):
|
||||||
"""
|
"""
|
||||||
Load a workspace profile.
|
Load a workspace profile.
|
||||||
|
|
||||||
Before switching, persist the current profile to the runtime copy.
|
Before switching, persist the current profile to the user copy.
|
||||||
Prefer loading the runtime copy; fall back to the baseline copy. When
|
Prefer loading the user copy; fall back to the default copy.
|
||||||
``restore_baseline`` is True, first overwrite the runtime copy with the
|
|
||||||
baseline profile and then load it.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str | None): The name of the profile to load. If None, prompts the user.
|
name (str | None): The name of the profile to load. If None, prompts the user.
|
||||||
restore_baseline (bool): If True, restore the runtime copy from the
|
|
||||||
baseline before loading. Defaults to False.
|
|
||||||
"""
|
"""
|
||||||
if name == "":
|
if name == "":
|
||||||
return
|
return
|
||||||
@@ -853,17 +844,14 @@ class BECDockArea(DockAreaWidget):
|
|||||||
if skip_pair and skip_pair == (prev_name, name):
|
if skip_pair and skip_pair == (prev_name, name):
|
||||||
self._pending_autosave_skip = None
|
self._pending_autosave_skip = None
|
||||||
else:
|
else:
|
||||||
us_prev = open_runtime_settings(prev_name, namespace=namespace)
|
us_prev = open_user_settings(prev_name, namespace=namespace)
|
||||||
self._write_snapshot_to_settings(us_prev, save_preview=True)
|
self._write_snapshot_to_settings(us_prev, save_preview=True)
|
||||||
|
|
||||||
if restore_baseline:
|
|
||||||
restore_runtime_from_baseline(name, namespace=namespace)
|
|
||||||
|
|
||||||
settings = None
|
settings = None
|
||||||
if any(os.path.exists(path) for path in runtime_profile_candidates(name, namespace)):
|
if any(os.path.exists(path) for path in user_profile_candidates(name, namespace)):
|
||||||
settings = open_runtime_settings(name, namespace=namespace)
|
settings = open_user_settings(name, namespace=namespace)
|
||||||
elif any(os.path.exists(path) for path in baseline_profile_candidates(name, namespace)):
|
elif any(os.path.exists(path) for path in default_profile_candidates(name, namespace)):
|
||||||
settings = open_baseline_settings(name, namespace=namespace)
|
settings = open_default_settings(name, namespace=namespace)
|
||||||
if settings is None:
|
if settings is None:
|
||||||
logger.warning(f"Profile '{name}' not found in namespace '{namespace}'. Creating new.")
|
logger.warning(f"Profile '{name}' not found in namespace '{namespace}'. Creating new.")
|
||||||
self.delete_all()
|
self.delete_all()
|
||||||
@@ -905,36 +893,32 @@ class BECDockArea(DockAreaWidget):
|
|||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
@SafeSlot(str)
|
@SafeSlot(str)
|
||||||
@SafeSlot(str, bool)
|
def restore_user_profile_from_default(self, name: str | None = None):
|
||||||
@rpc_timeout(None)
|
|
||||||
def restore_baseline_profile(self, name: str | None = None, show_dialog: bool = False):
|
|
||||||
"""
|
"""
|
||||||
Overwrite the runtime copy of *name* with the baseline.
|
Overwrite the user copy of *name* with the default baseline.
|
||||||
If *name* is None, target the currently active profile.
|
If *name* is None, target the currently active profile.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str | None): The name of the profile to restore. If None, uses the current profile.
|
name (str | None): The name of the profile to restore. If None, uses the current profile.
|
||||||
show_dialog (bool): If True, ask for confirmation before restoring.
|
|
||||||
"""
|
"""
|
||||||
target = name or getattr(self, "_current_profile_name", None)
|
target = name or getattr(self, "_current_profile_name", None)
|
||||||
if not target:
|
if not target:
|
||||||
return
|
return
|
||||||
namespace = self.profile_namespace
|
namespace = self.profile_namespace
|
||||||
|
|
||||||
if show_dialog:
|
current_pixmap = None
|
||||||
current_pixmap = None
|
if self.isVisible():
|
||||||
if self.isVisible():
|
current_pixmap = QPixmap()
|
||||||
current_pixmap = QPixmap()
|
ba = bytes(self.screenshot_bytes())
|
||||||
ba = bytes(self.screenshot_bytes())
|
current_pixmap.loadFromData(ba)
|
||||||
current_pixmap.loadFromData(ba)
|
if current_pixmap is None or current_pixmap.isNull():
|
||||||
if current_pixmap is None or current_pixmap.isNull():
|
current_pixmap = load_user_profile_screenshot(target, namespace=namespace)
|
||||||
current_pixmap = load_runtime_profile_screenshot(target, namespace=namespace)
|
default_pixmap = load_default_profile_screenshot(target, namespace=namespace)
|
||||||
baseline_pixmap = load_baseline_profile_screenshot(target, namespace=namespace)
|
|
||||||
|
|
||||||
if not RestoreProfileDialog.confirm(self, current_pixmap, baseline_pixmap):
|
if not RestoreProfileDialog.confirm(self, current_pixmap, default_pixmap):
|
||||||
return
|
return
|
||||||
|
|
||||||
restore_runtime_from_baseline(target, namespace=namespace)
|
restore_user_from_default(target, namespace=namespace)
|
||||||
self.delete_all()
|
self.delete_all()
|
||||||
self.load_profile(target)
|
self.load_profile(target)
|
||||||
|
|
||||||
@@ -1069,7 +1053,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
manage_action = self.toolbar.components.get_action("manage_workspaces").action
|
manage_action = self.toolbar.components.get_action("manage_workspaces").action
|
||||||
if self.manage_dialog is None or not self.manage_dialog.isVisible():
|
if self.manage_dialog is None or not self.manage_dialog.isVisible():
|
||||||
self.manage_widget = WorkSpaceManager(
|
self.manage_widget = WorkSpaceManager(
|
||||||
self, target_widget=self, active_profile=self._current_profile_name
|
self, target_widget=self, default_profile=self._current_profile_name
|
||||||
)
|
)
|
||||||
self.manage_dialog = QDialog(modal=False)
|
self.manage_dialog = QDialog(modal=False)
|
||||||
|
|
||||||
@@ -1168,7 +1152,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
namespace = self.profile_namespace
|
namespace = self.profile_namespace
|
||||||
settings = open_runtime_settings(name, namespace=namespace)
|
settings = open_user_settings(name, namespace=namespace)
|
||||||
self._write_snapshot_to_settings(settings)
|
self._write_snapshot_to_settings(settings)
|
||||||
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
|
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
|
||||||
self._exit_snapshot_written = True
|
self._exit_snapshot_written = True
|
||||||
|
|||||||
@@ -2,13 +2,9 @@
|
|||||||
Utilities for managing BECDockArea profiles stored in INI files.
|
Utilities for managing BECDockArea profiles stored in INI files.
|
||||||
|
|
||||||
Policy:
|
Policy:
|
||||||
- All created/modified profiles are stored under the BEC settings root:
|
- All created/modified profiles are stored under the BEC settings root: <base_path>/profiles/{default,user}
|
||||||
<base_path>/profiles/{baseline,runtime}
|
- Bundled read-only defaults are discovered in BW core states/default and plugin bec_widgets/profiles but never written to.
|
||||||
- Bundled read-only baselines are discovered in BW core profiles and plugin
|
- Lookup order when reading: user → settings default → app or plugin bundled default.
|
||||||
bec_widgets/profiles but never written to.
|
|
||||||
- Lookup order when reading: runtime → settings baseline → app or plugin bundled baseline.
|
|
||||||
- Legacy settings paths profiles/{default,user} are read through a thin segment
|
|
||||||
alias layer and copied to the canonical location on first access.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -36,12 +32,6 @@ logger = bec_logger.logger
|
|||||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
ProfileOrigin = Literal["module", "plugin", "settings", "unknown"]
|
ProfileOrigin = Literal["module", "plugin", "settings", "unknown"]
|
||||||
ProfileSegment = Literal["baseline", "runtime"]
|
|
||||||
|
|
||||||
_PROFILE_SEGMENT_ALIASES: dict[ProfileSegment, tuple[str, str]] = {
|
|
||||||
"baseline": ("baseline", "default"),
|
|
||||||
"runtime": ("runtime", "user"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def module_profiles_dir() -> str:
|
def module_profiles_dir() -> str:
|
||||||
@@ -140,7 +130,7 @@ def _profiles_dir(segment: str, namespace: str | None) -> str:
|
|||||||
Build (and ensure) the directory that holds profiles for a namespace segment.
|
Build (and ensure) the directory that holds profiles for a namespace segment.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
segment (str): Profile segment directory name.
|
segment (str): Either ``"user"`` or ``"default"``.
|
||||||
namespace (str | None): Optional namespace label to scope profiles.
|
namespace (str | None): Optional namespace label to scope profiles.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -153,175 +143,157 @@ def _profiles_dir(segment: str, namespace: str | None) -> str:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def _candidate_namespaces(namespace: str | None) -> list[str | None]:
|
def _user_path_candidates(name: str, namespace: str | None) -> list[str]:
|
||||||
|
"""
|
||||||
|
Generate candidate user-profile paths honoring namespace fallbacks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Profile name without extension.
|
||||||
|
namespace (str | None): Optional namespace label.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: Ordered list of candidate user profile paths (.ini files).
|
||||||
|
"""
|
||||||
ns = slugify.slugify(namespace, separator="_") if namespace else None
|
ns = slugify.slugify(namespace, separator="_") if namespace else None
|
||||||
|
primary = os.path.join(_profiles_dir("user", ns), f"{name}.ini")
|
||||||
if not ns:
|
if not ns:
|
||||||
return [None]
|
return [primary]
|
||||||
return [ns, None]
|
legacy = os.path.join(_profiles_dir("user", None), f"{name}.ini")
|
||||||
|
return [primary, legacy] if legacy != primary else [primary]
|
||||||
|
|
||||||
|
|
||||||
def _segment_profile_path(segment_name: str, name: str, namespace: str | None) -> str:
|
def _default_path_candidates(name: str, namespace: str | None) -> list[str]:
|
||||||
return os.path.join(_profiles_dir(segment_name, namespace), f"{name}.ini")
|
|
||||||
|
|
||||||
|
|
||||||
def _canonical_profile_path(segment: ProfileSegment, name: str, namespace: str | None) -> str:
|
|
||||||
return _segment_profile_path(_PROFILE_SEGMENT_ALIASES[segment][0], name, namespace)
|
|
||||||
|
|
||||||
|
|
||||||
def _segment_path_candidates(
|
|
||||||
segment: ProfileSegment,
|
|
||||||
name: str,
|
|
||||||
namespace: str | None,
|
|
||||||
*,
|
|
||||||
include_legacy: bool = True,
|
|
||||||
migrate_legacy: bool = True,
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
"""
|
||||||
Generate profile candidates for a canonical segment.
|
Generate candidate default-profile paths honoring namespace fallbacks.
|
||||||
|
|
||||||
Canonical baseline/runtime files are always preferred. Namespace fallback
|
Args:
|
||||||
files and legacy default/user files are copied to the primary canonical path
|
name (str): Profile name without extension.
|
||||||
when the primary file does not exist.
|
namespace (str | None): Optional namespace label.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: Ordered list of candidate default profile paths (.ini files).
|
||||||
"""
|
"""
|
||||||
canonical = [
|
ns = slugify.slugify(namespace, separator="_") if namespace else None
|
||||||
_segment_profile_path(_PROFILE_SEGMENT_ALIASES[segment][0], name, ns)
|
primary = os.path.join(_profiles_dir("default", ns), f"{name}.ini")
|
||||||
for ns in _candidate_namespaces(namespace)
|
if not ns:
|
||||||
]
|
return [primary]
|
||||||
legacy = []
|
legacy = os.path.join(_profiles_dir("default", None), f"{name}.ini")
|
||||||
if include_legacy:
|
return [primary, legacy] if legacy != primary else [primary]
|
||||||
legacy = [
|
|
||||||
_segment_profile_path(_PROFILE_SEGMENT_ALIASES[segment][1], name, ns)
|
|
||||||
for ns in _candidate_namespaces(namespace)
|
|
||||||
]
|
|
||||||
|
|
||||||
primary_canonical = canonical[0]
|
|
||||||
if migrate_legacy and not os.path.exists(primary_canonical):
|
|
||||||
canonical_src = next((path for path in canonical[1:] if os.path.exists(path)), None)
|
|
||||||
if canonical_src:
|
|
||||||
os.makedirs(os.path.dirname(primary_canonical), exist_ok=True)
|
|
||||||
shutil.copy2(canonical_src, primary_canonical)
|
|
||||||
elif include_legacy:
|
|
||||||
legacy_src = next((path for path in legacy if os.path.exists(path)), None)
|
|
||||||
if legacy_src:
|
|
||||||
os.makedirs(os.path.dirname(primary_canonical), exist_ok=True)
|
|
||||||
shutil.copy2(legacy_src, primary_canonical)
|
|
||||||
|
|
||||||
return list(dict.fromkeys(canonical + legacy))
|
|
||||||
|
|
||||||
|
|
||||||
def baseline_profiles_dir(namespace: str | None = None) -> str:
|
def default_profiles_dir(namespace: str | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
Return the directory that stores baseline profiles for the namespace.
|
Return the directory that stores default profiles for the namespace.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Absolute path to the baseline profile directory.
|
str: Absolute path to the default profile directory.
|
||||||
"""
|
"""
|
||||||
return _profiles_dir("baseline", namespace)
|
return _profiles_dir("default", namespace)
|
||||||
|
|
||||||
|
|
||||||
def runtime_profiles_dir(namespace: str | None = None) -> str:
|
def user_profiles_dir(namespace: str | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
Return the directory that stores runtime profiles for the namespace.
|
Return the directory that stores user profiles for the namespace.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Absolute path to the runtime profile directory.
|
str: Absolute path to the user profile directory.
|
||||||
"""
|
"""
|
||||||
return _profiles_dir("runtime", namespace)
|
return _profiles_dir("user", namespace)
|
||||||
|
|
||||||
|
|
||||||
def baseline_profile_path(name: str, namespace: str | None = None) -> str:
|
def default_profile_path(name: str, namespace: str | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
Compute the canonical baseline profile path for a profile name.
|
Compute the canonical default profile path for a profile name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Absolute path to the baseline profile file (.ini).
|
str: Absolute path to the default profile file (.ini).
|
||||||
"""
|
"""
|
||||||
return _canonical_profile_path("baseline", name, namespace)
|
return _default_path_candidates(name, namespace)[0]
|
||||||
|
|
||||||
|
|
||||||
def runtime_profile_path(name: str, namespace: str | None = None) -> str:
|
def user_profile_path(name: str, namespace: str | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
Compute the canonical runtime profile path for a profile name.
|
Compute the canonical user profile path for a profile name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Absolute path to the runtime profile file (.ini).
|
str: Absolute path to the user profile file (.ini).
|
||||||
"""
|
"""
|
||||||
return _canonical_profile_path("runtime", name, namespace)
|
return _user_path_candidates(name, namespace)[0]
|
||||||
|
|
||||||
|
|
||||||
def runtime_profile_candidates(name: str, namespace: str | None = None) -> list[str]:
|
def user_profile_candidates(name: str, namespace: str | None = None) -> list[str]:
|
||||||
"""
|
"""
|
||||||
List all runtime profile path candidates for a profile name.
|
List all user profile path candidates for a profile name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[str]: De-duplicated list of candidate runtime profile paths.
|
list[str]: De-duplicated list of candidate user profile paths.
|
||||||
"""
|
"""
|
||||||
return _segment_path_candidates("runtime", name, namespace)
|
return list(dict.fromkeys(_user_path_candidates(name, namespace)))
|
||||||
|
|
||||||
|
|
||||||
def baseline_profile_candidates(name: str, namespace: str | None = None) -> list[str]:
|
def default_profile_candidates(name: str, namespace: str | None = None) -> list[str]:
|
||||||
"""
|
"""
|
||||||
List all baseline profile path candidates for a profile name.
|
List all default profile path candidates for a profile name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[str]: De-duplicated list of candidate baseline profile paths.
|
list[str]: De-duplicated list of candidate default profile paths.
|
||||||
"""
|
"""
|
||||||
return _segment_path_candidates("baseline", name, namespace)
|
return list(dict.fromkeys(_default_path_candidates(name, namespace)))
|
||||||
|
|
||||||
|
|
||||||
def _existing_runtime_settings(name: str, namespace: str | None = None) -> QSettings | None:
|
def _existing_user_settings(name: str, namespace: str | None = None) -> QSettings | None:
|
||||||
"""
|
"""
|
||||||
Resolve the first existing runtime profile settings object.
|
Resolve the first existing user profile settings object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label to search. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label to search. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QSettings | None: Config for the first existing runtime profile candidate, or ``None``
|
QSettings | None: Config for the first existing user profile candidate, or ``None``
|
||||||
when no files are present.
|
when no files are present.
|
||||||
"""
|
"""
|
||||||
for path in runtime_profile_candidates(name, namespace):
|
for path in user_profile_candidates(name, namespace):
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return QSettings(path, QSettings.IniFormat)
|
return QSettings(path, QSettings.IniFormat)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _existing_baseline_settings(name: str, namespace: str | None = None) -> QSettings | None:
|
def _existing_default_settings(name: str, namespace: str | None = None) -> QSettings | None:
|
||||||
"""
|
"""
|
||||||
Resolve the first existing baseline profile settings object.
|
Resolve the first existing default profile settings object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label to search. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label to search. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QSettings | None: Config for the first existing baseline profile candidate, or ``None``
|
QSettings | None: Config for the first existing default profile candidate, or ``None``
|
||||||
when no files are present.
|
when no files are present.
|
||||||
"""
|
"""
|
||||||
for path in baseline_profile_candidates(name, namespace):
|
for path in default_profile_candidates(name, namespace):
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return QSettings(path, QSettings.IniFormat)
|
return QSettings(path, QSettings.IniFormat)
|
||||||
return None
|
return None
|
||||||
@@ -375,7 +347,7 @@ def profile_origin(name: str, namespace: str | None = None) -> ProfileOrigin:
|
|||||||
plugin_path = plugin_profile_path(name)
|
plugin_path = plugin_profile_path(name)
|
||||||
if plugin_path and os.path.exists(plugin_path):
|
if plugin_path and os.path.exists(plugin_path):
|
||||||
return "plugin"
|
return "plugin"
|
||||||
for path in runtime_profile_candidates(name, namespace) + baseline_profile_candidates(
|
for path in user_profile_candidates(name, namespace) + default_profile_candidates(
|
||||||
name, namespace
|
name, namespace
|
||||||
):
|
):
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
@@ -434,8 +406,8 @@ def delete_profile_files(name: str, namespace: str | None = None) -> bool:
|
|||||||
read_only = is_profile_read_only(name, namespace)
|
read_only = is_profile_read_only(name, namespace)
|
||||||
|
|
||||||
removed = False
|
removed = False
|
||||||
# Always allow removing runtime copies; keep baseline copies for read-only origins.
|
# Always allow removing user copies; keep default copies for read-only origins.
|
||||||
for path in set(runtime_profile_candidates(name, namespace)):
|
for path in set(user_profile_candidates(name, namespace)):
|
||||||
try:
|
try:
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
removed = True
|
removed = True
|
||||||
@@ -443,7 +415,7 @@ def delete_profile_files(name: str, namespace: str | None = None) -> bool:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not read_only:
|
if not read_only:
|
||||||
for path in set(baseline_profile_candidates(name, namespace)):
|
for path in set(default_profile_candidates(name, namespace)):
|
||||||
try:
|
try:
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
removed = True
|
removed = True
|
||||||
@@ -471,7 +443,7 @@ SETTINGS_KEYS = {
|
|||||||
|
|
||||||
def list_profiles(namespace: str | None = None) -> list[str]:
|
def list_profiles(namespace: str | None = None) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Enumerate all known profile names, syncing bundled baselines when missing locally.
|
Enumerate all known profile names, syncing bundled defaults when missing locally.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace (str | None, optional): Namespace label scoped to the profile set.
|
namespace (str | None, optional): Namespace label scoped to the profile set.
|
||||||
@@ -487,27 +459,16 @@ def list_profiles(namespace: str | None = None) -> list[str]:
|
|||||||
return set()
|
return set()
|
||||||
return {os.path.splitext(f)[0] for f in os.listdir(directory) if f.endswith(".ini")}
|
return {os.path.splitext(f)[0] for f in os.listdir(directory) if f.endswith(".ini")}
|
||||||
|
|
||||||
settings_dirs = {baseline_profiles_dir(namespace), runtime_profiles_dir(namespace)}
|
settings_dirs = {default_profiles_dir(namespace), user_profiles_dir(namespace)}
|
||||||
if ns:
|
if ns:
|
||||||
settings_dirs.add(baseline_profiles_dir(None))
|
settings_dirs.add(default_profiles_dir(None))
|
||||||
settings_dirs.add(runtime_profiles_dir(None))
|
settings_dirs.add(user_profiles_dir(None))
|
||||||
|
|
||||||
for segment in ("baseline", "runtime"):
|
|
||||||
for legacy_dir in [
|
|
||||||
_profiles_dir(_PROFILE_SEGMENT_ALIASES[segment][1], item)
|
|
||||||
for item in _candidate_namespaces(namespace)
|
|
||||||
]:
|
|
||||||
settings_dirs.add(legacy_dir)
|
|
||||||
|
|
||||||
settings_names: set[str] = set()
|
settings_names: set[str] = set()
|
||||||
for directory in settings_dirs:
|
for directory in settings_dirs:
|
||||||
settings_names |= _collect_from(directory)
|
settings_names |= _collect_from(directory)
|
||||||
|
|
||||||
for name in sorted(settings_names):
|
# Also consider read-only defaults from core module and beamline plugin repositories
|
||||||
runtime_profile_candidates(name, namespace)
|
|
||||||
baseline_profile_candidates(name, namespace)
|
|
||||||
|
|
||||||
# Also consider read-only baselines from core module and beamline plugin repositories
|
|
||||||
read_only_sources: dict[str, tuple[str, str]] = {}
|
read_only_sources: dict[str, tuple[str, str]] = {}
|
||||||
sources: list[tuple[str, str | None]] = [
|
sources: list[tuple[str, str | None]] = [
|
||||||
("module", module_profiles_dir()),
|
("module", module_profiles_dir()),
|
||||||
@@ -523,17 +484,17 @@ def list_profiles(namespace: str | None = None) -> list[str]:
|
|||||||
read_only_sources.setdefault(name, (origin, os.path.join(directory, filename)))
|
read_only_sources.setdefault(name, (origin, os.path.join(directory, filename)))
|
||||||
|
|
||||||
for name, (_origin, src) in sorted(read_only_sources.items()):
|
for name, (_origin, src) in sorted(read_only_sources.items()):
|
||||||
# Ensure a copy in the namespace-specific settings baseline directory.
|
# Ensure a copy in the namespace-specific settings default directory
|
||||||
dst_baseline = baseline_profile_path(name, namespace)
|
dst_default = default_profile_path(name, namespace)
|
||||||
if not os.path.exists(dst_baseline):
|
if not os.path.exists(dst_default):
|
||||||
os.makedirs(os.path.dirname(dst_baseline), exist_ok=True)
|
os.makedirs(os.path.dirname(dst_default), exist_ok=True)
|
||||||
shutil.copy2(src, dst_baseline)
|
shutil.copyfile(src, dst_default)
|
||||||
# Ensure a runtime copy exists to allow edits in the writable settings area.
|
# Ensure a user copy exists to allow edits in the writable settings area
|
||||||
dst_runtime = runtime_profile_path(name, namespace)
|
dst_user = user_profile_path(name, namespace)
|
||||||
if not os.path.exists(dst_runtime):
|
if not os.path.exists(dst_user):
|
||||||
os.makedirs(os.path.dirname(dst_runtime), exist_ok=True)
|
os.makedirs(os.path.dirname(dst_user), exist_ok=True)
|
||||||
shutil.copy2(src, dst_runtime)
|
shutil.copyfile(src, dst_user)
|
||||||
s = open_runtime_settings(name, namespace)
|
s = open_user_settings(name, namespace)
|
||||||
if s.value(SETTINGS_KEYS["created_at"], "") == "":
|
if s.value(SETTINGS_KEYS["created_at"], "") == "":
|
||||||
s.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
s.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
||||||
|
|
||||||
@@ -543,34 +504,32 @@ def list_profiles(namespace: str | None = None) -> list[str]:
|
|||||||
return sorted(settings_names)
|
return sorted(settings_names)
|
||||||
|
|
||||||
|
|
||||||
def open_baseline_settings(name: str, namespace: str | None = None) -> QSettings:
|
def open_default_settings(name: str, namespace: str | None = None) -> QSettings:
|
||||||
"""
|
"""
|
||||||
Open (and create if necessary) the baseline profile settings file.
|
Open (and create if necessary) the default profile settings file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QSettings: Settings instance targeting the baseline profile file.
|
QSettings: Settings instance targeting the default profile file.
|
||||||
"""
|
"""
|
||||||
baseline_profile_candidates(name, namespace)
|
return QSettings(default_profile_path(name, namespace), QSettings.IniFormat)
|
||||||
return QSettings(baseline_profile_path(name, namespace), QSettings.IniFormat)
|
|
||||||
|
|
||||||
|
|
||||||
def open_runtime_settings(name: str, namespace: str | None = None) -> QSettings:
|
def open_user_settings(name: str, namespace: str | None = None) -> QSettings:
|
||||||
"""
|
"""
|
||||||
Open (and create if necessary) the runtime profile settings file.
|
Open (and create if necessary) the user profile settings file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QSettings: Settings instance targeting the runtime profile file.
|
QSettings: Settings instance targeting the user profile file.
|
||||||
"""
|
"""
|
||||||
runtime_profile_candidates(name, namespace)
|
return QSettings(user_profile_path(name, namespace), QSettings.IniFormat)
|
||||||
return QSettings(runtime_profile_path(name, namespace), QSettings.IniFormat)
|
|
||||||
|
|
||||||
|
|
||||||
def _app_settings() -> QSettings:
|
def _app_settings() -> QSettings:
|
||||||
@@ -800,26 +759,26 @@ def read_manifest(settings: QSettings) -> list[dict]:
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def restore_runtime_from_baseline(name: str, namespace: str | None = None) -> None:
|
def restore_user_from_default(name: str, namespace: str | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Copy the baseline profile to the runtime profile, preserving quick-select flag.
|
Copy the default profile to the user profile, preserving quick-select flag.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name(str): Profile name without extension.
|
name(str): Profile name without extension.
|
||||||
namespace(str | None, optional): Namespace label. Defaults to ``None``.
|
namespace(str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
"""
|
"""
|
||||||
src = None
|
src = None
|
||||||
for candidate in baseline_profile_candidates(name, namespace):
|
for candidate in default_profile_candidates(name, namespace):
|
||||||
if os.path.exists(candidate):
|
if os.path.exists(candidate):
|
||||||
src = candidate
|
src = candidate
|
||||||
break
|
break
|
||||||
if not src:
|
if not src:
|
||||||
return
|
return
|
||||||
dst = runtime_profile_path(name, namespace)
|
dst = user_profile_path(name, namespace)
|
||||||
preserve_quick_select = is_quick_select(name, namespace)
|
preserve_quick_select = is_quick_select(name, namespace)
|
||||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||||
shutil.copyfile(src, dst)
|
shutil.copyfile(src, dst)
|
||||||
s = open_runtime_settings(name, namespace)
|
s = open_user_settings(name, namespace)
|
||||||
if not s.value(SETTINGS_KEYS["created_at"], ""):
|
if not s.value(SETTINGS_KEYS["created_at"], ""):
|
||||||
s.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
s.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
|
||||||
if preserve_quick_select:
|
if preserve_quick_select:
|
||||||
@@ -837,9 +796,9 @@ def is_quick_select(name: str, namespace: str | None = None) -> bool:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if quick-select is enabled for the profile.
|
bool: True if quick-select is enabled for the profile.
|
||||||
"""
|
"""
|
||||||
s = _existing_runtime_settings(name, namespace)
|
s = _existing_user_settings(name, namespace)
|
||||||
if s is None:
|
if s is None:
|
||||||
s = _existing_baseline_settings(name, namespace)
|
s = _existing_default_settings(name, namespace)
|
||||||
if s is None:
|
if s is None:
|
||||||
return False
|
return False
|
||||||
return s.value(SETTINGS_KEYS["is_quick_select"], False, type=bool)
|
return s.value(SETTINGS_KEYS["is_quick_select"], False, type=bool)
|
||||||
@@ -854,13 +813,13 @@ def set_quick_select(name: str, enabled: bool, namespace: str | None = None) ->
|
|||||||
enabled(bool): True to enable quick-select, False to disable.
|
enabled(bool): True to enable quick-select, False to disable.
|
||||||
namespace(str | None, optional): Namespace label. Defaults to ``None``.
|
namespace(str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
"""
|
"""
|
||||||
s = open_runtime_settings(name, namespace)
|
s = open_user_settings(name, namespace)
|
||||||
s.setValue(SETTINGS_KEYS["is_quick_select"], bool(enabled))
|
s.setValue(SETTINGS_KEYS["is_quick_select"], bool(enabled))
|
||||||
|
|
||||||
|
|
||||||
def list_quick_profiles(namespace: str | None = None) -> list[str]:
|
def list_quick_profiles(namespace: str | None = None) -> list[str]:
|
||||||
"""
|
"""
|
||||||
List only profiles that have quick-select enabled (runtime wins over baseline).
|
List only profiles that have quick-select enabled (user wins over default).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace(str | None, optional): Namespace label. Defaults to ``None``.
|
namespace(str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
@@ -950,8 +909,8 @@ class ProfileInfo(BaseModel):
|
|||||||
is_quick_select: bool = False
|
is_quick_select: bool = False
|
||||||
widget_count: int = 0
|
widget_count: int = 0
|
||||||
size_kb: int = 0
|
size_kb: int = 0
|
||||||
runtime_path: str = ""
|
user_path: str = ""
|
||||||
baseline_path: str = ""
|
default_path: str = ""
|
||||||
origin: ProfileOrigin = "unknown"
|
origin: ProfileOrigin = "unknown"
|
||||||
is_read_only: bool = False
|
is_read_only: bool = False
|
||||||
|
|
||||||
@@ -965,19 +924,19 @@ def get_profile_info(name: str, namespace: str | None = None) -> ProfileInfo:
|
|||||||
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
namespace (str | None, optional): Namespace label. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ProfileInfo: Structured profile metadata, preferring the runtime copy when present.
|
ProfileInfo: Structured profile metadata, preferring the user copy when present.
|
||||||
"""
|
"""
|
||||||
runtime_paths = runtime_profile_candidates(name, namespace)
|
user_paths = user_profile_candidates(name, namespace)
|
||||||
baseline_paths = baseline_profile_candidates(name, namespace)
|
default_paths = default_profile_candidates(name, namespace)
|
||||||
r_path = next((p for p in runtime_paths if os.path.exists(p)), runtime_paths[0])
|
u_path = next((p for p in user_paths if os.path.exists(p)), user_paths[0])
|
||||||
b_path = next((p for p in baseline_paths if os.path.exists(p)), baseline_paths[0])
|
d_path = next((p for p in default_paths if os.path.exists(p)), default_paths[0])
|
||||||
origin = profile_origin(name, namespace)
|
origin = profile_origin(name, namespace)
|
||||||
read_only = origin in {"module", "plugin"}
|
read_only = origin in {"module", "plugin"}
|
||||||
prefer_runtime = os.path.exists(r_path)
|
prefer_user = os.path.exists(u_path)
|
||||||
if prefer_runtime:
|
if prefer_user:
|
||||||
s = QSettings(r_path, QSettings.IniFormat)
|
s = QSettings(u_path, QSettings.IniFormat)
|
||||||
elif os.path.exists(b_path):
|
elif os.path.exists(d_path):
|
||||||
s = QSettings(b_path, QSettings.IniFormat)
|
s = QSettings(d_path, QSettings.IniFormat)
|
||||||
else:
|
else:
|
||||||
s = None
|
s = None
|
||||||
if s is None:
|
if s is None:
|
||||||
@@ -998,14 +957,14 @@ def get_profile_info(name: str, namespace: str | None = None) -> ProfileInfo:
|
|||||||
is_quick_select=False,
|
is_quick_select=False,
|
||||||
widget_count=0,
|
widget_count=0,
|
||||||
size_kb=0,
|
size_kb=0,
|
||||||
runtime_path=r_path,
|
user_path=u_path,
|
||||||
baseline_path=b_path,
|
default_path=d_path,
|
||||||
origin=origin,
|
origin=origin,
|
||||||
is_read_only=read_only,
|
is_read_only=read_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
created = s.value(SETTINGS_KEYS["created_at"], "", type=str) or now_iso_utc()
|
created = s.value(SETTINGS_KEYS["created_at"], "", type=str) or now_iso_utc()
|
||||||
src_path = r_path if prefer_runtime else b_path
|
src_path = u_path if prefer_user else d_path
|
||||||
modified = _file_modified_iso(src_path)
|
modified = _file_modified_iso(src_path)
|
||||||
count = _manifest_count(s)
|
count = _manifest_count(s)
|
||||||
try:
|
try:
|
||||||
@@ -1031,8 +990,8 @@ def get_profile_info(name: str, namespace: str | None = None) -> ProfileInfo:
|
|||||||
is_quick_select=is_quick_select(name, namespace),
|
is_quick_select=is_quick_select(name, namespace),
|
||||||
widget_count=count,
|
widget_count=count,
|
||||||
size_kb=size_kb,
|
size_kb=size_kb,
|
||||||
runtime_path=r_path,
|
user_path=u_path,
|
||||||
baseline_path=b_path,
|
default_path=d_path,
|
||||||
origin=origin,
|
origin=origin,
|
||||||
is_read_only=read_only,
|
is_read_only=read_only,
|
||||||
)
|
)
|
||||||
@@ -1040,7 +999,7 @@ def get_profile_info(name: str, namespace: str | None = None) -> ProfileInfo:
|
|||||||
|
|
||||||
def load_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap | None:
|
def load_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap | None:
|
||||||
"""
|
"""
|
||||||
Load the stored screenshot pixmap for a profile from settings (runtime preferred).
|
Load the stored screenshot pixmap for a profile from settings (user preferred).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
@@ -1049,17 +1008,17 @@ def load_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap
|
|||||||
Returns:
|
Returns:
|
||||||
QPixmap | None: Screenshot pixmap or ``None`` if unavailable.
|
QPixmap | None: Screenshot pixmap or ``None`` if unavailable.
|
||||||
"""
|
"""
|
||||||
s = _existing_runtime_settings(name, namespace)
|
s = _existing_user_settings(name, namespace)
|
||||||
if s is None:
|
if s is None:
|
||||||
s = _existing_baseline_settings(name, namespace)
|
s = _existing_default_settings(name, namespace)
|
||||||
if s is None:
|
if s is None:
|
||||||
return None
|
return None
|
||||||
return _load_screenshot_from_settings(s)
|
return _load_screenshot_from_settings(s)
|
||||||
|
|
||||||
|
|
||||||
def load_baseline_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap | None:
|
def load_default_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap | None:
|
||||||
"""
|
"""
|
||||||
Load the screenshot from the baseline profile copy, if available.
|
Load the screenshot from the default profile copy, if available.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
@@ -1068,15 +1027,15 @@ def load_baseline_profile_screenshot(name: str, namespace: str | None = None) ->
|
|||||||
Returns:
|
Returns:
|
||||||
QPixmap | None: Screenshot pixmap or ``None`` if unavailable.
|
QPixmap | None: Screenshot pixmap or ``None`` if unavailable.
|
||||||
"""
|
"""
|
||||||
s = _existing_baseline_settings(name, namespace)
|
s = _existing_default_settings(name, namespace)
|
||||||
if s is None:
|
if s is None:
|
||||||
return None
|
return None
|
||||||
return _load_screenshot_from_settings(s)
|
return _load_screenshot_from_settings(s)
|
||||||
|
|
||||||
|
|
||||||
def load_runtime_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap | None:
|
def load_user_profile_screenshot(name: str, namespace: str | None = None) -> QPixmap | None:
|
||||||
"""
|
"""
|
||||||
Load the screenshot from the runtime profile copy, if available.
|
Load the screenshot from the user profile copy, if available.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Profile name without extension.
|
name (str): Profile name without extension.
|
||||||
@@ -1085,7 +1044,7 @@ def load_runtime_profile_screenshot(name: str, namespace: str | None = None) ->
|
|||||||
Returns:
|
Returns:
|
||||||
QPixmap | None: Screenshot pixmap or ``None`` if unavailable.
|
QPixmap | None: Screenshot pixmap or ``None`` if unavailable.
|
||||||
"""
|
"""
|
||||||
s = _existing_runtime_settings(name, namespace)
|
s = _existing_user_settings(name, namespace)
|
||||||
if s is None:
|
if s is None:
|
||||||
return None
|
return None
|
||||||
return _load_screenshot_from_settings(s)
|
return _load_screenshot_from_settings(s)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class SaveProfileDialog(QDialog):
|
|||||||
self,
|
self,
|
||||||
"Read-only profile",
|
"Read-only profile",
|
||||||
(
|
(
|
||||||
f"'{name}' is a baseline profile provided by {provider} and cannot be overwritten.\n"
|
f"'{name}' is a default profile provided by {provider} and cannot be overwritten.\n"
|
||||||
"Please choose a different name."
|
"Please choose a different name."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -179,7 +179,7 @@ class SaveProfileDialog(QDialog):
|
|||||||
"Overwrite profile",
|
"Overwrite profile",
|
||||||
(
|
(
|
||||||
f"A profile named '{name}' already exists.\n\n"
|
f"A profile named '{name}' already exists.\n\n"
|
||||||
"Overwriting will update both the runtime profile and its restore baseline.\n"
|
"Overwriting will update both the saved profile and its restore default.\n"
|
||||||
"Do you want to continue?"
|
"Do you want to continue?"
|
||||||
),
|
),
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
@@ -257,24 +257,21 @@ class PreviewPanel(QGroupBox):
|
|||||||
|
|
||||||
class RestoreProfileDialog(QDialog):
|
class RestoreProfileDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
Confirmation dialog that previews the current runtime screenshot against the baseline.
|
Confirmation dialog that previews the current profile screenshot against the default baseline.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, parent: QWidget | None, current_pixmap: QPixmap | None, default_pixmap: QPixmap | None
|
||||||
parent: QWidget | None,
|
|
||||||
current_pixmap: QPixmap | None,
|
|
||||||
baseline_pixmap: QPixmap | None,
|
|
||||||
):
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Restore Profile to Baseline")
|
self.setWindowTitle("Restore Profile to Default")
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self.resize(880, 480)
|
self.resize(880, 480)
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
info_label = QLabel(
|
info_label = QLabel(
|
||||||
"Restoring will discard your runtime layout and replace it with the baseline profile."
|
"Restoring will discard your custom layout and replace it with the default profile."
|
||||||
)
|
)
|
||||||
info_label.setWordWrap(True)
|
info_label.setWordWrap(True)
|
||||||
layout.addWidget(info_label)
|
layout.addWidget(info_label)
|
||||||
@@ -283,7 +280,7 @@ class RestoreProfileDialog(QDialog):
|
|||||||
layout.addLayout(preview_row)
|
layout.addLayout(preview_row)
|
||||||
|
|
||||||
current_preview = PreviewPanel("Current", current_pixmap, self)
|
current_preview = PreviewPanel("Current", current_pixmap, self)
|
||||||
baseline_preview = PreviewPanel("Baseline", baseline_pixmap, self)
|
default_preview = PreviewPanel("Default", default_pixmap, self)
|
||||||
|
|
||||||
# Equal expansion left/right
|
# Equal expansion left/right
|
||||||
preview_row.addWidget(current_preview, 1)
|
preview_row.addWidget(current_preview, 1)
|
||||||
@@ -295,7 +292,7 @@ class RestoreProfileDialog(QDialog):
|
|||||||
arrow_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
arrow_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
preview_row.addWidget(arrow_label)
|
preview_row.addWidget(arrow_label)
|
||||||
|
|
||||||
preview_row.addWidget(baseline_preview, 1)
|
preview_row.addWidget(default_preview, 1)
|
||||||
|
|
||||||
# Enforce equal stretch for both previews
|
# Enforce equal stretch for both previews
|
||||||
preview_row.setStretch(0, 1)
|
preview_row.setStretch(0, 1)
|
||||||
@@ -303,7 +300,7 @@ class RestoreProfileDialog(QDialog):
|
|||||||
preview_row.setStretch(2, 1)
|
preview_row.setStretch(2, 1)
|
||||||
|
|
||||||
warn_label = QLabel(
|
warn_label = QLabel(
|
||||||
"This action cannot be undone. Do you want to restore the baseline layout now?"
|
"This action cannot be undone. Do you want to restore the default layout now?"
|
||||||
)
|
)
|
||||||
warn_label.setWordWrap(True)
|
warn_label.setWordWrap(True)
|
||||||
layout.addWidget(warn_label)
|
layout.addWidget(warn_label)
|
||||||
@@ -327,7 +324,7 @@ class RestoreProfileDialog(QDialog):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def confirm(
|
def confirm(
|
||||||
parent: QWidget | None, current_pixmap: QPixmap | None, baseline_pixmap: QPixmap | None
|
parent: QWidget | None, current_pixmap: QPixmap | None, default_pixmap: QPixmap | None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
dialog = RestoreProfileDialog(parent, current_pixmap, baseline_pixmap)
|
dialog = RestoreProfileDialog(parent, current_pixmap, default_pixmap)
|
||||||
return dialog.exec() == QDialog.Accepted
|
return dialog.exec() == QDialog.Accepted
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class WorkSpaceManager(BECWidget, QWidget):
|
|||||||
HEADERS = ["Actions", "Profile", "Author"]
|
HEADERS = ["Actions", "Profile", "Author"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent=None, target_widget=None, active_profile: str | None = None, **kwargs
|
self, parent=None, target_widget=None, default_profile: str | None = None, **kwargs
|
||||||
):
|
):
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(parent=parent, **kwargs)
|
||||||
self.target_widget = target_widget
|
self.target_widget = target_widget
|
||||||
@@ -59,13 +59,13 @@ class WorkSpaceManager(BECWidget, QWidget):
|
|||||||
self._init_ui()
|
self._init_ui()
|
||||||
if self.target_widget is not None and hasattr(self.target_widget, "profile_changed"):
|
if self.target_widget is not None and hasattr(self.target_widget, "profile_changed"):
|
||||||
self.target_widget.profile_changed.connect(self.on_profile_changed)
|
self.target_widget.profile_changed.connect(self.on_profile_changed)
|
||||||
if active_profile is not None:
|
if default_profile is not None:
|
||||||
self._select_by_name(active_profile)
|
self._select_by_name(default_profile)
|
||||||
self._show_profile_details(active_profile)
|
self._show_profile_details(default_profile)
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
self.root_layout = QHBoxLayout(self)
|
self.root_layout = QHBoxLayout(self)
|
||||||
self.splitter = QSplitter(Qt.Orientation.Horizontal, self)
|
self.splitter = QSplitter(Qt.Horizontal, self)
|
||||||
self.root_layout.addWidget(self.splitter)
|
self.root_layout.addWidget(self.splitter)
|
||||||
|
|
||||||
# Init components
|
# Init components
|
||||||
@@ -89,9 +89,7 @@ class WorkSpaceManager(BECWidget, QWidget):
|
|||||||
left_panel.setMinimumWidth(220)
|
left_panel.setMinimumWidth(220)
|
||||||
|
|
||||||
# Make the screenshot preview expand to fill remaining space
|
# Make the screenshot preview expand to fill remaining space
|
||||||
self.screenshot_label.setSizePolicy(
|
self.screenshot_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
||||||
)
|
|
||||||
|
|
||||||
self.right_box = QGroupBox("Profile Screenshot Preview", self)
|
self.right_box = QGroupBox("Profile Screenshot Preview", self)
|
||||||
right_col = QVBoxLayout(self.right_box)
|
right_col = QVBoxLayout(self.right_box)
|
||||||
@@ -252,8 +250,8 @@ class WorkSpaceManager(BECWidget, QWidget):
|
|||||||
("Quick select", "Yes" if info.is_quick_select else "No"),
|
("Quick select", "Yes" if info.is_quick_select else "No"),
|
||||||
("Widgets", str(info.widget_count)),
|
("Widgets", str(info.widget_count)),
|
||||||
("Size (KB)", str(info.size_kb)),
|
("Size (KB)", str(info.size_kb)),
|
||||||
("Runtime path", info.runtime_path or ""),
|
("User path", info.user_path or ""),
|
||||||
("Baseline path", info.baseline_path or ""),
|
("Default path", info.default_path or ""),
|
||||||
]
|
]
|
||||||
for k, v in entries:
|
for k, v in entries:
|
||||||
self.profile_details_tree.addTopLevelItem(QTreeWidgetItem([k, v]))
|
self.profile_details_tree.addTopLevelItem(QTreeWidgetItem([k, v]))
|
||||||
|
|||||||
@@ -24,9 +24,19 @@ class ProfileComboBox(QComboBox):
|
|||||||
def set_quick_profile_provider(self, provider: Callable[[], list[str]]) -> None:
|
def set_quick_profile_provider(self, provider: Callable[[], list[str]]) -> None:
|
||||||
self._quick_provider = provider
|
self._quick_provider = provider
|
||||||
|
|
||||||
def _refresh_profiles(
|
def refresh_profiles(
|
||||||
self, current_text: str, active_profile: str | None = None, show_empty_profile: bool = False
|
self, active_profile: str | None = None, show_empty_profile: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Refresh the profile list and ensure the active profile is visible.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
active_profile(str | None): The currently active profile name.
|
||||||
|
show_empty_profile(bool): If True, show an explicit empty unsaved workspace entry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_text = active_profile or self.currentText()
|
||||||
|
self.blockSignals(True)
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
quick_profiles = self._quick_provider()
|
quick_profiles = self._quick_provider()
|
||||||
@@ -93,6 +103,7 @@ class ProfileComboBox(QComboBox):
|
|||||||
if index >= 0:
|
if index >= 0:
|
||||||
self.setCurrentIndex(index)
|
self.setCurrentIndex(index)
|
||||||
|
|
||||||
|
self.blockSignals(False)
|
||||||
if active_profile and self.currentText() != active_profile:
|
if active_profile and self.currentText() != active_profile:
|
||||||
idx = self.findText(active_profile)
|
idx = self.findText(active_profile)
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
@@ -104,24 +115,6 @@ class ProfileComboBox(QComboBox):
|
|||||||
else:
|
else:
|
||||||
self.setToolTip("")
|
self.setToolTip("")
|
||||||
|
|
||||||
def refresh_profiles(
|
|
||||||
self, active_profile: str | None = None, show_empty_profile: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Refresh the profile list and ensure the active profile is visible.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
active_profile(str | None): The currently active profile name.
|
|
||||||
show_empty_profile(bool): If True, show an explicit empty unsaved workspace entry.
|
|
||||||
"""
|
|
||||||
|
|
||||||
current_text = active_profile or self.currentText()
|
|
||||||
was_blocked = self.blockSignals(True)
|
|
||||||
try:
|
|
||||||
self._refresh_profiles(current_text, active_profile, show_empty_profile)
|
|
||||||
finally:
|
|
||||||
self.blockSignals(was_blocked)
|
|
||||||
|
|
||||||
|
|
||||||
def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -> ToolbarBundle:
|
def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -> ToolbarBundle:
|
||||||
"""
|
"""
|
||||||
@@ -129,7 +122,6 @@ def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
components (ToolbarComponents): The components to be added to the bundle.
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
enable_tools(bool): If True, show the workspace management tools; otherwise, only show the profile combo.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ToolbarBundle: The workspace toolbar bundle.
|
ToolbarBundle: The workspace toolbar bundle.
|
||||||
@@ -151,15 +143,15 @@ def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -
|
|||||||
components.get_action("save_workspace").action.setVisible(enable_tools)
|
components.get_action("save_workspace").action.setVisible(enable_tools)
|
||||||
|
|
||||||
components.add_safe(
|
components.add_safe(
|
||||||
"reset_baseline_workspace",
|
"reset_default_workspace",
|
||||||
MaterialIconAction(
|
MaterialIconAction(
|
||||||
icon_name="undo",
|
icon_name="undo",
|
||||||
tooltip="Restore Baseline Profile",
|
tooltip="Refresh Current Workspace",
|
||||||
checkable=False,
|
checkable=False,
|
||||||
parent=components.toolbar,
|
parent=components.toolbar,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
components.get_action("reset_baseline_workspace").action.setVisible(enable_tools)
|
components.get_action("reset_default_workspace").action.setVisible(enable_tools)
|
||||||
|
|
||||||
components.add_safe(
|
components.add_safe(
|
||||||
"manage_workspaces",
|
"manage_workspaces",
|
||||||
@@ -172,7 +164,7 @@ def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -
|
|||||||
bundle = ToolbarBundle("workspace", components)
|
bundle = ToolbarBundle("workspace", components)
|
||||||
bundle.add_action("workspace_combo")
|
bundle.add_action("workspace_combo")
|
||||||
bundle.add_action("save_workspace")
|
bundle.add_action("save_workspace")
|
||||||
bundle.add_action("reset_baseline_workspace")
|
bundle.add_action("reset_default_workspace")
|
||||||
bundle.add_action("manage_workspaces")
|
bundle.add_action("manage_workspaces")
|
||||||
return bundle
|
return bundle
|
||||||
|
|
||||||
@@ -202,9 +194,9 @@ class WorkspaceConnection(BundleConnection):
|
|||||||
self.target_widget.load_profile
|
self.target_widget.load_profile
|
||||||
)
|
)
|
||||||
|
|
||||||
reset_action = self.components.get_action("reset_baseline_workspace").action
|
reset_action = self.components.get_action("reset_default_workspace").action
|
||||||
if reset_action.isVisible():
|
if reset_action.isVisible():
|
||||||
reset_action.triggered.connect(self._reset_workspace_to_baseline)
|
reset_action.triggered.connect(self._reset_workspace_to_default)
|
||||||
|
|
||||||
manage_action = self.components.get_action("manage_workspaces").action
|
manage_action = self.components.get_action("manage_workspaces").action
|
||||||
if manage_action.isVisible():
|
if manage_action.isVisible():
|
||||||
@@ -221,9 +213,9 @@ class WorkspaceConnection(BundleConnection):
|
|||||||
self.target_widget.load_profile
|
self.target_widget.load_profile
|
||||||
)
|
)
|
||||||
|
|
||||||
reset_action = self.components.get_action("reset_baseline_workspace").action
|
reset_action = self.components.get_action("reset_default_workspace").action
|
||||||
if reset_action.isVisible():
|
if reset_action.isVisible():
|
||||||
reset_action.triggered.disconnect(self._reset_workspace_to_baseline)
|
reset_action.triggered.disconnect(self._reset_workspace_to_default)
|
||||||
|
|
||||||
manage_action = self.components.get_action("manage_workspaces").action
|
manage_action = self.components.get_action("manage_workspaces").action
|
||||||
if manage_action.isVisible():
|
if manage_action.isVisible():
|
||||||
@@ -231,8 +223,8 @@ class WorkspaceConnection(BundleConnection):
|
|||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def _reset_workspace_to_baseline(self):
|
def _reset_workspace_to_default(self):
|
||||||
"""
|
"""
|
||||||
Refreshes the current workspace.
|
Refreshes the current workspace.
|
||||||
"""
|
"""
|
||||||
self.target_widget.restore_baseline_profile(show_dialog=True)
|
self.target_widget.restore_user_profile_from_default()
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import webbrowser
|
|||||||
class BECWebLinksMixin:
|
class BECWebLinksMixin:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def open_bec_docs():
|
def open_bec_docs():
|
||||||
webbrowser.open("https://bec-project.github.io/bec_docs/")
|
webbrowser.open("https://beamline-experiment-control.readthedocs.io/en/latest/")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def open_bec_widgets_docs():
|
||||||
|
webbrowser.open("https://bec.readthedocs.io/projects/bec-widgets/en/latest/")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def open_bec_bug_report():
|
def open_bec_bug_report():
|
||||||
|
|||||||
@@ -355,13 +355,17 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
|
|
||||||
bec_docs = QAction("BEC Docs", self)
|
bec_docs = QAction("BEC Docs", self)
|
||||||
bec_docs.setIcon(help_icon)
|
bec_docs.setIcon(help_icon)
|
||||||
|
widgets_docs = QAction("BEC Widgets Docs", self)
|
||||||
|
widgets_docs.setIcon(help_icon)
|
||||||
bug_report = QAction("Bug Report", self)
|
bug_report = QAction("Bug Report", self)
|
||||||
bug_report.setIcon(bug_icon)
|
bug_report.setIcon(bug_icon)
|
||||||
|
|
||||||
bec_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
|
bec_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
|
||||||
|
widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
|
||||||
bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
|
bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
|
||||||
|
|
||||||
help_menu.addAction(bec_docs)
|
help_menu.addAction(bec_docs)
|
||||||
|
help_menu.addAction(widgets_docs)
|
||||||
help_menu.addAction(bug_report)
|
help_menu.addAction(bug_report)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "bec_widgets"
|
name = "bec_widgets"
|
||||||
version = "3.8.0"
|
version = "3.7.1"
|
||||||
description = "BEC Widgets"
|
description = "BEC Widgets"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
|||||||
@@ -59,4 +59,5 @@ def test_run_line_scan_with_parameters_e2e(scan_control, bec_client_lib, qtbot):
|
|||||||
last_scan = queue.scan_storage.storage[-1]
|
last_scan = queue.scan_storage.storage[-1]
|
||||||
assert last_scan.status_message.info["scan_name"] == scan_name
|
assert last_scan.status_message.info["scan_name"] == scan_name
|
||||||
assert last_scan.status_message.info["exp_time"] == kwargs["exp_time"]
|
assert last_scan.status_message.info["exp_time"] == kwargs["exp_time"]
|
||||||
|
assert last_scan.status_message.info["scan_motors"] == [args["device"]]
|
||||||
assert last_scan.status_message.info["num_points"] == kwargs["steps"]
|
assert last_scan.status_message.info["num_points"] == kwargs["steps"]
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ def test_scan_metadata_for_custom_scan(
|
|||||||
last_scan = queue.scan_storage.storage[-1]
|
last_scan = queue.scan_storage.storage[-1]
|
||||||
assert last_scan.status_message.info["scan_name"] == scan_name
|
assert last_scan.status_message.info["scan_name"] == scan_name
|
||||||
assert last_scan.status_message.info["exp_time"] == kwargs["exp_time"]
|
assert last_scan.status_message.info["exp_time"] == kwargs["exp_time"]
|
||||||
|
assert last_scan.status_message.info["scan_motors"] == [args["device"]]
|
||||||
assert last_scan.status_message.info["num_points"] == kwargs["steps"]
|
assert last_scan.status_message.info["num_points"] == kwargs["steps"]
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ def bec_queue_msg_full():
|
|||||||
},
|
},
|
||||||
"report_instructions": [{"scan_progress": 20}],
|
"report_instructions": [{"scan_progress": 20}],
|
||||||
"scan_id": "2d704cc3-c172-404c-866d-608ce09fce40",
|
"scan_id": "2d704cc3-c172-404c-866d-608ce09fce40",
|
||||||
|
"scan_motors": ["samx"],
|
||||||
"scan_number": 1289,
|
"scan_number": 1289,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ class TestDeviceManagerViewDialogs:
|
|||||||
group_combo: QtWidgets.QComboBox = dialog._control_widgets["group_combo"]
|
group_combo: QtWidgets.QComboBox = dialog._control_widgets["group_combo"]
|
||||||
assert group_combo.count() == len(OPHYD_DEVICE_TEMPLATES)
|
assert group_combo.count() == len(OPHYD_DEVICE_TEMPLATES)
|
||||||
|
|
||||||
|
# Test select a group from available templates
|
||||||
variant_combo = dialog._control_widgets["variant_combo"]
|
variant_combo = dialog._control_widgets["variant_combo"]
|
||||||
|
assert variant_combo.isEnabled() is False
|
||||||
|
|
||||||
with qtbot.waitSignal(group_combo.currentTextChanged):
|
with qtbot.waitSignal(group_combo.currentTextChanged):
|
||||||
epics_signal_index = group_combo.findText("EpicsSignal")
|
epics_signal_index = group_combo.findText("EpicsSignal")
|
||||||
group_combo.setCurrentIndex(epics_signal_index) # Select "EpicsSignal" group
|
group_combo.setCurrentIndex(epics_signal_index) # Select "EpicsSignal" group
|
||||||
@@ -232,7 +235,7 @@ class TestDeviceManagerViewDialogs:
|
|||||||
sample_config = {
|
sample_config = {
|
||||||
"name": "TestDevice",
|
"name": "TestDevice",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"deviceClass": "ophyd_devices.EpicsSignal",
|
"deviceClass": "ophyd.EpicsSignal",
|
||||||
"readoutPriority": "baseline",
|
"readoutPriority": "baseline",
|
||||||
"deviceConfig": {"read_pv": "X25DA-ES1-MOT:GET"},
|
"deviceConfig": {"read_pv": "X25DA-ES1-MOT:GET"},
|
||||||
}
|
}
|
||||||
@@ -245,7 +248,7 @@ class TestDeviceManagerViewDialogs:
|
|||||||
assert variant_combo.currentText() == "EpicsSignal"
|
assert variant_combo.currentText() == "EpicsSignal"
|
||||||
config = dialog._device_config_template.get_config_fields()
|
config = dialog._device_config_template.get_config_fields()
|
||||||
assert config["name"] == "TestDevice"
|
assert config["name"] == "TestDevice"
|
||||||
assert config["deviceClass"] == "ophyd_devices.EpicsSignal"
|
assert config["deviceClass"] == "ophyd.EpicsSignal"
|
||||||
assert config["deviceConfig"]["read_pv"] == "X25DA-ES1-MOT:GET"
|
assert config["deviceConfig"]["read_pv"] == "X25DA-ES1-MOT:GET"
|
||||||
|
|
||||||
# Test now to add the device config with different validation results
|
# Test now to add the device config with different validation results
|
||||||
|
|||||||
+180
-312
@@ -19,19 +19,19 @@ from bec_widgets.widgets.containers.dock_area.basic_dock_area import (
|
|||||||
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea, SaveProfileDialog
|
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea, SaveProfileDialog
|
||||||
from bec_widgets.widgets.containers.dock_area.profile_utils import (
|
from bec_widgets.widgets.containers.dock_area.profile_utils import (
|
||||||
SETTINGS_KEYS,
|
SETTINGS_KEYS,
|
||||||
baseline_profile_path,
|
default_profile_path,
|
||||||
get_profile_info,
|
get_profile_info,
|
||||||
is_profile_read_only,
|
is_profile_read_only,
|
||||||
is_quick_select,
|
is_quick_select,
|
||||||
list_profiles,
|
list_profiles,
|
||||||
load_baseline_profile_screenshot,
|
load_default_profile_screenshot,
|
||||||
load_runtime_profile_screenshot,
|
load_user_profile_screenshot,
|
||||||
open_baseline_settings,
|
open_default_settings,
|
||||||
open_runtime_settings,
|
open_user_settings,
|
||||||
read_manifest,
|
read_manifest,
|
||||||
restore_runtime_from_baseline,
|
restore_user_from_default,
|
||||||
runtime_profile_path,
|
|
||||||
set_quick_select,
|
set_quick_select,
|
||||||
|
user_profile_path,
|
||||||
write_manifest,
|
write_manifest,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.containers.dock_area.settings.dialogs import (
|
from bec_widgets.widgets.containers.dock_area.settings.dialogs import (
|
||||||
@@ -188,17 +188,17 @@ class _NamespaceProfiles:
|
|||||||
def __init__(self, widget: BECDockArea):
|
def __init__(self, widget: BECDockArea):
|
||||||
self.namespace = widget.profile_namespace
|
self.namespace = widget.profile_namespace
|
||||||
|
|
||||||
def open_runtime(self, name: str):
|
def open_user(self, name: str):
|
||||||
return open_runtime_settings(name, namespace=self.namespace)
|
return open_user_settings(name, namespace=self.namespace)
|
||||||
|
|
||||||
def open_baseline(self, name: str):
|
def open_default(self, name: str):
|
||||||
return open_baseline_settings(name, namespace=self.namespace)
|
return open_default_settings(name, namespace=self.namespace)
|
||||||
|
|
||||||
def runtime_path(self, name: str) -> str:
|
def user_path(self, name: str) -> str:
|
||||||
return runtime_profile_path(name, namespace=self.namespace)
|
return user_profile_path(name, namespace=self.namespace)
|
||||||
|
|
||||||
def baseline_path(self, name: str) -> str:
|
def default_path(self, name: str) -> str:
|
||||||
return baseline_profile_path(name, namespace=self.namespace)
|
return default_profile_path(name, namespace=self.namespace)
|
||||||
|
|
||||||
def list_profiles(self) -> list[str]:
|
def list_profiles(self) -> list[str]:
|
||||||
return list_profiles(namespace=self.namespace)
|
return list_profiles(namespace=self.namespace)
|
||||||
@@ -615,6 +615,35 @@ class TestBasicDockArea:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdvancedDockAreaInit:
|
||||||
|
"""Test initialization and basic properties."""
|
||||||
|
|
||||||
|
def test_init(self, advanced_dock_area):
|
||||||
|
assert advanced_dock_area is not None
|
||||||
|
assert isinstance(advanced_dock_area, BECDockArea)
|
||||||
|
assert advanced_dock_area.mode == "creator"
|
||||||
|
assert hasattr(advanced_dock_area, "dock_manager")
|
||||||
|
assert hasattr(advanced_dock_area, "toolbar")
|
||||||
|
assert hasattr(advanced_dock_area, "dark_mode_button")
|
||||||
|
assert hasattr(advanced_dock_area, "state_manager")
|
||||||
|
|
||||||
|
def test_rpc_and_plugin_flags(self):
|
||||||
|
assert BECDockArea.RPC is True
|
||||||
|
assert BECDockArea.PLUGIN is False
|
||||||
|
|
||||||
|
def test_user_access_list(self):
|
||||||
|
expected_methods = [
|
||||||
|
"new",
|
||||||
|
"widget_map",
|
||||||
|
"widget_list",
|
||||||
|
"workspace_is_locked",
|
||||||
|
"attach_all",
|
||||||
|
"delete_all",
|
||||||
|
]
|
||||||
|
for method in expected_methods:
|
||||||
|
assert method in BECDockArea.USER_ACCESS
|
||||||
|
|
||||||
|
|
||||||
class TestDockManagement:
|
class TestDockManagement:
|
||||||
"""Test dock creation, management, and manipulation."""
|
"""Test dock creation, management, and manipulation."""
|
||||||
|
|
||||||
@@ -822,6 +851,16 @@ class TestDeveloperMode:
|
|||||||
class TestToolbarFunctionality:
|
class TestToolbarFunctionality:
|
||||||
"""Test toolbar setup and functionality."""
|
"""Test toolbar setup and functionality."""
|
||||||
|
|
||||||
|
def test_toolbar_setup(self, advanced_dock_area):
|
||||||
|
"""Test toolbar is properly set up."""
|
||||||
|
assert hasattr(advanced_dock_area, "toolbar")
|
||||||
|
assert hasattr(advanced_dock_area, "_ACTION_MAPPINGS")
|
||||||
|
|
||||||
|
# Check that action mappings are properly set
|
||||||
|
assert "menu_plots" in advanced_dock_area._ACTION_MAPPINGS
|
||||||
|
assert "menu_devices" in advanced_dock_area._ACTION_MAPPINGS
|
||||||
|
assert "menu_utils" in advanced_dock_area._ACTION_MAPPINGS
|
||||||
|
|
||||||
def test_toolbar_plot_actions(self, advanced_dock_area):
|
def test_toolbar_plot_actions(self, advanced_dock_area):
|
||||||
"""Test plot toolbar actions trigger widget creation."""
|
"""Test plot toolbar actions trigger widget creation."""
|
||||||
plot_actions = [
|
plot_actions = [
|
||||||
@@ -907,7 +946,7 @@ class TestToolbarFunctionality:
|
|||||||
|
|
||||||
def test_load_profile_restores_floating_dock(self, advanced_dock_area, qtbot):
|
def test_load_profile_restores_floating_dock(self, advanced_dock_area, qtbot):
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
settings = helper.open_runtime("floating_profile")
|
settings = helper.open_user("floating_profile")
|
||||||
settings.clear()
|
settings.clear()
|
||||||
|
|
||||||
settings.setValue("profile/created_at", "2025-11-23T00:00:00Z")
|
settings.setValue("profile/created_at", "2025-11-23T00:00:00Z")
|
||||||
@@ -1176,6 +1215,18 @@ class TestPreviewPanel:
|
|||||||
assert "No preview available" in panel.image_label.text()
|
assert "No preview available" in panel.image_label.text()
|
||||||
|
|
||||||
|
|
||||||
|
class TestRestoreProfileDialog:
|
||||||
|
"""Test restore dialog confirmation flow."""
|
||||||
|
|
||||||
|
def test_confirm_accepts(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(RestoreProfileDialog, "exec", lambda self: QDialog.Accepted)
|
||||||
|
assert RestoreProfileDialog.confirm(None, QPixmap(), QPixmap()) is True
|
||||||
|
|
||||||
|
def test_confirm_rejects(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(RestoreProfileDialog, "exec", lambda self: QDialog.Rejected)
|
||||||
|
assert RestoreProfileDialog.confirm(None, QPixmap(), QPixmap()) is False
|
||||||
|
|
||||||
|
|
||||||
class TestProfileInfoAndScreenshots:
|
class TestProfileInfoAndScreenshots:
|
||||||
"""Tests for profile utilities metadata and screenshot helpers."""
|
"""Tests for profile utilities metadata and screenshot helpers."""
|
||||||
|
|
||||||
@@ -1195,9 +1246,9 @@ class TestProfileInfoAndScreenshots:
|
|||||||
settings.endArray()
|
settings.endArray()
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
def test_get_profile_info_runtime_origin(self, temp_profile_dir):
|
def test_get_profile_info_user_origin(self, temp_profile_dir):
|
||||||
name = "info_runtime"
|
name = "info_user"
|
||||||
settings = open_runtime_settings(name)
|
settings = open_user_settings(name)
|
||||||
settings.setValue(profile_utils.SETTINGS_KEYS["created_at"], "2023-01-01T00:00:00Z")
|
settings.setValue(profile_utils.SETTINGS_KEYS["created_at"], "2023-01-01T00:00:00Z")
|
||||||
settings.setValue("profile/author", "Custom")
|
settings.setValue("profile/author", "Custom")
|
||||||
set_quick_select(name, True)
|
set_quick_select(name, True)
|
||||||
@@ -1211,22 +1262,22 @@ class TestProfileInfoAndScreenshots:
|
|||||||
assert info.is_quick_select is True
|
assert info.is_quick_select is True
|
||||||
assert info.widget_count == 3
|
assert info.widget_count == 3
|
||||||
assert info.author == "User"
|
assert info.author == "User"
|
||||||
assert info.runtime_path.endswith(f"{name}.ini")
|
assert info.user_path.endswith(f"{name}.ini")
|
||||||
assert info.size_kb >= 0
|
assert info.size_kb >= 0
|
||||||
|
|
||||||
def test_get_profile_info_baseline_only(self, temp_profile_dir):
|
def test_get_profile_info_default_only(self, temp_profile_dir):
|
||||||
name = "info_baseline"
|
name = "info_default"
|
||||||
settings = open_baseline_settings(name)
|
settings = open_default_settings(name)
|
||||||
self._write_manifest(settings, count=1)
|
self._write_manifest(settings, count=1)
|
||||||
|
|
||||||
runtime_path = runtime_profile_path(name)
|
user_path = user_profile_path(name)
|
||||||
if os.path.exists(runtime_path):
|
if os.path.exists(user_path):
|
||||||
os.remove(runtime_path)
|
os.remove(user_path)
|
||||||
|
|
||||||
info = get_profile_info(name)
|
info = get_profile_info(name)
|
||||||
|
|
||||||
assert info.origin == "settings"
|
assert info.origin == "settings"
|
||||||
assert info.baseline_path.endswith(f"{name}.ini")
|
assert info.user_path.endswith(f"{name}.ini")
|
||||||
assert info.widget_count == 1
|
assert info.widget_count == 1
|
||||||
|
|
||||||
def test_get_profile_info_module_readonly(self, module_profile_factory):
|
def test_get_profile_info_module_readonly(self, module_profile_factory):
|
||||||
@@ -1238,10 +1289,10 @@ class TestProfileInfoAndScreenshots:
|
|||||||
|
|
||||||
def test_get_profile_info_unknown_profile(self):
|
def test_get_profile_info_unknown_profile(self):
|
||||||
name = "nonexistent_profile"
|
name = "nonexistent_profile"
|
||||||
if os.path.exists(runtime_profile_path(name)):
|
if os.path.exists(user_profile_path(name)):
|
||||||
os.remove(runtime_profile_path(name))
|
os.remove(user_profile_path(name))
|
||||||
if os.path.exists(baseline_profile_path(name)):
|
if os.path.exists(default_profile_path(name)):
|
||||||
os.remove(baseline_profile_path(name))
|
os.remove(default_profile_path(name))
|
||||||
|
|
||||||
info = get_profile_info(name)
|
info = get_profile_info(name)
|
||||||
|
|
||||||
@@ -1249,29 +1300,29 @@ class TestProfileInfoAndScreenshots:
|
|||||||
assert info.is_read_only is False
|
assert info.is_read_only is False
|
||||||
assert info.widget_count == 0
|
assert info.widget_count == 0
|
||||||
|
|
||||||
def test_load_runtime_profile_screenshot(self, temp_profile_dir):
|
def test_load_user_profile_screenshot(self, temp_profile_dir):
|
||||||
name = "runtime_screenshot"
|
name = "user_screenshot"
|
||||||
settings = open_runtime_settings(name)
|
settings = open_user_settings(name)
|
||||||
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES)
|
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES)
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
pix = load_runtime_profile_screenshot(name)
|
pix = load_user_profile_screenshot(name)
|
||||||
|
|
||||||
assert pix is not None and not pix.isNull()
|
assert pix is not None and not pix.isNull()
|
||||||
|
|
||||||
def test_load_baseline_profile_screenshot(self, temp_profile_dir):
|
def test_load_default_profile_screenshot(self, temp_profile_dir):
|
||||||
name = "baseline_screenshot"
|
name = "default_screenshot"
|
||||||
settings = open_baseline_settings(name)
|
settings = open_default_settings(name)
|
||||||
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES)
|
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES)
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
pix = load_baseline_profile_screenshot(name)
|
pix = load_default_profile_screenshot(name)
|
||||||
|
|
||||||
assert pix is not None and not pix.isNull()
|
assert pix is not None and not pix.isNull()
|
||||||
|
|
||||||
def test_load_screenshot_from_settings_invalid(self, temp_profile_dir):
|
def test_load_screenshot_from_settings_invalid(self, temp_profile_dir):
|
||||||
name = "invalid_screenshot"
|
name = "invalid_screenshot"
|
||||||
settings = open_runtime_settings(name)
|
settings = open_user_settings(name)
|
||||||
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], "not-an-image")
|
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], "not-an-image")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
@@ -1281,7 +1332,7 @@ class TestProfileInfoAndScreenshots:
|
|||||||
|
|
||||||
def test_load_screenshot_from_settings_bytes(self, temp_profile_dir):
|
def test_load_screenshot_from_settings_bytes(self, temp_profile_dir):
|
||||||
name = "bytes_screenshot"
|
name = "bytes_screenshot"
|
||||||
settings = open_runtime_settings(name)
|
settings = open_user_settings(name)
|
||||||
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES)
|
settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES)
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
@@ -1296,7 +1347,7 @@ class TestWorkSpaceManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_profiles(names):
|
def _create_profiles(names):
|
||||||
for name in names:
|
for name in names:
|
||||||
settings = open_runtime_settings(name)
|
settings = open_user_settings(name)
|
||||||
settings.setValue("meta", "value")
|
settings.setValue("meta", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
@@ -1360,7 +1411,7 @@ class TestWorkSpaceManager:
|
|||||||
|
|
||||||
manager.delete_profile(name)
|
manager.delete_profile(name)
|
||||||
|
|
||||||
assert not os.path.exists(runtime_profile_path(name))
|
assert not os.path.exists(user_profile_path(name))
|
||||||
assert target.refresh_calls >= 1
|
assert target.refresh_calls >= 1
|
||||||
|
|
||||||
def test_delete_readonly_profile_shows_message(
|
def test_delete_readonly_profile_shows_message(
|
||||||
@@ -1390,23 +1441,21 @@ class TestWorkSpaceManager:
|
|||||||
class TestAdvancedDockAreaRestoreAndDialogs:
|
class TestAdvancedDockAreaRestoreAndDialogs:
|
||||||
"""Additional coverage for restore flows and workspace dialogs."""
|
"""Additional coverage for restore flows and workspace dialogs."""
|
||||||
|
|
||||||
def test_restore_runtime_profile_from_baseline_confirm_true(
|
def test_restore_user_profile_from_default_confirm_true(self, advanced_dock_area, monkeypatch):
|
||||||
self, advanced_dock_area, monkeypatch
|
|
||||||
):
|
|
||||||
profile_name = "profile_restore_true"
|
profile_name = "profile_restore_true"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
helper.open_baseline(profile_name).sync()
|
helper.open_default(profile_name).sync()
|
||||||
helper.open_runtime(profile_name).sync()
|
helper.open_user(profile_name).sync()
|
||||||
advanced_dock_area._current_profile_name = profile_name
|
advanced_dock_area._current_profile_name = profile_name
|
||||||
advanced_dock_area.isVisible = lambda: False
|
advanced_dock_area.isVisible = lambda: False
|
||||||
pix = QPixmap(8, 8)
|
pix = QPixmap(8, 8)
|
||||||
pix.fill(Qt.red)
|
pix.fill(Qt.red)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.load_runtime_profile_screenshot",
|
"bec_widgets.widgets.containers.dock_area.dock_area.load_user_profile_screenshot",
|
||||||
lambda name, namespace=None: pix,
|
lambda name, namespace=None: pix,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.load_baseline_profile_screenshot",
|
"bec_widgets.widgets.containers.dock_area.dock_area.load_default_profile_screenshot",
|
||||||
lambda name, namespace=None: pix,
|
lambda name, namespace=None: pix,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
@@ -1416,12 +1465,12 @@ class TestAdvancedDockAreaRestoreAndDialogs:
|
|||||||
|
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.restore_runtime_from_baseline"
|
"bec_widgets.widgets.containers.dock_area.dock_area.restore_user_from_default"
|
||||||
) as mock_restore,
|
) as mock_restore,
|
||||||
patch.object(advanced_dock_area, "delete_all") as mock_delete_all,
|
patch.object(advanced_dock_area, "delete_all") as mock_delete_all,
|
||||||
patch.object(advanced_dock_area, "load_profile") as mock_load_profile,
|
patch.object(advanced_dock_area, "load_profile") as mock_load_profile,
|
||||||
):
|
):
|
||||||
advanced_dock_area.restore_baseline_profile(show_dialog=True)
|
advanced_dock_area.restore_user_profile_from_default()
|
||||||
|
|
||||||
assert mock_restore.call_count == 1
|
assert mock_restore.call_count == 1
|
||||||
args, kwargs = mock_restore.call_args
|
args, kwargs = mock_restore.call_args
|
||||||
@@ -1430,22 +1479,20 @@ class TestAdvancedDockAreaRestoreAndDialogs:
|
|||||||
mock_delete_all.assert_called_once()
|
mock_delete_all.assert_called_once()
|
||||||
mock_load_profile.assert_called_once_with(profile_name)
|
mock_load_profile.assert_called_once_with(profile_name)
|
||||||
|
|
||||||
def test_restore_runtime_profile_from_baseline_confirm_false(
|
def test_restore_user_profile_from_default_confirm_false(self, advanced_dock_area, monkeypatch):
|
||||||
self, advanced_dock_area, monkeypatch
|
|
||||||
):
|
|
||||||
profile_name = "profile_restore_false"
|
profile_name = "profile_restore_false"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
helper.open_baseline(profile_name).sync()
|
helper.open_default(profile_name).sync()
|
||||||
helper.open_runtime(profile_name).sync()
|
helper.open_user(profile_name).sync()
|
||||||
advanced_dock_area._current_profile_name = profile_name
|
advanced_dock_area._current_profile_name = profile_name
|
||||||
advanced_dock_area.isVisible = lambda: False
|
advanced_dock_area.isVisible = lambda: False
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.load_runtime_profile_screenshot",
|
"bec_widgets.widgets.containers.dock_area.dock_area.load_user_profile_screenshot",
|
||||||
lambda name, namespace=None: QPixmap(),
|
lambda name: QPixmap(),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.load_baseline_profile_screenshot",
|
"bec_widgets.widgets.containers.dock_area.dock_area.load_default_profile_screenshot",
|
||||||
lambda name, namespace=None: QPixmap(),
|
lambda name: QPixmap(),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.RestoreProfileDialog.confirm",
|
"bec_widgets.widgets.containers.dock_area.dock_area.RestoreProfileDialog.confirm",
|
||||||
@@ -1453,49 +1500,24 @@ class TestAdvancedDockAreaRestoreAndDialogs:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.restore_runtime_from_baseline"
|
"bec_widgets.widgets.containers.dock_area.dock_area.restore_user_from_default"
|
||||||
) as mock_restore:
|
) as mock_restore:
|
||||||
advanced_dock_area.restore_baseline_profile(show_dialog=True)
|
advanced_dock_area.restore_user_profile_from_default()
|
||||||
|
|
||||||
mock_restore.assert_not_called()
|
mock_restore.assert_not_called()
|
||||||
|
|
||||||
def test_restore_runtime_profile_from_baseline_without_dialog(self, advanced_dock_area):
|
def test_restore_user_profile_from_default_no_target(self, advanced_dock_area, monkeypatch):
|
||||||
profile_name = "alignment_scan"
|
|
||||||
helper = profile_helper(advanced_dock_area)
|
|
||||||
helper.open_baseline(profile_name).sync()
|
|
||||||
helper.open_runtime(profile_name).sync()
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.RestoreProfileDialog.confirm"
|
|
||||||
) as mock_confirm,
|
|
||||||
patch(
|
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.restore_runtime_from_baseline"
|
|
||||||
) as mock_restore,
|
|
||||||
patch.object(advanced_dock_area, "delete_all") as mock_delete_all,
|
|
||||||
patch.object(advanced_dock_area, "load_profile") as mock_load_profile,
|
|
||||||
):
|
|
||||||
advanced_dock_area.restore_baseline_profile(profile_name, show_dialog=False)
|
|
||||||
|
|
||||||
mock_confirm.assert_not_called()
|
|
||||||
mock_restore.assert_called_once_with(
|
|
||||||
profile_name, namespace=advanced_dock_area.profile_namespace
|
|
||||||
)
|
|
||||||
mock_delete_all.assert_called_once()
|
|
||||||
mock_load_profile.assert_called_once_with(profile_name)
|
|
||||||
|
|
||||||
def test_restore_runtime_profile_from_baseline_no_target(self, advanced_dock_area, monkeypatch):
|
|
||||||
advanced_dock_area._current_profile_name = None
|
advanced_dock_area._current_profile_name = None
|
||||||
with patch(
|
with patch(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.RestoreProfileDialog.confirm"
|
"bec_widgets.widgets.containers.dock_area.dock_area.RestoreProfileDialog.confirm"
|
||||||
) as mock_confirm:
|
) as mock_confirm:
|
||||||
advanced_dock_area.restore_baseline_profile(show_dialog=True)
|
advanced_dock_area.restore_user_profile_from_default()
|
||||||
mock_confirm.assert_not_called()
|
mock_confirm.assert_not_called()
|
||||||
|
|
||||||
def test_refresh_workspace_list_with_refresh_profiles(self, advanced_dock_area):
|
def test_refresh_workspace_list_with_refresh_profiles(self, advanced_dock_area):
|
||||||
profile_name = "refresh_profile"
|
profile_name = "refresh_profile"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
helper.open_runtime(profile_name).sync()
|
helper.open_user(profile_name).sync()
|
||||||
# Simulate a normal named-profile state (not transient empty startup mode).
|
# Simulate a normal named-profile state (not transient empty startup mode).
|
||||||
advanced_dock_area._empty_profile_active = False
|
advanced_dock_area._empty_profile_active = False
|
||||||
advanced_dock_area._current_profile_name = profile_name
|
advanced_dock_area._current_profile_name = profile_name
|
||||||
@@ -1550,8 +1572,8 @@ class TestAdvancedDockAreaRestoreAndDialogs:
|
|||||||
active = "active_profile"
|
active = "active_profile"
|
||||||
quick = "quick_profile"
|
quick = "quick_profile"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
helper.open_runtime(active).sync()
|
helper.open_user(active).sync()
|
||||||
helper.open_runtime(quick).sync()
|
helper.open_user(quick).sync()
|
||||||
helper.set_quick_select(quick, True)
|
helper.set_quick_select(quick, True)
|
||||||
|
|
||||||
combo_stub = ComboStub()
|
combo_stub = ComboStub()
|
||||||
@@ -1578,7 +1600,7 @@ class TestAdvancedDockAreaRestoreAndDialogs:
|
|||||||
|
|
||||||
advanced_dock_area._current_profile_name = "manager_profile"
|
advanced_dock_area._current_profile_name = "manager_profile"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
helper.open_runtime("manager_profile").sync()
|
helper.open_user("manager_profile").sync()
|
||||||
|
|
||||||
advanced_dock_area.show_workspace_manager()
|
advanced_dock_area.show_workspace_manager()
|
||||||
|
|
||||||
@@ -1613,104 +1635,18 @@ class TestProfileManagement:
|
|||||||
|
|
||||||
def test_profile_path(self, temp_profile_dir):
|
def test_profile_path(self, temp_profile_dir):
|
||||||
"""Test profile path generation."""
|
"""Test profile path generation."""
|
||||||
path = runtime_profile_path("test_profile")
|
path = user_profile_path("test_profile")
|
||||||
expected = os.path.join(temp_profile_dir, "runtime", "test_profile.ini")
|
expected = os.path.join(temp_profile_dir, "user", "test_profile.ini")
|
||||||
assert path == expected
|
assert path == expected
|
||||||
|
|
||||||
baseline_path = baseline_profile_path("test_profile")
|
default_path = default_profile_path("test_profile")
|
||||||
expected_baseline = os.path.join(temp_profile_dir, "baseline", "test_profile.ini")
|
expected_default = os.path.join(temp_profile_dir, "default", "test_profile.ini")
|
||||||
assert baseline_path == expected_baseline
|
assert default_path == expected_default
|
||||||
|
|
||||||
def test_legacy_user_profile_is_mapped_to_runtime(self, temp_profile_dir):
|
def test_open_settings(self, temp_profile_dir):
|
||||||
"""Legacy user profiles are copied into the canonical runtime segment."""
|
"""Test opening settings for a profile."""
|
||||||
name = "legacy_runtime"
|
settings = open_user_settings("test_profile")
|
||||||
legacy_dir = os.path.join(temp_profile_dir, "user")
|
assert isinstance(settings, QSettings)
|
||||||
os.makedirs(legacy_dir, exist_ok=True)
|
|
||||||
legacy_path = os.path.join(legacy_dir, f"{name}.ini")
|
|
||||||
legacy_settings = QSettings(legacy_path, QSettings.IniFormat)
|
|
||||||
legacy_settings.setValue("test/value", "legacy")
|
|
||||||
legacy_settings.sync()
|
|
||||||
|
|
||||||
canonical_path = runtime_profile_path(name)
|
|
||||||
assert not os.path.exists(canonical_path)
|
|
||||||
|
|
||||||
assert name in list_profiles()
|
|
||||||
|
|
||||||
assert os.path.exists(canonical_path)
|
|
||||||
assert open_runtime_settings(name).value("test/value", "", type=str) == "legacy"
|
|
||||||
|
|
||||||
def test_legacy_default_profile_is_mapped_to_baseline(self, temp_profile_dir):
|
|
||||||
"""Legacy default profiles are copied into the canonical baseline segment."""
|
|
||||||
name = "legacy_baseline"
|
|
||||||
legacy_dir = os.path.join(temp_profile_dir, "default")
|
|
||||||
os.makedirs(legacy_dir, exist_ok=True)
|
|
||||||
legacy_path = os.path.join(legacy_dir, f"{name}.ini")
|
|
||||||
legacy_settings = QSettings(legacy_path, QSettings.IniFormat)
|
|
||||||
legacy_settings.setValue("test/value", "legacy")
|
|
||||||
legacy_settings.sync()
|
|
||||||
|
|
||||||
canonical_path = baseline_profile_path(name)
|
|
||||||
assert not os.path.exists(canonical_path)
|
|
||||||
|
|
||||||
assert name in list_profiles()
|
|
||||||
|
|
||||||
assert os.path.exists(canonical_path)
|
|
||||||
assert open_baseline_settings(name).value("test/value", "", type=str) == "legacy"
|
|
||||||
|
|
||||||
def test_runtime_namespace_fallback_is_materialized(self, temp_profile_dir):
|
|
||||||
"""Canonical runtime namespace fallback is copied before opening primary settings."""
|
|
||||||
name = "runtime_namespace_fallback"
|
|
||||||
fallback_settings = open_runtime_settings(name)
|
|
||||||
fallback_settings.setValue("test/value", "fallback")
|
|
||||||
fallback_settings.sync()
|
|
||||||
|
|
||||||
namespaced_path = runtime_profile_path(name, namespace="beamline")
|
|
||||||
assert not os.path.exists(namespaced_path)
|
|
||||||
|
|
||||||
settings = open_runtime_settings(name, namespace="beamline")
|
|
||||||
|
|
||||||
assert os.path.exists(namespaced_path)
|
|
||||||
assert settings.value("test/value", "", type=str) == "fallback"
|
|
||||||
|
|
||||||
def test_baseline_namespace_fallback_is_materialized(self, temp_profile_dir):
|
|
||||||
"""Canonical baseline namespace fallback is copied before opening primary settings."""
|
|
||||||
name = "baseline_namespace_fallback"
|
|
||||||
fallback_settings = open_baseline_settings(name)
|
|
||||||
fallback_settings.setValue("test/value", "fallback")
|
|
||||||
fallback_settings.sync()
|
|
||||||
|
|
||||||
namespaced_path = baseline_profile_path(name, namespace="beamline")
|
|
||||||
assert not os.path.exists(namespaced_path)
|
|
||||||
|
|
||||||
settings = open_baseline_settings(name, namespace="beamline")
|
|
||||||
|
|
||||||
assert os.path.exists(namespaced_path)
|
|
||||||
assert settings.value("test/value", "", type=str) == "fallback"
|
|
||||||
|
|
||||||
def test_canonical_profile_wins_over_legacy_profile(self, temp_profile_dir):
|
|
||||||
"""Canonical runtime/baseline files are not overwritten by legacy fallback files."""
|
|
||||||
name = "canonical_wins"
|
|
||||||
runtime_settings = open_runtime_settings(name)
|
|
||||||
runtime_settings.setValue("test/value", "canonical-runtime")
|
|
||||||
runtime_settings.sync()
|
|
||||||
baseline_settings = open_baseline_settings(name)
|
|
||||||
baseline_settings.setValue("test/value", "canonical-baseline")
|
|
||||||
baseline_settings.sync()
|
|
||||||
|
|
||||||
for segment, value in (("user", "legacy-runtime"), ("default", "legacy-baseline")):
|
|
||||||
legacy_dir = os.path.join(temp_profile_dir, segment)
|
|
||||||
os.makedirs(legacy_dir, exist_ok=True)
|
|
||||||
legacy_settings = QSettings(
|
|
||||||
os.path.join(legacy_dir, f"{name}.ini"), QSettings.IniFormat
|
|
||||||
)
|
|
||||||
legacy_settings.setValue("test/value", value)
|
|
||||||
legacy_settings.sync()
|
|
||||||
|
|
||||||
assert name in list_profiles()
|
|
||||||
assert open_runtime_settings(name).value("test/value", "", type=str) == "canonical-runtime"
|
|
||||||
assert (
|
|
||||||
open_baseline_settings(name).value("test/value", "", type=str) == "canonical-baseline"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_list_profiles_empty(self, temp_profile_dir):
|
def test_list_profiles_empty(self, temp_profile_dir):
|
||||||
"""Test listing profiles when directory is empty."""
|
"""Test listing profiles when directory is empty."""
|
||||||
@@ -1730,7 +1666,7 @@ class TestProfileManagement:
|
|||||||
# Create some test profile files
|
# Create some test profile files
|
||||||
profile_names = ["profile1", "profile2", "profile3"]
|
profile_names = ["profile1", "profile2", "profile3"]
|
||||||
for name in profile_names:
|
for name in profile_names:
|
||||||
settings = open_runtime_settings(name)
|
settings = open_user_settings(name)
|
||||||
settings.setValue("test", "value")
|
settings.setValue("test", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
@@ -1740,24 +1676,24 @@ class TestProfileManagement:
|
|||||||
|
|
||||||
def test_readonly_profile_operations(self, temp_profile_dir, module_profile_factory):
|
def test_readonly_profile_operations(self, temp_profile_dir, module_profile_factory):
|
||||||
"""Test read-only profile functionality."""
|
"""Test read-only profile functionality."""
|
||||||
profile_name = "runtime_profile"
|
profile_name = "user_profile"
|
||||||
|
|
||||||
# Initially should not be read-only
|
# Initially should not be read-only
|
||||||
assert not is_profile_read_only(profile_name)
|
assert not is_profile_read_only(profile_name)
|
||||||
|
|
||||||
# Create a runtime profile and ensure it's writable
|
# Create a user profile and ensure it's writable
|
||||||
settings = open_runtime_settings(profile_name)
|
settings = open_user_settings(profile_name)
|
||||||
settings.setValue("test", "value")
|
settings.setValue("test", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
assert not is_profile_read_only(profile_name)
|
assert not is_profile_read_only(profile_name)
|
||||||
|
|
||||||
# Verify a bundled module profile is detected as read-only
|
# Verify a bundled module profile is detected as read-only
|
||||||
readonly_name = module_profile_factory("module_baseline")
|
readonly_name = module_profile_factory("module_default")
|
||||||
assert is_profile_read_only(readonly_name)
|
assert is_profile_read_only(readonly_name)
|
||||||
|
|
||||||
def test_write_and_read_manifest(self, temp_profile_dir, advanced_dock_area, qtbot):
|
def test_write_and_read_manifest(self, temp_profile_dir, advanced_dock_area, qtbot):
|
||||||
"""Test writing and reading dock manifest."""
|
"""Test writing and reading dock manifest."""
|
||||||
settings = open_runtime_settings("test_manifest")
|
settings = open_user_settings("test_manifest")
|
||||||
|
|
||||||
# Create real docks
|
# Create real docks
|
||||||
advanced_dock_area.new("DarkModeButton")
|
advanced_dock_area.new("DarkModeButton")
|
||||||
@@ -1787,18 +1723,18 @@ class TestProfileManagement:
|
|||||||
def test_restore_preserves_quick_select(self, temp_profile_dir):
|
def test_restore_preserves_quick_select(self, temp_profile_dir):
|
||||||
"""Ensure restoring keeps the quick select flag when it was enabled."""
|
"""Ensure restoring keeps the quick select flag when it was enabled."""
|
||||||
profile_name = "restorable_profile"
|
profile_name = "restorable_profile"
|
||||||
baseline_settings = open_baseline_settings(profile_name)
|
default_settings = open_default_settings(profile_name)
|
||||||
baseline_settings.setValue("test", "baseline")
|
default_settings.setValue("test", "default")
|
||||||
baseline_settings.sync()
|
default_settings.sync()
|
||||||
|
|
||||||
runtime_settings = open_runtime_settings(profile_name)
|
user_settings = open_user_settings(profile_name)
|
||||||
runtime_settings.setValue("test", "runtime")
|
user_settings.setValue("test", "user")
|
||||||
runtime_settings.sync()
|
user_settings.sync()
|
||||||
|
|
||||||
set_quick_select(profile_name, True)
|
set_quick_select(profile_name, True)
|
||||||
assert is_quick_select(profile_name)
|
assert is_quick_select(profile_name)
|
||||||
|
|
||||||
restore_runtime_from_baseline(profile_name)
|
restore_user_from_default(profile_name)
|
||||||
|
|
||||||
assert is_quick_select(profile_name)
|
assert is_quick_select(profile_name)
|
||||||
|
|
||||||
@@ -1822,7 +1758,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
widget.prepare_for_shutdown()
|
widget.prepare_for_shutdown()
|
||||||
mock_write.assert_not_called()
|
mock_write.assert_not_called()
|
||||||
|
|
||||||
helper.open_runtime("real_profile").sync()
|
helper.open_user("real_profile").sync()
|
||||||
widget.load_profile("real_profile")
|
widget.load_profile("real_profile")
|
||||||
assert widget._empty_profile_active is False
|
assert widget._empty_profile_active is False
|
||||||
assert widget._empty_profile_consumed is True
|
assert widget._empty_profile_consumed is True
|
||||||
@@ -1836,7 +1772,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
profile_name = module_profile_factory("readonly_profile")
|
profile_name = module_profile_factory("readonly_profile")
|
||||||
new_profile = f"{profile_name}_custom"
|
new_profile = f"{profile_name}_custom"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
target_path = helper.runtime_path(new_profile)
|
target_path = helper.user_path(new_profile)
|
||||||
if os.path.exists(target_path):
|
if os.path.exists(target_path):
|
||||||
os.remove(target_path)
|
os.remove(target_path)
|
||||||
|
|
||||||
@@ -1866,7 +1802,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
|
|
||||||
# Create a profile with manifest
|
# Create a profile with manifest
|
||||||
settings = helper.open_runtime(profile_name)
|
settings = helper.open_user(profile_name)
|
||||||
settings.beginWriteArray("manifest/widgets", 1)
|
settings.beginWriteArray("manifest/widgets", 1)
|
||||||
settings.setArrayIndex(0)
|
settings.setArrayIndex(0)
|
||||||
settings.setValue("object_name", "test_widget")
|
settings.setValue("object_name", "test_widget")
|
||||||
@@ -1887,83 +1823,6 @@ class TestWorkspaceProfileOperations:
|
|||||||
widget_map = advanced_dock_area.widget_map()
|
widget_map = advanced_dock_area.widget_map()
|
||||||
assert "test_widget" in widget_map
|
assert "test_widget" in widget_map
|
||||||
|
|
||||||
def test_load_profile_default_does_not_restore_baseline(self, advanced_dock_area):
|
|
||||||
"""Regular profile loading should not restore the runtime copy."""
|
|
||||||
profile_name = "load_without_baseline_restore"
|
|
||||||
helper = profile_helper(advanced_dock_area)
|
|
||||||
helper.open_runtime(profile_name).sync()
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.restore_runtime_from_baseline"
|
|
||||||
) as mock_restore:
|
|
||||||
advanced_dock_area.load_profile(profile_name)
|
|
||||||
|
|
||||||
mock_restore.assert_not_called()
|
|
||||||
assert advanced_dock_area._current_profile_name == profile_name
|
|
||||||
|
|
||||||
def test_load_profile_restores_baseline_without_dialog(self, advanced_dock_area):
|
|
||||||
"""CLI loading can restore the runtime copy from baseline without confirmation."""
|
|
||||||
profile_name = "alignment_scan"
|
|
||||||
helper = profile_helper(advanced_dock_area)
|
|
||||||
helper.open_baseline(profile_name).sync()
|
|
||||||
helper.open_runtime(profile_name).sync()
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.RestoreProfileDialog.confirm"
|
|
||||||
) as mock_confirm,
|
|
||||||
patch(
|
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.restore_runtime_from_baseline"
|
|
||||||
) as mock_restore,
|
|
||||||
):
|
|
||||||
advanced_dock_area.load_profile(profile_name, restore_baseline=True)
|
|
||||||
|
|
||||||
mock_confirm.assert_not_called()
|
|
||||||
mock_restore.assert_called_once_with(
|
|
||||||
profile_name, namespace=advanced_dock_area.profile_namespace
|
|
||||||
)
|
|
||||||
assert advanced_dock_area._current_profile_name == profile_name
|
|
||||||
|
|
||||||
def test_load_profile_materializes_runtime_namespace_fallback(self, advanced_dock_area):
|
|
||||||
"""Loading a runtime fallback copies it into the active namespace before opening."""
|
|
||||||
profile_name = "load_runtime_namespace_fallback"
|
|
||||||
helper = profile_helper(advanced_dock_area)
|
|
||||||
fallback_settings = open_runtime_settings(profile_name)
|
|
||||||
fallback_settings.setValue("test/value", "fallback")
|
|
||||||
fallback_settings.sync()
|
|
||||||
|
|
||||||
namespaced_path = helper.runtime_path(profile_name)
|
|
||||||
assert not os.path.exists(namespaced_path)
|
|
||||||
|
|
||||||
advanced_dock_area.load_profile(profile_name)
|
|
||||||
|
|
||||||
assert os.path.exists(namespaced_path)
|
|
||||||
assert (
|
|
||||||
QSettings(namespaced_path, QSettings.IniFormat).value("test/value", "", type=str)
|
|
||||||
== "fallback"
|
|
||||||
)
|
|
||||||
assert advanced_dock_area._current_profile_name == profile_name
|
|
||||||
|
|
||||||
def test_load_profile_materializes_baseline_namespace_fallback(self, advanced_dock_area):
|
|
||||||
"""Loading a baseline fallback copies it into the active namespace before opening."""
|
|
||||||
profile_name = "load_baseline_namespace_fallback"
|
|
||||||
helper = profile_helper(advanced_dock_area)
|
|
||||||
fallback_settings = open_baseline_settings(profile_name)
|
|
||||||
fallback_settings.setValue("test/value", "fallback")
|
|
||||||
fallback_settings.sync()
|
|
||||||
|
|
||||||
namespaced_path = helper.baseline_path(profile_name)
|
|
||||||
assert not os.path.exists(namespaced_path)
|
|
||||||
|
|
||||||
advanced_dock_area.load_profile(profile_name)
|
|
||||||
|
|
||||||
assert os.path.exists(namespaced_path)
|
|
||||||
assert (
|
|
||||||
QSettings(namespaced_path, QSettings.IniFormat).value("test/value", "", type=str)
|
|
||||||
== "fallback"
|
|
||||||
)
|
|
||||||
assert advanced_dock_area._current_profile_name == profile_name
|
|
||||||
|
|
||||||
def test_save_as_skips_autosave_source_profile(
|
def test_save_as_skips_autosave_source_profile(
|
||||||
self, advanced_dock_area, temp_profile_dir, qtbot
|
self, advanced_dock_area, temp_profile_dir, qtbot
|
||||||
):
|
):
|
||||||
@@ -1972,7 +1831,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
new_profile = "autosave_new"
|
new_profile = "autosave_new"
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
|
|
||||||
settings = helper.open_runtime(source_profile)
|
settings = helper.open_user(source_profile)
|
||||||
settings.beginWriteArray("manifest/widgets", 1)
|
settings.beginWriteArray("manifest/widgets", 1)
|
||||||
settings.setArrayIndex(0)
|
settings.setArrayIndex(0)
|
||||||
settings.setValue("object_name", "source_widget")
|
settings.setValue("object_name", "source_widget")
|
||||||
@@ -2004,16 +1863,11 @@ class TestWorkspaceProfileOperations:
|
|||||||
with patch(
|
with patch(
|
||||||
"bec_widgets.widgets.containers.dock_area.dock_area.SaveProfileDialog", StubDialog
|
"bec_widgets.widgets.containers.dock_area.dock_area.SaveProfileDialog", StubDialog
|
||||||
):
|
):
|
||||||
widgets_before_save = list(advanced_dock_area.widget_list())
|
advanced_dock_area.save_profile(show_dialog=True)
|
||||||
with patch.object(advanced_dock_area, "load_profile") as mock_load_profile:
|
|
||||||
advanced_dock_area.save_profile(show_dialog=True)
|
|
||||||
qtbot.wait(100)
|
|
||||||
mock_load_profile.assert_not_called()
|
|
||||||
|
|
||||||
qtbot.wait(500)
|
qtbot.wait(500)
|
||||||
assert list(advanced_dock_area.widget_list()) == widgets_before_save
|
source_manifest = read_manifest(helper.open_user(source_profile))
|
||||||
source_manifest = read_manifest(helper.open_runtime(source_profile))
|
new_manifest = read_manifest(helper.open_user(new_profile))
|
||||||
new_manifest = read_manifest(helper.open_runtime(new_profile))
|
|
||||||
|
|
||||||
assert len(source_manifest) == 1
|
assert len(source_manifest) == 1
|
||||||
assert len(new_manifest) == 2
|
assert len(new_manifest) == 2
|
||||||
@@ -2025,7 +1879,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
|
|
||||||
for profile in (profile_a, profile_b):
|
for profile in (profile_a, profile_b):
|
||||||
settings = helper.open_runtime(profile)
|
settings = helper.open_user(profile)
|
||||||
settings.beginWriteArray("manifest/widgets", 1)
|
settings.beginWriteArray("manifest/widgets", 1)
|
||||||
settings.setArrayIndex(0)
|
settings.setArrayIndex(0)
|
||||||
settings.setValue("object_name", f"{profile}_widget")
|
settings.setValue("object_name", f"{profile}_widget")
|
||||||
@@ -2044,7 +1898,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
advanced_dock_area.load_profile(profile_b)
|
advanced_dock_area.load_profile(profile_b)
|
||||||
qtbot.wait(500)
|
qtbot.wait(500)
|
||||||
|
|
||||||
manifest_a = read_manifest(helper.open_runtime(profile_a))
|
manifest_a = read_manifest(helper.open_user(profile_a))
|
||||||
assert len(manifest_a) == 2
|
assert len(manifest_a) == 2
|
||||||
|
|
||||||
def test_delete_profile_readonly(
|
def test_delete_profile_readonly(
|
||||||
@@ -2053,15 +1907,15 @@ class TestWorkspaceProfileOperations:
|
|||||||
"""Test deleting bundled profile removes only the writable copy."""
|
"""Test deleting bundled profile removes only the writable copy."""
|
||||||
profile_name = module_profile_factory("readonly_profile")
|
profile_name = module_profile_factory("readonly_profile")
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
helper.list_profiles() # ensure baseline and runtime copies are materialized
|
helper.list_profiles() # ensure default and user copies are materialized
|
||||||
helper.open_baseline(profile_name).sync()
|
helper.open_default(profile_name).sync()
|
||||||
settings = helper.open_runtime(profile_name)
|
settings = helper.open_user(profile_name)
|
||||||
settings.setValue("test", "value")
|
settings.setValue("test", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
runtime_path = helper.runtime_path(profile_name)
|
user_path = helper.user_path(profile_name)
|
||||||
baseline_path = helper.baseline_path(profile_name)
|
default_path = helper.default_path(profile_name)
|
||||||
assert os.path.exists(runtime_path)
|
assert os.path.exists(user_path)
|
||||||
assert os.path.exists(baseline_path)
|
assert os.path.exists(default_path)
|
||||||
|
|
||||||
with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action:
|
with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action:
|
||||||
mock_combo = MagicMock()
|
mock_combo = MagicMock()
|
||||||
@@ -2082,9 +1936,9 @@ class TestWorkspaceProfileOperations:
|
|||||||
|
|
||||||
mock_question.assert_not_called()
|
mock_question.assert_not_called()
|
||||||
mock_info.assert_called_once()
|
mock_info.assert_called_once()
|
||||||
# Read-only profile should remain intact (runtime + baseline copies)
|
# Read-only profile should remain intact (user + default copies)
|
||||||
assert os.path.exists(runtime_path)
|
assert os.path.exists(user_path)
|
||||||
assert os.path.exists(baseline_path)
|
assert os.path.exists(default_path)
|
||||||
|
|
||||||
def test_delete_profile_success(self, advanced_dock_area, temp_profile_dir):
|
def test_delete_profile_success(self, advanced_dock_area, temp_profile_dir):
|
||||||
"""Test successful profile deletion."""
|
"""Test successful profile deletion."""
|
||||||
@@ -2092,11 +1946,11 @@ class TestWorkspaceProfileOperations:
|
|||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
|
|
||||||
# Create regular profile
|
# Create regular profile
|
||||||
settings = helper.open_runtime(profile_name)
|
settings = helper.open_user(profile_name)
|
||||||
settings.setValue("test", "value")
|
settings.setValue("test", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
runtime_path = helper.runtime_path(profile_name)
|
user_path = helper.user_path(profile_name)
|
||||||
assert os.path.exists(runtime_path)
|
assert os.path.exists(user_path)
|
||||||
|
|
||||||
with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action:
|
with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action:
|
||||||
mock_combo = MagicMock()
|
mock_combo = MagicMock()
|
||||||
@@ -2114,7 +1968,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
mock_question.assert_called_once()
|
mock_question.assert_called_once()
|
||||||
mock_refresh.assert_called_once()
|
mock_refresh.assert_called_once()
|
||||||
# Profile should be deleted
|
# Profile should be deleted
|
||||||
assert not os.path.exists(runtime_path)
|
assert not os.path.exists(user_path)
|
||||||
|
|
||||||
def test_delete_profile_cli_usage(self, advanced_dock_area, temp_profile_dir):
|
def test_delete_profile_cli_usage(self, advanced_dock_area, temp_profile_dir):
|
||||||
"""Test delete_profile with explicit name (CLI usage - no dialog by default)."""
|
"""Test delete_profile with explicit name (CLI usage - no dialog by default)."""
|
||||||
@@ -2122,24 +1976,24 @@ class TestWorkspaceProfileOperations:
|
|||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
|
|
||||||
# Create regular profile
|
# Create regular profile
|
||||||
settings = helper.open_runtime(profile_name)
|
settings = helper.open_user(profile_name)
|
||||||
settings.setValue("test", "value")
|
settings.setValue("test", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
runtime_path = helper.runtime_path(profile_name)
|
user_path = helper.user_path(profile_name)
|
||||||
assert os.path.exists(runtime_path)
|
assert os.path.exists(user_path)
|
||||||
|
|
||||||
# Delete without dialog (CLI usage - default behavior)
|
# Delete without dialog (CLI usage - default behavior)
|
||||||
result = advanced_dock_area.delete_profile(profile_name)
|
result = advanced_dock_area.delete_profile(profile_name)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
assert not os.path.exists(runtime_path)
|
assert not os.path.exists(user_path)
|
||||||
|
|
||||||
def test_refresh_workspace_list(self, advanced_dock_area, temp_profile_dir):
|
def test_refresh_workspace_list(self, advanced_dock_area, temp_profile_dir):
|
||||||
"""Test refreshing workspace list."""
|
"""Test refreshing workspace list."""
|
||||||
# Create some profiles
|
# Create some profiles
|
||||||
helper = profile_helper(advanced_dock_area)
|
helper = profile_helper(advanced_dock_area)
|
||||||
for name in ["profile1", "profile2"]:
|
for name in ["profile1", "profile2"]:
|
||||||
settings = helper.open_runtime(name)
|
settings = helper.open_user(name)
|
||||||
settings.setValue("test", "value")
|
settings.setValue("test", "value")
|
||||||
settings.sync()
|
settings.sync()
|
||||||
|
|
||||||
@@ -2179,6 +2033,20 @@ class TestCleanupAndMisc:
|
|||||||
# Verify dock was removed
|
# Verify dock was removed
|
||||||
assert len(advanced_dock_area.dock_list()) == initial_count - 1
|
assert len(advanced_dock_area.dock_list()) == initial_count - 1
|
||||||
|
|
||||||
|
def test_apply_dock_lock(self, advanced_dock_area, qtbot):
|
||||||
|
"""Test _apply_dock_lock functionality."""
|
||||||
|
# Create a dock first
|
||||||
|
advanced_dock_area.new("DarkModeButton")
|
||||||
|
qtbot.wait(200)
|
||||||
|
|
||||||
|
# Test locking
|
||||||
|
advanced_dock_area._apply_dock_lock(True)
|
||||||
|
# No assertion needed - just verify it doesn't crash
|
||||||
|
|
||||||
|
# Test unlocking
|
||||||
|
advanced_dock_area._apply_dock_lock(False)
|
||||||
|
# No assertion needed - just verify it doesn't crash
|
||||||
|
|
||||||
def test_make_dock(self, advanced_dock_area):
|
def test_make_dock(self, advanced_dock_area):
|
||||||
"""Test _make_dock functionality."""
|
"""Test _make_dock functionality."""
|
||||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import (
|
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import (
|
||||||
|
|||||||
@@ -247,10 +247,12 @@ def test_bec_weblinks(monkeypatch):
|
|||||||
monkeypatch.setattr(webbrowser, "open", fake_open)
|
monkeypatch.setattr(webbrowser, "open", fake_open)
|
||||||
|
|
||||||
BECWebLinksMixin.open_bec_docs()
|
BECWebLinksMixin.open_bec_docs()
|
||||||
|
BECWebLinksMixin.open_bec_widgets_docs()
|
||||||
BECWebLinksMixin.open_bec_bug_report()
|
BECWebLinksMixin.open_bec_bug_report()
|
||||||
|
|
||||||
assert opened_urls == [
|
assert opened_urls == [
|
||||||
"https://bec-project.github.io/bec_docs/",
|
"https://beamline-experiment-control.readthedocs.io/en/latest/",
|
||||||
|
"https://bec.readthedocs.io/projects/bec-widgets/en/latest/",
|
||||||
"https://github.com/bec-project/bec_widgets/issues",
|
"https://github.com/bec-project/bec_widgets/issues",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user