0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat(dock_area): Added toolbar to dock area to add widgets without CLI interactions

This commit is contained in:
2024-07-21 13:33:57 +02:00
parent 28f26e92a4
commit cce1367a72
19 changed files with 180 additions and 31 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="m145.26-88.13-57.13-57.13 137.39-137.39H105.87v-79.22h256v256h-79.22v-119.65L145.26-88.13Zm669.48 0L677.91-225.52v119.65h-79.78v-256H854.7v79.22H734.48l137.39 137.39-57.13 57.13Zm-708.87-510v-79.78h119.65L88.13-814.74l57.13-57.13 137.39 137.39V-854.7h79.22v256.57h-256Zm492.26 0V-854.7h79.78v120.22l137.83-138.39 57.13 57.13-138.39 137.83H854.7v79.78H598.13Z"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M403.39-158.74 82.13-480l321.26-321.26v642.52Zm153.22 0v-642.52L878.44-480 556.61-158.74Zm81.57-195.74L763.13-480 638.18-605.52v251.04Z"/>
</svg>

After

Width:  |  Height:  |  Size: 266 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h589.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v589.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09Zm43.56-166.04h503.7L578-481.48l-132 171-93-127-124.35 165.57Z"/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M443.78-28.43v-75Q305.65-118 211.54-212.11q-94.11-94.11-108.11-231.67h-75v-72.44h75Q118-654.35 212.11-748.46q94.11-94.11 231.67-108.11v-75h72.44v75q137.56 14 231.67 108.11Q842-654.35 856.57-516.22h75v72.44h-75q-14 137.56-108.11 231.67Q654.35-118 516.22-103.43v75h-72.44Zm36.12-152.66q123.4 0 211.21-87.7 87.8-87.71 87.8-211.11 0-123.4-87.7-211.21-87.71-87.8-211.11-87.8-123.4 0-211.21 87.7-87.8 87.71-87.8 211.11 0 123.4 87.7 211.21 87.71 87.8 211.11 87.8ZM480-330q-63 0-106.5-43.5T330-480q0-63 43.5-106.5T480-630q63 0 106.5 43.5T630-480q0 63-43.5 106.5T480-330Z"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M480-65.87q-87.51 0-162.97-31.89-75.47-31.89-131.43-87.84-55.95-55.96-87.84-131.43Q65.87-392.49 65.87-480q0-87.47 31.88-162.87 31.89-75.39 87.75-131.51 55.87-56.11 131.41-88.21Q392.44-894.7 480-894.7q15.96 0 27.78 12.16 11.83 12.16 11.83 28.07 0 15.9-11.83 27.73-11.82 11.83-27.78 11.83-139.31 0-237.11 97.8-97.8 97.8-97.8 237.1 0 139.31 97.8 237.12 97.8 97.8 237.1 97.8 139.31 0 237.12-97.8 97.8-97.8 97.8-237.11 0-15.96 11.83-27.78 11.83-11.83 27.73-11.83 15.91 0 28.07 11.83Q894.7-495.96 894.7-480q0 87.56-32.14 163.1-32.14 75.54-88.11 131.44-55.97 55.9-131.44 87.74Q567.55-65.87 480-65.87Z"/>
</svg>

After

