1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-04 16:02:51 +01:00

fix(CLI): change the default behavior of launching the profiles in CLI

This commit is contained in:
2026-01-15 17:22:49 +01:00
committed by Jan Wyzula
parent 91050e88ae
commit 4f2a840c21
5 changed files with 116 additions and 56 deletions

View File

@@ -8,26 +8,38 @@ from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
logger = bec_logger.logger
def dock_area(object_name: str | None = None, profile: str | None = None) -> AdvancedDockArea:
def dock_area(
object_name: str | None = None, profile: str | None = None, start_empty: bool = False
) -> AdvancedDockArea:
"""
Create an advanced dock area using Qt Advanced Docking System.
Args:
object_name(str): The name of the advanced dock area.
profile(str|None): Optional profile to load; if None the last profile is restored.
profile(str|None): Optional profile to load; if None the "general" profile is used.
start_empty(bool): If True, start with an empty dock area when loading specified profile.
Returns:
AdvancedDockArea: The created advanced dock area.
Note:
The "general" profile is mandatory and will always exist. If manually deleted,
it will be automatically recreated.
"""
# Default to "general" profile when called from CLI without specifying a profile
effective_profile = profile if profile is not None else "general"
widget = AdvancedDockArea(
object_name=object_name,
restore_initial_profile=(profile is None),
restore_initial_profile=True,
root_widget=True,
profile_namespace="bec",
init_profile=effective_profile,
start_empty=start_empty,
)
logger.info(
f"Created advanced dock area with profile: {effective_profile}, start_empty: {start_empty}"
)
if profile:
widget.load_profile(profile)
logger.info(f"Created advanced dock area with profile: {profile}")
return widget

View File

