mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 11:11:49 +02:00
feat(modular_toolbar): remove action/bundle by id
This commit is contained in:
@ -702,6 +702,85 @@ class ModularToolBar(QToolBar):
|
|||||||
self.bundles[bundle_id].append(action_id)
|
self.bundles[bundle_id].append(action_id)
|
||||||
self.update_separators()
|
self.update_separators()
|
||||||
|
|
||||||
|
def remove_action(self, action_id: str):
|
||||||
|
"""
|
||||||
|
Completely remove a single action from the toolbar.
|
||||||
|
|
||||||
|
The method takes care of both standalone actions and actions that are
|
||||||
|
part of an existing bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_id (str): Unique identifier for the action.
|
||||||
|
"""
|
||||||
|
if action_id not in self.widgets:
|
||||||
|
raise ValueError(f"Action with ID '{action_id}' does not exist.")
|
||||||
|
|
||||||
|
# Identify potential bundle membership
|
||||||
|
parent_bundle = None
|
||||||
|
for b_id, a_ids in self.bundles.items():
|
||||||
|
if action_id in a_ids:
|
||||||
|
parent_bundle = b_id
|
||||||
|
break
|
||||||
|
|
||||||
|
# 1. Remove the QAction from the QToolBar and delete it
|
||||||
|
tool_action = self.widgets.pop(action_id)
|
||||||
|
if hasattr(tool_action, "action") and tool_action.action is not None:
|
||||||
|
self.removeAction(tool_action.action)
|
||||||
|
tool_action.action.deleteLater()
|
||||||
|
|
||||||
|
# 2. Clean bundle bookkeeping if the action belonged to one
|
||||||
|
if parent_bundle:
|
||||||
|
self.bundles[parent_bundle].remove(action_id)
|
||||||
|
# If the bundle becomes empty, get rid of the bundle entry as well
|
||||||
|
if not self.bundles[parent_bundle]:
|
||||||
|
self.remove_bundle(parent_bundle)
|
||||||
|
|
||||||
|
# 3. Remove from the ordering list
|
||||||
|
self.toolbar_items = [
|
||||||
|
item
|
||||||
|
for item in self.toolbar_items
|
||||||
|
if not (item[0] == "action" and item[1] == action_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.update_separators()
|
||||||
|
|
||||||
|
def remove_bundle(self, bundle_id: str):
|
||||||
|
"""
|
||||||
|
Remove an entire bundle (and all of its actions) from the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bundle_id (str): Unique identifier for the bundle.
|
||||||
|
"""
|
||||||
|
if bundle_id not in self.bundles:
|
||||||
|
raise ValueError(f"Bundle '{bundle_id}' does not exist.")
|
||||||
|
|
||||||
|
# Remove every action belonging to this bundle
|
||||||
|
for action_id in list(self.bundles[bundle_id]): # copy the list
|
||||||
|
if action_id in self.widgets:
|
||||||
|
tool_action = self.widgets.pop(action_id)
|
||||||
|
if hasattr(tool_action, "action") and tool_action.action is not None:
|
||||||
|
self.removeAction(tool_action.action)
|
||||||
|
tool_action.action.deleteLater()
|
||||||
|
|
||||||
|
# Drop the bundle entry
|
||||||
|
self.bundles.pop(bundle_id, None)
|
||||||
|
|
||||||
|
# Remove bundle entry and its preceding separator (if any) from the ordering list
|
||||||
|
cleaned_items = []
|
||||||
|
skip_next_separator = False
|
||||||
|
for item_type, ident in self.toolbar_items:
|
||||||
|
if item_type == "bundle" and ident == bundle_id:
|
||||||
|
# mark to skip one following separator if present
|
||||||
|
skip_next_separator = True
|
||||||
|
continue
|
||||||
|
if skip_next_separator and item_type == "separator":
|
||||||
|
skip_next_separator = False
|
||||||
|
continue
|
||||||
|
cleaned_items.append((item_type, ident))
|
||||||
|
self.toolbar_items = cleaned_items
|
||||||
|
|
||||||
|
self.update_separators()
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
"""
|
"""
|
||||||
Overrides the context menu event to show toolbar actions with checkboxes and icons.
|
Overrides the context menu event to show toolbar actions with checkboxes and icons.
|
||||||
|
@ -518,3 +518,156 @@ def test_long_pressbutton(toolbar_fixture, dummy_widget, switchable_toolbar_acti
|
|||||||
|
|
||||||
# Verify that fake_showMenu() was called.
|
# Verify that fake_showMenu() was called.
|
||||||
assert call_flag, "Long press did not trigger showMenu() as expected."
|
assert call_flag, "Long press did not trigger showMenu() as expected."
|
||||||
|
|
||||||
|
|
||||||
|
# Additional tests for action/bundle removal
|
||||||
|
def test_remove_standalone_action(toolbar_fixture, icon_action, dummy_widget):
|
||||||
|
"""
|
||||||
|
Ensure that a standalone action is fully removed and no longer accessible.
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
# Add the action and check it is present
|
||||||
|
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||||||
|
assert "icon_action" in toolbar.widgets
|
||||||
|
assert icon_action.action in toolbar.actions()
|
||||||
|
|
||||||
|
# Now remove it
|
||||||
|
toolbar.remove_action("icon_action")
|
||||||
|
|
||||||
|
# Action bookkeeping
|
||||||
|
assert "icon_action" not in toolbar.widgets
|
||||||
|
# QAction list
|
||||||
|
assert icon_action.action not in toolbar.actions()
|
||||||
|
# Attempting to hide / show it should raise
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
toolbar.hide_action("icon_action")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
toolbar.show_action("icon_action")
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_action_from_bundle(
|
||||||
|
toolbar_fixture, dummy_widget, icon_action, material_icon_action
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Remove a single action that is part of a bundle and verify clean‑up.
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
bundle = ToolbarBundle(
|
||||||
|
bundle_id="test_bundle",
|
||||||
|
actions=[("icon_action", icon_action), ("material_action", material_icon_action)],
|
||||||
|
)
|
||||||
|
toolbar.add_bundle(bundle, dummy_widget)
|
||||||
|
|
||||||
|
# Initial assertions
|
||||||
|
assert "test_bundle" in toolbar.bundles
|
||||||
|
assert "icon_action" in toolbar.widgets
|
||||||
|
assert "material_action" in toolbar.widgets
|
||||||
|
|
||||||
|
# Remove one action from the bundle
|
||||||
|
toolbar.remove_action("icon_action")
|
||||||
|
|
||||||
|
# icon_action should be fully gone
|
||||||
|
assert "icon_action" not in toolbar.widgets
|
||||||
|
assert icon_action.action not in toolbar.actions()
|
||||||
|
# Bundle tracking should be updated
|
||||||
|
assert "icon_action" not in toolbar.bundles["test_bundle"]
|
||||||
|
# The other action must still exist
|
||||||
|
assert "material_action" in toolbar.widgets
|
||||||
|
assert material_icon_action.action in toolbar.actions()
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_last_action_from_bundle_removes_bundle(toolbar_fixture, dummy_widget, icon_action):
|
||||||
|
"""
|
||||||
|
Removing the final action from a bundle should delete the bundle entry itself.
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
bundle = ToolbarBundle(bundle_id="single_action_bundle", actions=[("only_action", icon_action)])
|
||||||
|
toolbar.add_bundle(bundle, dummy_widget)
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
assert "single_action_bundle" in toolbar.bundles
|
||||||
|
assert "only_action" in toolbar.widgets
|
||||||
|
|
||||||
|
# Remove the sole action
|
||||||
|
toolbar.remove_action("only_action")
|
||||||
|
|
||||||
|
# Bundle should be gone
|
||||||
|
assert "single_action_bundle" not in toolbar.bundles
|
||||||
|
# QAction removed
|
||||||
|
assert icon_action.action not in toolbar.actions()
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_entire_bundle(toolbar_fixture, dummy_widget, icon_action, material_icon_action):
|
||||||
|
"""
|
||||||
|
Ensure that removing a bundle deletes all its actions and separators.
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
bundle = ToolbarBundle(
|
||||||
|
bundle_id="to_remove",
|
||||||
|
actions=[("icon_action", icon_action), ("material_action", material_icon_action)],
|
||||||
|
)
|
||||||
|
toolbar.add_bundle(bundle, dummy_widget)
|
||||||
|
|
||||||
|
# Confirm bundle presence
|
||||||
|
assert "to_remove" in toolbar.bundles
|
||||||
|
|
||||||
|
# Remove the whole bundle
|
||||||
|
toolbar.remove_bundle("to_remove")
|
||||||
|
|
||||||
|
# Bundle mapping gone
|
||||||
|
assert "to_remove" not in toolbar.bundles
|
||||||
|
# All actions gone
|
||||||
|
for aid, act in [("icon_action", icon_action), ("material_action", material_icon_action)]:
|
||||||
|
assert aid not in toolbar.widgets
|
||||||
|
assert act.action not in toolbar.actions()
|
||||||
|
|
||||||
|
|
||||||
|
def test_trigger_removed_action_raises(toolbar_fixture, icon_action, dummy_widget, qtbot):
|
||||||
|
"""
|
||||||
|
Add an action, connect a mock slot, then remove the action and verify that
|
||||||
|
attempting to trigger it afterwards raises RuntimeError (since the underlying
|
||||||
|
QAction has been deleted).
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
|
||||||
|
# Add the action and connect a mock slot
|
||||||
|
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||||||
|
called = []
|
||||||
|
|
||||||
|
def mock_slot():
|
||||||
|
called.append(True)
|
||||||
|
|
||||||
|
icon_action.action.triggered.connect(mock_slot)
|
||||||
|
|
||||||
|
# Trigger once to confirm connection works
|
||||||
|
icon_action.action.trigger()
|
||||||
|
assert called == [True]
|
||||||
|
|
||||||
|
# Now remove the action
|
||||||
|
toolbar.remove_action("icon_action")
|
||||||
|
# Allow deleteLater event to process
|
||||||
|
qtbot.wait(50)
|
||||||
|
|
||||||
|
# The underlying C++ object should be deleted; triggering should raise
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
icon_action.action.trigger()
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_nonexistent_action(toolbar_fixture):
|
||||||
|
"""
|
||||||
|
Attempting to remove an action that does not exist should raise ValueError.
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
toolbar.remove_action("nonexistent_action")
|
||||||
|
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_nonexistent_bundle(toolbar_fixture):
|
||||||
|
"""
|
||||||
|
Attempting to remove a bundle that does not exist should raise ValueError.
|
||||||
|
"""
|
||||||
|
toolbar = toolbar_fixture
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
toolbar.remove_bundle("nonexistent_bundle")
|
||||||
|
assert "Bundle 'nonexistent_bundle' does not exist." in str(excinfo.value)
|
||||||
|
Reference in New Issue
Block a user