Width:  |  Height:  |  Size: 724 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M854.7-105.87H105.87v-209.61H854.7v209.61Zm0-269.61H105.87v-209.04H854.7v209.04Zm0-269.04H105.87V-854.7H854.7v210.18Z"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M336.04-487.39 145.09-678.35v156.65H65.87v-293H358.3v79.79H201.22l191.39 190.95-56.57 56.57ZM145.09-145.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87V-451.7h79.22v226.61H493v79.22H145.09Zm669.82-278.3v-310.74H428.3v-79.79h386.61q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v310.74h-79.79Zm79.79 60v218.87H553v-218.87h341.7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M185.87-98.52v-681.39q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h429.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v681.39L480-224.17 185.87-98.52Zm79.22-120.39L480-309.18l214.91 90.27v-561H265.09v561Zm0-561h429.82-429.82Z"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M185.09-105.87q-32.93 0-56.08-23.14-23.14-23.15-23.14-56.08v-589.82q0-33.16 23.14-56.47 23.15-23.32 56.08-23.32h589.82q33.16 0 56.47 23.32 23.32 23.31 23.32 56.47v589.82q0 32.93-23.32 56.08-23.31 23.14-56.47 23.14H185.09Zm439.21-79.22h71l79.61-79.61v-40.39H744.3l-120 120ZM281-389.48l141-140 90 90L725.52-654 679-700.52l-167 167-90-90L234.48-436 281-389.48Zm-95.91 204.39h34.56l120-120h-71l-83.56 83.57v36.43Zm350.91 0 120-120h-71l-120 120h71Zm-159.87 0 120-120h-71l-120 120h71Z"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M286.83-277h60v-205h-60v205Zm326.91 0h60v-420h-60v420ZM450-277h60v-118h-60v118Zm0-205h60v-60h-60v60ZM185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h589.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v589.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09Z"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M145.09-145.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-509.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h669.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v509.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H145.09Zm0-79.22h669.82v-425.82H145.09v425.82ZM300-292l-42-42 103-104-104-104 43-42 146 146-146 146Zm190 4v-60h220v60H490Z"/>
</svg>

After