@@ -30,7 +30,10 @@ from bec_widgets.utils.round_frame import RoundedFrame
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import list_profiles
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
get_last_profile,
list_profiles,
)
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
@@ -182,7 +185,6 @@ class LaunchTile(RoundedFrame):
class LaunchWindow(BECMainWindow):
RPC = True
TILE_SIZE = (250, 300)
DEFAULT_WORKSPACE_OPTION = "Last used workspace"
USER_ACCESS = ["show_launcher", "hide_launcher"]
def __init__(
@@ -345,6 +347,7 @@ class LaunchWindow(BECMainWindow):
def _refresh_dock_area_profiles(self, preserve_selection: bool = True) -> None:
"""
Refresh the dock-area profile selector, optionally preserving the selection.
Sets the combobox to the last used profile or "general" if no selection preserved.
Args:
preserve_selection(bool): Whether to preserve the current selection or not.
@@ -361,19 +364,49 @@ class LaunchWindow(BECMainWindow):
profiles = list_profiles("bec")
selector.blockSignals(True)
selector.clear()
selector.addItem(self.DEFAULT_WORKSPACE_OPTION)
for profile in profiles:
selector.addItem(profile)
if not selected_text or selected_text == self.DEFAULT_WORKSPACE_OPTION:
idx = 0
else:
if selected_text:
# Try to preserve the current selection
idx = selector.findText(selected_text, Qt.MatchFlag.MatchExactly)
if idx < 0:
idx = 0
selector.setCurrentIndex(idx)
if idx >= 0:
selector.setCurrentIndex(idx)
else:
# Selection no longer exists, fall back to last profile or "general"
self._set_selector_to_default_profile(selector, profiles)
else:
# No selection to preserve, use last profile or "general"
self._set_selector_to_default_profile(selector, profiles)
selector.blockSignals(False)
def _set_selector_to_default_profile(self, selector: QComboBox, profiles: list[str]) -> None:
"""
Set the selector to the last used profile or "general" as fallback.
Args:
selector(QComboBox): The combobox to set.
profiles(list[str]): List of available profiles.
"""
# Try to get last used profile
last_profile = get_last_profile(namespace="bec")
if last_profile and last_profile in profiles:
idx = selector.findText(last_profile, Qt.MatchFlag.MatchExactly)
if idx >= 0:
selector.setCurrentIndex(idx)
return
# Fall back to "general" profile
if "general" in profiles:
idx = selector.findText("general", Qt.MatchFlag.MatchExactly)
if idx >= 0:
selector.setCurrentIndex(idx)
return
# If nothing else, select first item
if selector.count() > 0:
selector.setCurrentIndex(0)
def launch(
self,
launch_script: str,
@@ -541,17 +574,14 @@ class LaunchWindow(BECMainWindow):
def _open_dock_area(self):
"""
Open Advanced Dock Area using the selected profile (if any).
Open Advanced Dock Area using the selected profile.
"""
tile = self.tiles.get("dock_area")
if tile is None or tile.selector is None:
profile = None
else:
selection = tile.selector.currentText().strip()
if not selection or selection == self.DEFAULT_WORKSPACE_OPTION:
profile = None
else:
profile = selection
profile = selection if selection else None
return self.launch("dock_area", profile=profile)
def _open_widget(self):

View File

@@ -293,7 +293,7 @@ class AdvancedDockArea(RPCBase):
@rpc_timeout(None)
@rpc_call
def load_profile(self, name: "str | None" = None):
def load_profile(self, name: "str | None" = None, start_empty: "bool" = False):
"""
Load a workspace profile.
@@ -302,6 +302,7 @@ class AdvancedDockArea(RPCBase):
Args:
name (str | None): The name of the profile to load. If None, prompts the user.
start_empty (bool): If True, load a profile without any widgets. Danger of overwriting the dynamic state of that profile.
"""
@rpc_call

View File

@@ -304,7 +304,7 @@ class BECGuiClient(RPCBase):
geometry: tuple[int, int, int, int] | None = None,
launch_script: str = "dock_area",
profile: str | None = None,
empty: bool = False,
start_empty: bool = False,
**kwargs,
) -> client.AdvancedDockArea:
"""Create a new top-level dock area.
@@ -314,24 +314,24 @@ class BECGuiClient(RPCBase):
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h).
launch_script(str): The launch script to use. Defaults to "dock_area".
profile(str | None): The profile name to load. If None, restores the last used profile.
profile(str | None): The profile name to load. If None, loads the "general" profile.
Use a profile name to load a specific saved profile.
empty(bool): If True, start with an empty dock area without loading any profile.
This takes precedence over the profile argument. Defaults to False.
start_empty(bool): If True, start with an empty dock area when loading specified profile.
**kwargs: Additional keyword arguments passed to the dock area.
Returns:
client.AdvancedDockArea: The new dock area.
Note:
The "general" profile is mandatory and will always exist. If manually deleted,
it will be automatically recreated.
Examples:
>>> gui.new() # Restore last used profile
>>> gui.new(profile="my_profile") # Load specific profile, if profile do not exist, the new profile is created empty with specified name
>>> gui.new(empty=True) # Start with empty dock area
>>> gui.new(name="custom_dock", empty=True) # Named empty dock area
>>> gui.new() # Start with the "general" profile
>>> gui.new(profile="my_profile") # Load specific profile, if profile does not exist, the new profile is created empty with specified name
>>> gui.new(start_empty=True) # Start with "general" profile but empty dock area
>>> gui.new(profile="my_profile", start_empty=True) # Start with "my_profile" profile but empty dock area
"""
if empty:
# Use a unique non-existent profile name to ensure empty start
profile = "__empty__"
if not self._check_if_server_is_alive():
self.start(wait=True)
if wait:
@@ -342,6 +342,7 @@ class BECGuiClient(RPCBase):
name=name,
geometry=geometry,
profile=profile,
start_empty=start_empty,
**kwargs,
) # pylint: disable=protected-access
return widget
@@ -351,6 +352,7 @@ class BECGuiClient(RPCBase):
name=name,
geometry=geometry,
profile=profile,
start_empty=start_empty,
**kwargs,
) # pylint: disable=protected-access
return widget

View File

@@ -126,6 +126,8 @@ class AdvancedDockArea(DockAreaWidget):
auto_save_upon_exit: bool = True,
enable_profile_management: bool = True,
restore_initial_profile: bool = True,
init_profile: str | None = None,
start_empty: bool = False,
**kwargs,
):
self._profile_namespace_hint = profile_namespace
@@ -135,6 +137,8 @@ class AdvancedDockArea(DockAreaWidget):
self._auto_save_upon_exit = auto_save_upon_exit
self._profile_management_enabled = enable_profile_management
self._restore_initial_profile = restore_initial_profile
self._init_profile = init_profile
self._start_empty = start_empty
super().__init__(
parent,
default_add_direction=default_add_direction,
@@ -184,7 +188,8 @@ class AdvancedDockArea(DockAreaWidget):
def _ensure_initial_profile(self) -> bool:
"""
Ensure at least one workspace profile exists for the current namespace.
Ensure the "general" workspace profile always exists for the current namespace.
The "general" profile is mandatory and will be recreated if deleted.
If list_profile fails due to file permission or corrupted profiles, no action taken.
Returns:
@@ -197,12 +202,13 @@ class AdvancedDockArea(DockAreaWidget):
logger.warning(f"Unable to enumerate profiles for namespace '{namespace}': {exc}")
return False
if existing_profiles:
# Always ensure "general" profile exists
name = "general"
if name in existing_profiles:
return False
name = "general"
logger.info(
f"No profiles found for namespace '{namespace}'. Bootstrapping '{name}' workspace."
f"Profile '{name}' not found in namespace '{namespace}'. Creating mandatory '{name}' workspace."
)
self._write_profile_settings(name, namespace, save_preview=False)
@@ -215,30 +221,37 @@ class AdvancedDockArea(DockAreaWidget):
combo = self.toolbar.components.get_action("workspace_combo").widget
namespace = self.profile_namespace
init_profile = None
instance_id = self._last_profile_instance_id()
if instance_id:
inst_profile = get_last_profile(
namespace=namespace, instance=instance_id, allow_namespace_fallback=False
)
if inst_profile and self._profile_exists(inst_profile, namespace):
init_profile = inst_profile
if not init_profile:
last = get_last_profile(namespace=namespace)
if last and self._profile_exists(last, namespace):
init_profile = last
else:
text = combo.currentText()
init_profile = text if text else None
if not init_profile:
if self._profile_exists("general", namespace):
init_profile = "general"
# First priority: use init_profile if explicitly provided
if self._init_profile:
init_profile = self._init_profile
else:
# Try to restore from last used profile
instance_id = self._last_profile_instance_id()
if instance_id:
inst_profile = get_last_profile(
namespace=namespace, instance=instance_id, allow_namespace_fallback=False
)
if inst_profile and self._profile_exists(inst_profile, namespace):
init_profile = inst_profile
if not init_profile:
last = get_last_profile(namespace=namespace)
if last and self._profile_exists(last, namespace):
init_profile = last
else:
text = combo.currentText()
init_profile = text if text else None
if not init_profile:
# Fall back to "general" profile which is guaranteed to exist
if self._profile_exists("general", namespace):
init_profile = "general"
if init_profile:
# Defer initial load to the event loop so child widgets exist before state restore.
QTimer.singleShot(0, lambda: self._load_initial_profile(init_profile))
def _load_initial_profile(self, name: str) -> None:
"""Load the initial profile after construction when the event loop is running."""
self.load_profile(name)
self.load_profile(name, start_empty=self._start_empty)
combo = self.toolbar.components.get_action("workspace_combo").widget
combo.blockSignals(True)
combo.setCurrentText(name)
@@ -807,8 +820,9 @@ class AdvancedDockArea(DockAreaWidget):
self.save_profile(name, show_dialog=True)
@SafeSlot(str)
@SafeSlot(str, bool)
@rpc_timeout(None)
def load_profile(self, name: str | None = None):
def load_profile(self, name: str | None = None, start_empty: bool = False):
"""
Load a workspace profile.
@@ -817,6 +831,7 @@ class AdvancedDockArea(DockAreaWidget):
Args:
name (str | None): The name of the profile to load. If None, prompts the user.
start_empty (bool): If True, load a profile without any widgets. Danger of overwriting the dynamic state of that profile.
"""
if not name: # Gui fallback if the name is not provided
name, ok = QInputDialog.getText(
@@ -849,7 +864,7 @@ class AdvancedDockArea(DockAreaWidget):
# Clear existing docks and remove all widgets
self.delete_all()
if name == "__empty__":
if start_empty:
self._finalize_profile_change(name, namespace)
return