1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-09 10:17:50 +01:00

fix(advanced_dock_area): profile behaviour adjusted, cleanup of the codebase

This commit is contained in:
2026-01-12 14:50:37 +01:00
committed by Jan Wyzula
parent e94ce73950
commit 46fe5498b5
5 changed files with 184 additions and 70 deletions

View File

@@ -214,6 +214,46 @@ class AdvancedDockArea(RPCBase):
None
"""
@rpc_call
def save_profile(
self,
name: "str | None" = None,
*,
show_dialog: "bool" = False,
quick_select: "bool | None" = None,
):
"""
Save the current workspace profile.
On first save of a given name:
- writes a default copy to states/default/<name>.ini with tag=default and created_at
- writes a user copy to states/user/<name>.ini with tag=user and created_at
On subsequent saves of user-owned profiles:
- updates both the default and user copies so restore uses the latest snapshot.
Read-only bundled profiles cannot be overwritten.
Args:
name (str | None): The name of the profile to save. If None and show_dialog is True,
prompts the user.
show_dialog (bool): If True, shows the SaveProfileDialog for user interaction.
If False (default), saves directly without user interaction (useful for CLI usage).
quick_select (bool | None): Whether to include the profile in quick selection.
If None (default), uses the existing value or True for new profiles.
Only used when show_dialog is False; otherwise the dialog provides the value.
"""
@rpc_call
def load_profile(self, name: "str | None" = None):
"""
Load a workspace profile.
Before switching, persist the current profile to the user copy.
Prefer loading the user copy; fall back to the default copy.
Args:
name (str | None): The name of the profile to load. If None, prompts the user.
"""
class AutoUpdates(RPCBase):
@property

View File

@@ -201,18 +201,7 @@ class AdvancedDockArea(DockAreaWidget):
f"No profiles found for namespace '{namespace}'. Bootstrapping '{name}' workspace."
)
default_settings = open_default_settings(name, namespace=namespace)
self._write_snapshot_to_settings(default_settings, save_preview=False)
if not default_settings.value(SETTINGS_KEYS["created_at"], ""):
default_settings.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
default_settings.setValue(SETTINGS_KEYS["is_quick_select"], True)
user_settings = open_user_settings(name, namespace=namespace)
self._write_snapshot_to_settings(user_settings, save_preview=False)
if not user_settings.value(SETTINGS_KEYS["created_at"], ""):
user_settings.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
user_settings.setValue(SETTINGS_KEYS["is_quick_select"], True)
self._write_profile_settings(name, namespace, save_preview=False)
set_quick_select(name, True, namespace=namespace)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
return True
@@ -608,8 +597,63 @@ class AdvancedDockArea(DockAreaWidget):
logger.info(f"Workspace snapshot written to settings: {settings.fileName()}")
def _write_profile_settings(
self,
name: str,
namespace: str | None,
*,
write_default: bool = True,
write_user: bool = True,
save_preview: bool = True,
) -> None:
"""
Write profile settings to default and/or user settings files.
Args:
name: The profile name.
namespace: The profile namespace.
write_default: Whether to write to the default settings file.
write_user: Whether to write to the user settings file.
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)
if write_user:
us = open_user_settings(name, namespace=namespace)
self._write_snapshot_to_settings(us, save_preview=save_preview)
if not us.value(SETTINGS_KEYS["created_at"], ""):
us.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
if not us.value(SETTINGS_KEYS["is_quick_select"], None):
us.setValue(SETTINGS_KEYS["is_quick_select"], True)
def _finalize_profile_change(self, name: str, namespace: str | None) -> None:
"""
Finalize a profile change by updating state and refreshing the UI.
Args:
name: The profile name.
namespace: The profile namespace.
"""
self._current_profile_name = name
self.profile_changed.emit(name)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
combo = self.toolbar.components.get_action("workspace_combo").widget
combo.refresh_profiles(active_profile=name)
@SafeSlot(str)
def save_profile(self, name: str | None = None):
def save_profile(
self,
name: str | None = None,
*,
show_dialog: bool = False,
quick_select: bool | None = None,
):
"""
Save the current workspace profile.
@@ -621,76 +665,106 @@ class AdvancedDockArea(DockAreaWidget):
Read-only bundled profiles cannot be overwritten.
Args:
name (str | None): The name of the profile to save. If None, prompts the user.
name (str | None): The name of the profile to save. If None and show_dialog is True,
prompts the user.
show_dialog (bool): If True, shows the SaveProfileDialog for user interaction.
If False (default), saves directly without user interaction (useful for CLI usage).
quick_select (bool | None): Whether to include the profile in quick selection.
If None (default), uses the existing value or True for new profiles.
Only used when show_dialog is False; otherwise the dialog provides the value.
"""
namespace = self.profile_namespace
current_profile = getattr(self, "_current_profile_name", "") or ""
def _profile_exists(profile_name: str) -> bool:
return profile_origin(profile_name, namespace=namespace) != "unknown"
initial_name = name or ""
quickselect_default = is_quick_select(name, namespace=namespace) if name else True
# Determine final values either from dialog or directly
if show_dialog:
initial_name = name or ""
quickselect_default = is_quick_select(name, namespace=namespace) if name else True
current_profile = getattr(self, "_current_profile_name", "") or ""
dialog = SaveProfileDialog(
self,
current_name=initial_name,
current_profile_name=current_profile,
name_exists=_profile_exists,
profile_origin=lambda n: profile_origin(n, namespace=namespace),
origin_label=lambda n: profile_origin_display(n, namespace=namespace),
quick_select_checked=quickselect_default,
)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
dialog = SaveProfileDialog(
self,
current_name=initial_name,
current_profile_name=current_profile,
name_exists=_profile_exists,
profile_origin=lambda n: profile_origin(n, namespace=namespace),
origin_label=lambda n: profile_origin_display(n, namespace=namespace),
quick_select_checked=quickselect_default,
)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
name = dialog.get_profile_name()
quickselect = dialog.is_quick_select()
overwrite_existing = dialog.overwrite_existing
else:
# CLI / programmatic usage - no dialog
if not name:
logger.warning("save_profile called without name and show_dialog=False")
return
# Determine quick_select value
if quick_select is None:
# Use existing value if profile exists, otherwise default to True
quickselect = (
is_quick_select(name, namespace=namespace) if _profile_exists(name) else True
)
else:
quickselect = quick_select
# For programmatic saves, check if profile is read-only
origin = profile_origin(name, namespace=namespace)
if origin in {"module", "plugin"}:
logger.warning(f"Cannot save to read-only profile '{name}' (origin: {origin})")
return
# Overwrite existing settings profile when saving programmatically
overwrite_existing = origin == "settings"
name = dialog.get_profile_name()
quickselect = dialog.is_quick_select()
origin_before_save = profile_origin(name, namespace=namespace)
overwrite_default = dialog.overwrite_existing and origin_before_save == "settings"
# Display saving placeholder
overwrite_default = overwrite_existing and origin_before_save == "settings"
# Display saving placeholder in toolbar
workspace_combo = self.toolbar.components.get_action("workspace_combo").widget
workspace_combo.blockSignals(True)
workspace_combo.insertItem(0, f"{name}-saving")
workspace_combo.setCurrentIndex(0)
workspace_combo.blockSignals(False)
# Create or update default copy controlled by overwrite flag
# Write to default and/or user settings
should_write_default = overwrite_default or not any(
os.path.exists(path) for path in default_profile_candidates(name, namespace)
)
if should_write_default:
ds = open_default_settings(name, namespace=namespace)
self._write_snapshot_to_settings(ds)
if not ds.value(SETTINGS_KEYS["created_at"], ""):
ds.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
# Ensure new profiles are quick-select by default
if not ds.value(SETTINGS_KEYS["is_quick_select"], None):
ds.setValue(SETTINGS_KEYS["is_quick_select"], True)
# Always (over)write the user copy
us = open_user_settings(name, namespace=namespace)
self._write_snapshot_to_settings(us)
if not us.value(SETTINGS_KEYS["created_at"], ""):
us.setValue(SETTINGS_KEYS["created_at"], now_iso_utc())
# Ensure new profiles are quick-select by default (only if missing)
if not us.value(SETTINGS_KEYS["is_quick_select"], None):
us.setValue(SETTINGS_KEYS["is_quick_select"], True)
self._write_profile_settings(
name, namespace, write_default=should_write_default, write_user=True
)
set_quick_select(name, quickselect, namespace=namespace)
self._refresh_workspace_list()
if current_profile and current_profile != name and not dialog.overwrite_existing:
if current_profile and current_profile != name and not overwrite_existing:
self._pending_autosave_skip = (current_profile, name)
else:
self._pending_autosave_skip = None
workspace_combo.setCurrentText(name)
self._current_profile_name = name
self.profile_changed.emit(name)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
combo = self.toolbar.components.get_action("workspace_combo").widget
combo.refresh_profiles(active_profile=name)
self._finalize_profile_change(name, namespace)
@SafeSlot()
def save_profile_dialog(self, name: str | None = None):
"""
Save the current workspace profile with a dialog prompt.
This is a convenience method for UI usage (toolbar, dialogs) that
always shows the SaveProfileDialog. For programmatic/CLI usage,
use save_profile() directly.
Args:
name (str | None): Optional initial name to populate in the dialog.
"""
self.save_profile(name, show_dialog=True)
def load_profile(self, name: str | None = None):
"""
@@ -725,7 +799,9 @@ class AdvancedDockArea(DockAreaWidget):
elif any(os.path.exists(path) for path in default_profile_candidates(name, namespace)):
settings = open_default_settings(name, namespace=namespace)
if settings is None:
QMessageBox.warning(self, "Profile not found", f"Profile '{name}' not found.")
logger.warning(f"Profile '{name}' not found in namespace '{namespace}'. Creating new.")
self.delete_all()
self.save_profile(name, show_dialog=False, quick_select=True)
return
# Clear existing docks and remove all widgets
@@ -759,11 +835,7 @@ class AdvancedDockArea(DockAreaWidget):
self.state_manager.load_state(settings=settings)
self._set_editable(self._editable)
self._current_profile_name = name
self.profile_changed.emit(name)
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
combo = self.toolbar.components.get_action("workspace_combo").widget
combo.refresh_profiles(active_profile=name)
self._finalize_profile_change(name, namespace)
@SafeSlot()
@SafeSlot(str)

View File

@@ -20,7 +20,7 @@ from bec_lib import bec_logger
from bec_lib.client import BECClient
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
from pydantic import BaseModel, Field
from qtpy.QtCore import QByteArray, QDateTime, QSettings, Qt
from qtpy.QtCore import QByteArray, QDateTime, QSettings, QTimeZone
from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import QApplication
@@ -627,7 +627,7 @@ def now_iso_utc() -> str:
Returns:
str: UTC timestamp string (e.g., ``"2024-06-05T12:34:56Z"``).
"""
return QDateTime.currentDateTimeUtc().toString(Qt.ISODate)
return QDateTime.currentDateTimeUtc().toString("yyyy-MM-ddTHH:mm:ssZ")
def write_manifest(settings: QSettings, docks: list[CDockWidget]) -> None:
@@ -843,7 +843,9 @@ def _file_modified_iso(path: str) -> str:
"""
try:
mtime = os.path.getmtime(path)
return QDateTime.fromSecsSinceEpoch(int(mtime), Qt.UTC).toString(Qt.ISODate)
return QDateTime.fromSecsSinceEpoch(int(mtime), QTimeZone.utc()).toString(
"yyyy-MM-ddTHH:mm:ssZ"
)
except Exception:
return now_iso_utc()

View File

@@ -330,7 +330,7 @@ class WorkSpaceManager(BECWidget, QWidget):
)
return
self.target_widget.save_profile()
self.target_widget.save_profile_dialog()
# AdvancedDockArea will emit profile_changed which will trigger table refresh,
# but ensure the UI stays in sync even if the signal is delayed.
self.render_table()
@@ -402,7 +402,7 @@ class WorkSpaceManager(BECWidget, QWidget):
scaled = pm.scaled(
self.screenshot_label.width() or 800,
self.screenshot_label.height() or 450,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self.screenshot_label.setPixmap(scaled)

View File

@@ -156,7 +156,7 @@ class WorkspaceConnection(BundleConnection):
# Connect the action to the target widget's method
save_action = self.components.get_action("save_workspace").action
if save_action.isVisible():
save_action.triggered.connect(self.target_widget.save_profile)
save_action.triggered.connect(self.target_widget.save_profile_dialog)
self.components.get_action("workspace_combo").widget.currentTextChanged.connect(
self.target_widget.load_profile
@@ -176,7 +176,7 @@ class WorkspaceConnection(BundleConnection):
# Disconnect the action from the target widget's method
save_action = self.components.get_action("save_workspace").action
if save_action.isVisible():
save_action.triggered.disconnect(self.target_widget.save_profile)
save_action.triggered.disconnect(self.target_widget.save_profile_dialog)
self.components.get_action("workspace_combo").widget.currentTextChanged.disconnect(
self.target_widget.load_profile
)