Width:  |  Height:  |  Size: 466 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M126-206.43 66.43-266 380-579.57 539.43-419.7l298-335 56.14 54.57-352.44 399.26L380-460.43l-254 254Z"/>
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@ -46,7 +46,7 @@ class BECDock(BECWidget, Dock):
def __init__(
self,
parent: QWidget | None = None,
parent_dock_area: BECDockArea | None = None,
parent_dock_area: QWidget | None = None,
config: DockConfig | None = None,
name: str | None = None,
client=None,
@ -64,6 +64,8 @@ class BECDock(BECWidget, Dock):
super().__init__(client=client, config=config, gui_id=gui_id)
Dock.__init__(self, name=name, **kwargs)
self.parent_dock_area = parent_dock_area
# Layout Manager
self.layout_manager = GridLayoutManager(self.layout)
@ -230,7 +232,7 @@ class BECDock(BECWidget, Dock):
Remove the dock from the parent dock area.
"""
# self.cleanup()
self.orig_area.remove_dock(self.name())
self.parent_dock_area.remove_dock(self.name())
def cleanup(self):
"""
@ -249,3 +251,4 @@ class BECDock(BECWidget, Dock):
"""
self.cleanup()
super().close()
self.parent_dock_area.dock_area.docks.pop(self.name(), None)

View File

@ -7,11 +7,18 @@ from pydantic import Field
from pyqtgraph.dockarea.DockArea import DockArea
from qtpy.QtCore import Qt
from qtpy.QtGui import QPainter, QPaintEvent
from qtpy.QtWidgets import QWidget
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.qt_utils.toolbar import (
ExpandableMenuAction,
IconAction,
ModularToolBar,
SeparatorAction,
)
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
from bec_widgets.utils.bec_widget import BECWidget
from ...qt_utils.error_popups import SafeSlot
from .dock import BECDock, DockConfig
@ -22,7 +29,7 @@ class DockAreaConfig(ConnectionConfig):
)
class BECDockArea(BECWidget, DockArea):
class BECDockArea(BECWidget, QWidget):
USER_ACCESS = [
"_config_dict",
"panels",
@ -51,15 +58,112 @@ class BECDockArea(BECWidget, DockArea):
config = DockAreaConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id)
DockArea.__init__(self, parent=parent)
QWidget.__init__(self, parent=parent)
self.layout = QVBoxLayout(self)
self.layout.setSpacing(5)
self.layout.setContentsMargins(0, 0, 0, 0)
self._instructions_visible = True
def paintEvent(self, event: QPaintEvent):
self.dock_area = DockArea()
self.toolbar = ModularToolBar(
actions={
"menu_plots": ExpandableMenuAction(
label="Add Plot ",
actions={
"waveform": IconAction(icon_path="waveform.svg", tooltip="Add Waveform"),
"image": IconAction(icon_path="image.svg", tooltip="Add Image"),
"motor_map": IconAction(icon_path="motor_map.svg", tooltip="Add Motor Map"),
},
),
"separator_0": SeparatorAction(),
"menu_devices": ExpandableMenuAction(
label="Add Device Control ",
actions={
"scan_control": IconAction(
icon_path="scan_control.svg", tooltip="Add Scan Control"
),
"device_box": IconAction(
icon_path="device_box.svg", tooltip="Add Device Box"
),
},
),
"separator_1": SeparatorAction(),
"menu_utils": ExpandableMenuAction(
label="Add Utils ",
actions={
"queue": IconAction(icon_path="queue.svg", tooltip="Add Scan Queue"),
"vs_code": IconAction(icon_path="terminal.svg", tooltip="Add VS Code"),
"status": IconAction(icon_path="status.svg", tooltip="Add BEC Status Box"),
"progress_bar": IconAction(
icon_path="progress.svg", tooltip="Add Circular ProgressBar"
),
},
),
"separator_2": SeparatorAction(),
"attach_all": IconAction(
icon_path="attach_all.svg", tooltip="Attach all floating docks"
),
"save_state": IconAction(icon_path="save_state.svg", tooltip="Save Dock State"),
"restore_state": IconAction(
icon_path="restore_state.svg", tooltip="Restore Dock State"
),
},
target_widget=self,
)
self.layout.addWidget(self.toolbar)
self.layout.addWidget(self.dock_area)
self._hook_toolbar()
def _hook_toolbar(self):
# Menu Plot
self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect(
lambda: self.add_dock(widget="BECWaveformWidget")
)
self.toolbar.widgets["menu_plots"].widgets["image"].triggered.connect(
lambda: self.add_dock(widget="BECImageWidget")
)
self.toolbar.widgets["menu_plots"].widgets["motor_map"].triggered.connect(
lambda: self.add_dock(widget="BECMotorMapWidget")
)
# Menu Devices
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
lambda: self.add_dock(widget="ScanControl")
)
self.toolbar.widgets["menu_devices"].widgets["device_box"].triggered.connect(
lambda: self.add_dock(widget="DeviceBox")
)
# Menu Utils
self.toolbar.widgets["menu_utils"].widgets["queue"].triggered.connect(
lambda: self.add_dock(widget="BECQueue")
)
self.toolbar.widgets["menu_utils"].widgets["status"].triggered.connect(
lambda: self.add_dock(widget="BECStatusBox")
)
self.toolbar.widgets["menu_utils"].widgets["vs_code"].triggered.connect(
lambda: self.add_dock(widget="VSCodeEditor")
)
self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect(
lambda: self.add_dock(widget="RingProgressBar")
)
# Icons
self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
self.toolbar.widgets["save_state"].action.triggered.connect(self.save_state)
self.toolbar.widgets["restore_state"].action.triggered.connect(self.restore_state)
def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions
super().paintEvent(event)
if self._instructions_visible:
painter = QPainter(self)
painter.drawText(self.rect(), Qt.AlignCenter, "Add docks using 'add_dock' method")
painter.drawText(
self.rect(),
Qt.AlignCenter,
"Add docks using 'add_dock' method from CLI\n or \n Add widget docks using the toolbar",
)
@property
def panels(self) -> dict[str, BECDock]:
@ -68,11 +172,11 @@ class BECDockArea(BECWidget, DockArea):
Returns:
dock_dict(dict): The docks in the dock area.
"""
return dict(self.docks)
return dict(self.dock_area.docks)
@panels.setter
def panels(self, value: dict[str, BECDock]):
self.docks = WeakValueDictionary(value)
self.dock_area.docks = WeakValueDictionary(value)
@property
def temp_areas(self) -> list:
@ -82,12 +186,13 @@ class BECDockArea(BECWidget, DockArea):
Returns:
list: The temporary areas in the dock area.
"""
return list(map(str, self.tempAreas))
return list(map(str, self.dock_area.tempAreas))
@temp_areas.setter
def temp_areas(self, value: list):
self.tempAreas = list(map(str, value))
self.dock_area.tempAreas = list(map(str, value))
@SafeSlot()
def restore_state(
self, state: dict = None, missing: Literal["ignore", "error"] = "ignore", extra="bottom"
):
@ -101,8 +206,9 @@ class BECDockArea(BECWidget, DockArea):
"""
if state is None:
state = self.config.docks_state
self.restoreState(state, missing=missing, extra=extra)
self.dock_area.restoreState(state, missing=missing, extra=extra)
@SafeSlot()
def save_state(self) -> dict:
"""
Save the state of the dock area.
@ -110,7 +216,7 @@ class BECDockArea(BECWidget, DockArea):
Returns:
dict: The state of the dock area.
"""
last_state = self.saveState()
last_state = self.dock_area.saveState()
self.config.docks_state = last_state
return last_state
@ -121,23 +227,24 @@ class BECDockArea(BECWidget, DockArea):
Args:
name(str): The name of the dock to remove.
"""
dock = self.docks.pop(name, None)
dock = self.dock_area.docks.pop(name, None)
self.config.docks.pop(name, None)
if dock:
dock.close()
if len(self.docks) <= 1:
for dock in self.docks.values():
if len(self.dock_area.docks) <= 1:
for dock in self.dock_area.docks.values():
dock.hide_title_bar()
else:
raise ValueError(f"Dock with name {name} does not exist.")
@SafeSlot(popup_error=True)
def add_dock(
self,
name: str = None,
position: Literal["bottom", "top", "left", "right", "above", "below"] = None,
relative_to: BECDock | None = None,
closable: bool = False,
closable: bool = True,
floating: bool = False,
prefix: str = "dock",
widget: str | QWidget | None = None,
@ -167,10 +274,10 @@ class BECDockArea(BECWidget, DockArea):
"""
if name is None:
name = WidgetContainerUtils.generate_unique_widget_id(
container=self.docks, prefix=prefix
container=self.dock_area.docks, prefix=prefix
)
if name in set(self.docks.keys()):
if name in set(self.dock_area.docks.keys()):
raise ValueError(f"Dock with name {name} already exists.")
if position is None:
@ -180,19 +287,21 @@ class BECDockArea(BECWidget, DockArea):
dock.config.position = position
self.config.docks[name] = dock.config
self.addDock(dock=dock, position=position, relativeTo=relative_to)
self.dock_area.addDock(dock=dock, position=position, relativeTo=relative_to)
if len(self.docks) <= 1:
if len(self.dock_area.docks) <= 1:
dock.hide_title_bar()
elif len(self.docks) > 1:
for dock in self.docks.values():
elif len(self.dock_area.docks) > 1:
for dock in self.dock_area.docks.values():
dock.show_title_bar()
if widget is not None and isinstance(widget, str):
dock.add_widget(widget=widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
elif widget is not None and isinstance(widget, QWidget):
dock.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if self._instructions_visible:
if (
self._instructions_visible
): # TODO still decide how initial instructions should be handled
self._instructions_visible = False
self.update()
if floating:
@ -209,26 +318,27 @@ class BECDockArea(BECWidget, DockArea):
Returns:
BECDock: The undocked dock.
"""
dock = self.docks[dock_name]
dock = self.dock_area.docks[dock_name]
dock.detach()
return dock
@SafeSlot()
def attach_all(self):
"""
Return all floating docks to the dock area.
"""
while self.tempAreas:
for temp_area in self.tempAreas:
self.removeTempArea(temp_area)
while self.dock_area.tempAreas:
for temp_area in self.dock_area.tempAreas:
self.dock_area.removeTempArea(temp_area)
def clear_all(self):
"""
Close all docks and remove all temp areas.
"""
self.attach_all()
for dock in dict(self.docks).values():
for dock in dict(self.dock_area.docks).values():
dock.remove()
self.docks.clear()
self.dock_area.docks.clear()
def cleanup(self):
"""

View File

@ -34,7 +34,7 @@ class BECWaveformWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
return "BEC Plots"
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "BECWaveformWidget.png")
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "waveform.png")
return QIcon(icon_path)
def includeFile(self):