bec-widgets 1.9.0__py3-none-any.whl → 1.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +16 -16
- PKG-INFO +1 -1
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +24 -19
- bec_widgets/utils/bec_designer.py +12 -5
- bec_widgets/widgets/containers/layout_manager/__init__.py +0 -0
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +881 -0
- {bec_widgets-1.9.0.dist-info → bec_widgets-1.10.0.dist-info}/METADATA +1 -1
- {bec_widgets-1.9.0.dist-info → bec_widgets-1.10.0.dist-info}/RECORD +12 -10
- pyproject.toml +1 -1
- {bec_widgets-1.9.0.dist-info → bec_widgets-1.10.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.9.0.dist-info → bec_widgets-1.10.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.9.0.dist-info → bec_widgets-1.10.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
3
|
|
4
|
+
## v1.10.0 (2024-12-10)
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
- **layout_manager**: Grid layout manager widget
|
9
|
+
([`17a63e3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17a63e3b639ecf6b41c379717d81339b04ef10f8))
|
10
|
+
|
11
|
+
|
12
|
+
## v1.9.1 (2024-12-10)
|
13
|
+
|
14
|
+
### Bug Fixes
|
15
|
+
|
16
|
+
- **designer**: General way to find python lib on linux
|
17
|
+
([`6563abf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6563abfddc9fc9baba6769022d6925545decdba9))
|
18
|
+
|
19
|
+
|
4
20
|
## v1.9.0 (2024-12-10)
|
5
21
|
|
6
22
|
### Features
|
@@ -185,9 +201,6 @@ Depending on the test, auto-updates are enabled or not.
|
|
185
201
|
- **crosshair**: Label of coordinates of TextItem is updated according to the current theme of qapp
|
186
202
|
([`4f31ea6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f31ea655cf6190e141e6a2720a2d6da517a2b5b))
|
187
203
|
|
188
|
-
- **crosshair**: Log is separately scaled for backend logic and for signal emit
|
189
|
-
([`b2eb71a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b2eb71aae0b6a7c82158f2d150ae1e31411cfdeb))
|
190
|
-
|
191
204
|
### Features
|
192
205
|
|
193
206
|
- **crosshair**: Textitem to display crosshair coordinates
|
@@ -197,16 +210,3 @@ Depending on the test, auto-updates are enabled or not.
|
|
197
210
|
|
198
211
|
- **crosshair**: Tests extended
|
199
212
|
([`64df805`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/64df805a9ed92bb97e580ac3bc0a1bbd2b1cb81e))
|
200
|
-
|
201
|
-
|
202
|
-
## v1.3.3 (2024-11-07)
|
203
|
-
|
204
|
-
### Bug Fixes
|
205
|
-
|
206
|
-
- **scan_control**: Devicelineedit kwargs readings changed to get name of the positioner
|
207
|
-
([`5fabd4b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5fabd4bea95bafd2352102686357cc1db80813fd))
|
208
|
-
|
209
|
-
### Documentation
|
210
|
-
|
211
|
-
- Update outdated text in docs
|
212
|
-
([`4f0693c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f0693cae34b391d75884837e1ae6353a0501868))
|
PKG-INFO
CHANGED
@@ -7,6 +7,7 @@ from qtpy.QtWidgets import (
|
|
7
7
|
QApplication,
|
8
8
|
QGroupBox,
|
9
9
|
QHBoxLayout,
|
10
|
+
QPushButton,
|
10
11
|
QSplitter,
|
11
12
|
QTabWidget,
|
12
13
|
QVBoxLayout,
|
@@ -17,6 +18,7 @@ from bec_widgets.utils import BECDispatcher
|
|
17
18
|
from bec_widgets.utils.colors import apply_theme
|
18
19
|
from bec_widgets.widgets.containers.dock import BECDockArea
|
19
20
|
from bec_widgets.widgets.containers.figure import BECFigure
|
21
|
+
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
20
22
|
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
21
23
|
|
22
24
|
|
@@ -50,11 +52,16 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
50
52
|
"d1": self.d1,
|
51
53
|
"d2": self.d2,
|
52
54
|
"wave": self.wf,
|
53
|
-
# "bar": self.bar,
|
54
|
-
# "cm": self.colormap,
|
55
55
|
"im": self.im,
|
56
56
|
"mm": self.mm,
|
57
57
|
"mw": self.mw,
|
58
|
+
"lm": self.lm,
|
59
|
+
"btn1": self.btn1,
|
60
|
+
"btn2": self.btn2,
|
61
|
+
"btn3": self.btn3,
|
62
|
+
"btn4": self.btn4,
|
63
|
+
"btn5": self.btn5,
|
64
|
+
"btn6": self.btn6,
|
58
65
|
}
|
59
66
|
)
|
60
67
|
|
@@ -79,11 +86,25 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
79
86
|
second_tab_layout.addWidget(self.figure)
|
80
87
|
tab_widget.addTab(second_tab, "BEC Figure")
|
81
88
|
|
89
|
+
third_tab = QWidget()
|
90
|
+
third_tab_layout = QVBoxLayout(third_tab)
|
91
|
+
self.lm = LayoutManagerWidget()
|
92
|
+
third_tab_layout.addWidget(self.lm)
|
93
|
+
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
94
|
+
|
82
95
|
group_box = QGroupBox("Jupyter Console", splitter)
|
83
96
|
group_box_layout = QVBoxLayout(group_box)
|
84
97
|
self.console = BECJupyterConsole(inprocess=True)
|
85
98
|
group_box_layout.addWidget(self.console)
|
86
99
|
|
100
|
+
# Some buttons for layout testing
|
101
|
+
self.btn1 = QPushButton("Button 1")
|
102
|
+
self.btn2 = QPushButton("Button 2")
|
103
|
+
self.btn3 = QPushButton("Button 3")
|
104
|
+
self.btn4 = QPushButton("Button 4")
|
105
|
+
self.btn5 = QPushButton("Button 5")
|
106
|
+
self.btn6 = QPushButton("Button 6")
|
107
|
+
|
87
108
|
# add stuff to figure
|
88
109
|
self._init_figure()
|
89
110
|
|
@@ -93,15 +114,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
93
114
|
self.setWindowTitle("Jupyter Console Window")
|
94
115
|
|
95
116
|
def _init_figure(self):
|
96
|
-
self.w1 = self.figure.plot(
|
97
|
-
x_name="samx",
|
98
|
-
y_name="bpm4i",
|
99
|
-
# title="Standard Plot with sync device, custom labels - w1",
|
100
|
-
# x_label="Motor Position",
|
101
|
-
# y_label="Intensity (A.U.)",
|
102
|
-
row=0,
|
103
|
-
col=0,
|
104
|
-
)
|
117
|
+
self.w1 = self.figure.plot(x_name="samx", y_name="bpm4i", row=0, col=0)
|
105
118
|
self.w1.set(
|
106
119
|
title="Standard Plot with sync device, custom labels - w1",
|
107
120
|
x_label="Motor Position",
|
@@ -169,14 +182,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
169
182
|
self.wf = self.d2.add_widget("BECFigure", row=0, col=0)
|
170
183
|
|
171
184
|
self.mw = self.wf.multi_waveform(monitor="waveform") # , config=config)
|
172
|
-
# self.wf.plot(x_name="samx", y_name="bpm3a")
|
173
|
-
# self.wf.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
174
|
-
# self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1)
|
175
|
-
# self.bar.set_diameter(200)
|
176
|
-
|
177
|
-
# self.d3 = self.dock.add_dock(name="dock_3", position="bottom")
|
178
|
-
# self.colormap = pg.GradientWidget()
|
179
|
-
# self.d3.add_widget(self.colormap, row=0, col=0)
|
180
185
|
|
181
186
|
self.dock.save_state()
|
182
187
|
|
@@ -93,17 +93,24 @@ def patch_designer(): # pragma: no cover
|
|
93
93
|
_extend_path_var("PATH", os.fspath(Path(sys._base_executable).parent), True)
|
94
94
|
else:
|
95
95
|
if sys.platform == "linux":
|
96
|
-
suffix = f"{sys.abiflags}.so"
|
97
96
|
env_var = "LD_PRELOAD"
|
97
|
+
current_pid = os.getpid()
|
98
|
+
with open(f"/proc/{current_pid}/maps", "rt") as f:
|
99
|
+
for line in f:
|
100
|
+
if "libpython" in line:
|
101
|
+
lib_path = line.split()[-1]
|
102
|
+
os.environ[env_var] = lib_path
|
103
|
+
break
|
104
|
+
|
98
105
|
elif sys.platform == "darwin":
|
99
106
|
suffix = ".dylib"
|
100
107
|
env_var = "DYLD_INSERT_LIBRARIES"
|
108
|
+
version = f"{major_version}.{minor_version}"
|
109
|
+
library_name = f"libpython{version}{suffix}"
|
110
|
+
lib_path = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
|
111
|
+
os.environ[env_var] = lib_path
|
101
112
|
else:
|
102
113
|
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
103
|
-
version = f"{major_version}.{minor_version}"
|
104
|
-
library_name = f"libpython{version}{suffix}"
|
105
|
-
lib_path = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
|
106
|
-
os.environ[env_var] = lib_path
|
107
114
|
|
108
115
|
if is_pyenv_python() or is_virtual_env():
|
109
116
|
# append all editable packages to the PYTHONPATH
|
File without changes
|
@@ -0,0 +1,881 @@
|
|
1
|
+
import math
|
2
|
+
import sys
|
3
|
+
from typing import Dict, Literal, Optional, Set, Tuple, Union
|
4
|
+
|
5
|
+
from qtpy.QtWidgets import (
|
6
|
+
QApplication,
|
7
|
+
QComboBox,
|
8
|
+
QGridLayout,
|
9
|
+
QGroupBox,
|
10
|
+
QHBoxLayout,
|
11
|
+
QLabel,
|
12
|
+
QLineEdit,
|
13
|
+
QMainWindow,
|
14
|
+
QMessageBox,
|
15
|
+
QPushButton,
|
16
|
+
QSpinBox,
|
17
|
+
QSplitter,
|
18
|
+
QVBoxLayout,
|
19
|
+
QWidget,
|
20
|
+
)
|
21
|
+
from typeguard import typechecked
|
22
|
+
|
23
|
+
from bec_widgets.cli.rpc_wigdet_handler import widget_handler
|
24
|
+
|
25
|
+
|
26
|
+
class LayoutManagerWidget(QWidget):
|
27
|
+
"""
|
28
|
+
A robust layout manager that extends QGridLayout functionality, allowing
|
29
|
+
users to add/remove widgets, access widgets by coordinates, shift widgets,
|
30
|
+
and change the layout dynamically with automatic reindexing to keep the grid compact.
|
31
|
+
|
32
|
+
Supports adding widgets via QWidget instances or string identifiers referencing the widget handler.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, parent=None, auto_reindex=True):
|
36
|
+
super().__init__(parent)
|
37
|
+
self.layout = QGridLayout(self)
|
38
|
+
self.auto_reindex = auto_reindex
|
39
|
+
|
40
|
+
# Mapping from widget to its position (row, col, rowspan, colspan)
|
41
|
+
self.widget_positions: Dict[QWidget, Tuple[int, int, int, int]] = {}
|
42
|
+
|
43
|
+
# Mapping from (row, col) to widget
|
44
|
+
self.position_widgets: Dict[Tuple[int, int], QWidget] = {}
|
45
|
+
|
46
|
+
# Keep track of the current position for automatic placement
|
47
|
+
self.current_row = 0
|
48
|
+
self.current_col = 0
|
49
|
+
|
50
|
+
def add_widget(
|
51
|
+
self,
|
52
|
+
widget: QWidget | str,
|
53
|
+
row: int | None = None,
|
54
|
+
col: Optional[int] = None,
|
55
|
+
rowspan: int = 1,
|
56
|
+
colspan: int = 1,
|
57
|
+
shift_existing: bool = True,
|
58
|
+
shift_direction: Literal["down", "up", "left", "right"] = "right",
|
59
|
+
) -> QWidget:
|
60
|
+
"""
|
61
|
+
Add a widget to the grid with enhanced shifting capabilities.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
widget (QWidget | str): The widget to add. If str, it is used to create a widget via widget_handler.
|
65
|
+
row (int, optional): The row to add the widget to. If None, the next available row is used.
|
66
|
+
col (int, optional): The column to add the widget to. If None, the next available column is used.
|
67
|
+
rowspan (int): Number of rows the widget spans. Default is 1.
|
68
|
+
colspan (int): Number of columns the widget spans. Default is 1.
|
69
|
+
shift_existing (bool): Whether to shift existing widgets if the target position is occupied. Default is True.
|
70
|
+
shift_direction (Literal["down", "up", "left", "right"]): Direction to shift existing widgets. Default is "right".
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
QWidget: The widget that was added.
|
74
|
+
"""
|
75
|
+
# Handle widget creation if a BECWidget string identifier is provided
|
76
|
+
if isinstance(widget, str):
|
77
|
+
widget = widget_handler.create_widget(widget)
|
78
|
+
|
79
|
+
if row is None:
|
80
|
+
row = self.current_row
|
81
|
+
if col is None:
|
82
|
+
col = self.current_col
|
83
|
+
|
84
|
+
if (row, col) in self.position_widgets:
|
85
|
+
if shift_existing:
|
86
|
+
# Attempt to shift the existing widget in the specified direction
|
87
|
+
self.shift_widgets(direction=shift_direction, start_row=row, start_col=col)
|
88
|
+
else:
|
89
|
+
raise ValueError(f"Position ({row}, {col}) is already occupied.")
|
90
|
+
|
91
|
+
# Add the widget to the layout
|
92
|
+
self.layout.addWidget(widget, row, col, rowspan, colspan)
|
93
|
+
self.widget_positions[widget] = (row, col, rowspan, colspan)
|
94
|
+
self.position_widgets[(row, col)] = widget
|
95
|
+
|
96
|
+
# Update current position for automatic placement
|
97
|
+
self.current_col = col + colspan
|
98
|
+
self.current_row = max(self.current_row, row)
|
99
|
+
|
100
|
+
if self.auto_reindex:
|
101
|
+
self.reindex_grid()
|
102
|
+
|
103
|
+
return widget
|
104
|
+
|
105
|
+
def add_widget_relative(
|
106
|
+
self,
|
107
|
+
widget: QWidget | str,
|
108
|
+
reference_widget: QWidget,
|
109
|
+
position: Literal["left", "right", "top", "bottom"],
|
110
|
+
rowspan: int = 1,
|
111
|
+
colspan: int = 1,
|
112
|
+
shift_existing: bool = True,
|
113
|
+
shift_direction: Literal["down", "up", "left", "right"] = "right",
|
114
|
+
) -> QWidget:
|
115
|
+
"""
|
116
|
+
Add a widget relative to an existing widget.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
widget (QWidget | str): The widget to add. If str, it is used to create a widget via widget_handler.
|
120
|
+
reference_widget (QWidget): The widget relative to which the new widget will be placed.
|
121
|
+
position (Literal["left", "right", "top", "bottom"]): Position relative to the reference widget.
|
122
|
+
rowspan (int): Number of rows the widget spans. Default is 1.
|
123
|
+
colspan (int): Number of columns the widget spans. Default is 1.
|
124
|
+
shift_existing (bool): Whether to shift existing widgets if the target position is occupied.
|
125
|
+
shift_direction (Literal["down", "up", "left", "right"]): Direction to shift existing widgets.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
QWidget: The widget that was added.
|
129
|
+
|
130
|
+
Raises:
|
131
|
+
ValueError: If the reference widget is not found.
|
132
|
+
"""
|
133
|
+
if reference_widget not in self.widget_positions:
|
134
|
+
raise ValueError("Reference widget not found in layout.")
|
135
|
+
|
136
|
+
ref_row, ref_col, ref_rowspan, ref_colspan = self.widget_positions[reference_widget]
|
137
|
+
|
138
|
+
# Determine new widget position based on the specified relative position
|
139
|
+
if position == "left":
|
140
|
+
new_row = ref_row
|
141
|
+
new_col = ref_col - 1
|
142
|
+
elif position == "right":
|
143
|
+
new_row = ref_row
|
144
|
+
new_col = ref_col + ref_colspan
|
145
|
+
elif position == "top":
|
146
|
+
new_row = ref_row - 1
|
147
|
+
new_col = ref_col
|
148
|
+
elif position == "bottom":
|
149
|
+
new_row = ref_row + ref_rowspan
|
150
|
+
new_col = ref_col
|
151
|
+
else:
|
152
|
+
raise ValueError("Invalid position. Choose from 'left', 'right', 'top', 'bottom'.")
|
153
|
+
|
154
|
+
# Add the widget at the calculated position
|
155
|
+
return self.add_widget(
|
156
|
+
widget=widget,
|
157
|
+
row=new_row,
|
158
|
+
col=new_col,
|
159
|
+
rowspan=rowspan,
|
160
|
+
colspan=colspan,
|
161
|
+
shift_existing=shift_existing,
|
162
|
+
shift_direction=shift_direction,
|
163
|
+
)
|
164
|
+
|
165
|
+
def move_widget_by_coords(
|
166
|
+
self,
|
167
|
+
current_row: int,
|
168
|
+
current_col: int,
|
169
|
+
new_row: int,
|
170
|
+
new_col: int,
|
171
|
+
shift: bool = True,
|
172
|
+
shift_direction: Literal["down", "up", "left", "right"] = "right",
|
173
|
+
) -> None:
|
174
|
+
"""
|
175
|
+
Move a widget from (current_row, current_col) to (new_row, new_col).
|
176
|
+
|
177
|
+
Args:
|
178
|
+
current_row (int): Current row of the widget.
|
179
|
+
current_col (int): Current column of the widget.
|
180
|
+
new_row (int): Target row.
|
181
|
+
new_col (int): Target column.
|
182
|
+
shift (bool): Whether to shift existing widgets if the target position is occupied.
|
183
|
+
shift_direction (Literal["down", "up", "left", "right"]): Direction to shift existing widgets.
|
184
|
+
|
185
|
+
Raises:
|
186
|
+
ValueError: If the widget is not found or target position is invalid.
|
187
|
+
"""
|
188
|
+
self.move_widget(
|
189
|
+
old_row=current_row,
|
190
|
+
old_col=current_col,
|
191
|
+
new_row=new_row,
|
192
|
+
new_col=new_col,
|
193
|
+
shift=shift,
|
194
|
+
shift_direction=shift_direction,
|
195
|
+
)
|
196
|
+
|
197
|
+
@typechecked
|
198
|
+
def move_widget_by_object(
|
199
|
+
self,
|
200
|
+
widget: QWidget,
|
201
|
+
new_row: int,
|
202
|
+
new_col: int,
|
203
|
+
shift: bool = True,
|
204
|
+
shift_direction: Literal["down", "up", "left", "right"] = "right",
|
205
|
+
) -> None:
|
206
|
+
"""
|
207
|
+
Move a widget to a new position using the widget object.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
widget (QWidget): The widget to move.
|
211
|
+
new_row (int): Target row.
|
212
|
+
new_col (int): Target column.
|
213
|
+
shift (bool): Whether to shift existing widgets if the target position is occupied.
|
214
|
+
shift_direction (Literal["down", "up", "left", "right"]): Direction to shift existing widgets.
|
215
|
+
|
216
|
+
Raises:
|
217
|
+
ValueError: If the widget is not found or target position is invalid.
|
218
|
+
"""
|
219
|
+
if widget not in self.widget_positions:
|
220
|
+
raise ValueError("Widget not found in layout.")
|
221
|
+
|
222
|
+
old_position = self.widget_positions[widget]
|
223
|
+
old_row, old_col = old_position[0], old_position[1]
|
224
|
+
|
225
|
+
self.move_widget(
|
226
|
+
old_row=old_row,
|
227
|
+
old_col=old_col,
|
228
|
+
new_row=new_row,
|
229
|
+
new_col=new_col,
|
230
|
+
shift=shift,
|
231
|
+
shift_direction=shift_direction,
|
232
|
+
)
|
233
|
+
|
234
|
+
@typechecked
|
235
|
+
def move_widget(
|
236
|
+
self,
|
237
|
+
old_row: int | None = None,
|
238
|
+
old_col: int | None = None,
|
239
|
+
new_row: int | None = None,
|
240
|
+
new_col: int | None = None,
|
241
|
+
shift: bool = True,
|
242
|
+
shift_direction: Literal["down", "up", "left", "right"] = "right",
|
243
|
+
) -> None:
|
244
|
+
"""
|
245
|
+
Move a widget to a new position. If the new position is occupied and shift is True,
|
246
|
+
shift the existing widget to the specified direction.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
old_row (int, optional): The current row of the widget.
|
250
|
+
old_col (int, optional): The current column of the widget.
|
251
|
+
new_row (int, optional): The target row to move the widget to.
|
252
|
+
new_col (int, optional): The target column to move the widget to.
|
253
|
+
shift (bool): Whether to shift existing widgets if the target position is occupied.
|
254
|
+
shift_direction (Literal["down", "up", "left", "right"]): Direction to shift existing widgets.
|
255
|
+
|
256
|
+
Raises:
|
257
|
+
ValueError: If the widget is not found or target position is invalid.
|
258
|
+
"""
|
259
|
+
if new_row is None or new_col is None:
|
260
|
+
raise ValueError("Must provide both new_row and new_col to move a widget.")
|
261
|
+
|
262
|
+
if old_row is None and old_col is None:
|
263
|
+
raise ValueError(f"No widget found at position ({old_row}, {old_col}).")
|
264
|
+
widget = self.get_widget(old_row, old_col)
|
265
|
+
|
266
|
+
if (new_row, new_col) in self.position_widgets:
|
267
|
+
if not shift:
|
268
|
+
raise ValueError(f"Position ({new_row}, {new_col}) is already occupied.")
|
269
|
+
# Shift the existing widget to make space
|
270
|
+
self.shift_widgets(
|
271
|
+
direction=shift_direction,
|
272
|
+
start_row=new_row if shift_direction in ["down", "up"] else 0,
|
273
|
+
start_col=new_col if shift_direction in ["left", "right"] else 0,
|
274
|
+
)
|
275
|
+
|
276
|
+
# Proceed to move the widget
|
277
|
+
self.layout.removeWidget(widget)
|
278
|
+
old_position = self.widget_positions.pop(widget)
|
279
|
+
self.position_widgets.pop((old_position[0], old_position[1]))
|
280
|
+
|
281
|
+
self.layout.addWidget(widget, new_row, new_col, old_position[2], old_position[3])
|
282
|
+
self.widget_positions[widget] = (new_row, new_col, old_position[2], old_position[3])
|
283
|
+
self.position_widgets[(new_row, new_col)] = widget
|
284
|
+
|
285
|
+
# Update current_row and current_col for automatic placement if needed
|
286
|
+
self.current_row = max(self.current_row, new_row)
|
287
|
+
self.current_col = max(self.current_col, new_col + old_position[3])
|
288
|
+
|
289
|
+
if self.auto_reindex:
|
290
|
+
self.reindex_grid()
|
291
|
+
|
292
|
+
@typechecked
|
293
|
+
def shift_widgets(
|
294
|
+
self,
|
295
|
+
direction: Literal["down", "up", "left", "right"],
|
296
|
+
start_row: int = 0,
|
297
|
+
start_col: int = 0,
|
298
|
+
) -> None:
|
299
|
+
"""
|
300
|
+
Shift widgets in the grid in the specified direction starting from the given position.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
direction (Literal["down", "up", "left", "right"]): Direction to shift widgets.
|
304
|
+
start_row (int): Starting row index.
|
305
|
+
start_col (int): Starting column index.
|
306
|
+
|
307
|
+
Raises:
|
308
|
+
ValueError: If shifting causes widgets to go out of grid boundaries.
|
309
|
+
"""
|
310
|
+
shifts = []
|
311
|
+
positions_to_shift = [(start_row, start_col)]
|
312
|
+
visited_positions = set()
|
313
|
+
|
314
|
+
while positions_to_shift:
|
315
|
+
row, col = positions_to_shift.pop(0)
|
316
|
+
if (row, col) in visited_positions:
|
317
|
+
continue
|
318
|
+
visited_positions.add((row, col))
|
319
|
+
|
320
|
+
widget = self.position_widgets.get((row, col))
|
321
|
+
if widget is None:
|
322
|
+
continue # No widget at this position
|
323
|
+
|
324
|
+
# Compute new position based on the direction
|
325
|
+
if direction == "down":
|
326
|
+
new_row = row + 1
|
327
|
+
new_col = col
|
328
|
+
elif direction == "up":
|
329
|
+
new_row = row - 1
|
330
|
+
new_col = col
|
331
|
+
elif direction == "right":
|
332
|
+
new_row = row
|
333
|
+
new_col = col + 1
|
334
|
+
elif direction == "left":
|
335
|
+
new_row = row
|
336
|
+
new_col = col - 1
|
337
|
+
|
338
|
+
# Check for negative indices
|
339
|
+
if new_row < 0 or new_col < 0:
|
340
|
+
raise ValueError("Shifting widgets out of grid boundaries.")
|
341
|
+
|
342
|
+
# If the new position is occupied, add it to the positions to shift
|
343
|
+
if (new_row, new_col) in self.position_widgets:
|
344
|
+
positions_to_shift.append((new_row, new_col))
|
345
|
+
|
346
|
+
shifts.append(
|
347
|
+
(widget, (row, col), (new_row, new_col), self.widget_positions[widget][2:])
|
348
|
+
)
|
349
|
+
|
350
|
+
# Remove all widgets from their old positions
|
351
|
+
for widget, (old_row, old_col), _, _ in shifts:
|
352
|
+
self.layout.removeWidget(widget)
|
353
|
+
self.position_widgets.pop((old_row, old_col))
|
354
|
+
|
355
|
+
# Add widgets to their new positions
|
356
|
+
for widget, _, (new_row, new_col), (rowspan, colspan) in shifts:
|
357
|
+
self.layout.addWidget(widget, new_row, new_col, rowspan, colspan)
|
358
|
+
self.widget_positions[widget] = (new_row, new_col, rowspan, colspan)
|
359
|
+
self.position_widgets[(new_row, new_col)] = widget
|
360
|
+
|
361
|
+
# Update current_row and current_col if needed
|
362
|
+
self.current_row = max(self.current_row, new_row)
|
363
|
+
self.current_col = max(self.current_col, new_col + colspan)
|
364
|
+
|
365
|
+
def shift_all_widgets(self, direction: Literal["down", "up", "left", "right"]) -> None:
|
366
|
+
"""
|
367
|
+
Shift all widgets in the grid in the specified direction to make room and prevent negative indices.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
direction (Literal["down", "up", "left", "right"]): Direction to shift all widgets.
|
371
|
+
"""
|
372
|
+
# First, collect all the shifts to perform
|
373
|
+
shifts = []
|
374
|
+
for widget, (row, col, rowspan, colspan) in self.widget_positions.items():
|
375
|
+
|
376
|
+
if direction == "down":
|
377
|
+
new_row = row + 1
|
378
|
+
new_col = col
|
379
|
+
elif direction == "up":
|
380
|
+
new_row = row - 1
|
381
|
+
new_col = col
|
382
|
+
elif direction == "right":
|
383
|
+
new_row = row
|
384
|
+
new_col = col + 1
|
385
|
+
elif direction == "left":
|
386
|
+
new_row = row
|
387
|
+
new_col = col - 1
|
388
|
+
|
389
|
+
# Check for negative indices
|
390
|
+
if new_row < 0 or new_col < 0:
|
391
|
+
raise ValueError("Shifting widgets out of grid boundaries.")
|
392
|
+
|
393
|
+
shifts.append((widget, (row, col), (new_row, new_col), (rowspan, colspan)))
|
394
|
+
|
395
|
+
# Now perform the shifts
|
396
|
+
for widget, (old_row, old_col), (new_row, new_col), (rowspan, colspan) in shifts:
|
397
|
+
self.layout.removeWidget(widget)
|
398
|
+
self.position_widgets.pop((old_row, old_col))
|
399
|
+
|
400
|
+
for widget, (old_row, old_col), (new_row, new_col), (rowspan, colspan) in shifts:
|
401
|
+
self.layout.addWidget(widget, new_row, new_col, rowspan, colspan)
|
402
|
+
self.widget_positions[widget] = (new_row, new_col, rowspan, colspan)
|
403
|
+
self.position_widgets[(new_row, new_col)] = widget
|
404
|
+
|
405
|
+
# Update current_row and current_col based on new widget positions
|
406
|
+
self.current_row = max((pos[0] for pos in self.position_widgets.keys()), default=0)
|
407
|
+
self.current_col = max((pos[1] for pos in self.position_widgets.keys()), default=0)
|
408
|
+
|
409
|
+
def remove(
|
410
|
+
self,
|
411
|
+
row: Optional[int] = None,
|
412
|
+
col: Optional[int] = None,
|
413
|
+
coordinates: Optional[Tuple[int, int]] = None,
|
414
|
+
) -> None:
|
415
|
+
"""
|
416
|
+
Remove a widget from the layout. Can be removed by widget ID or by coordinates.
|
417
|
+
|
418
|
+
Args:
|
419
|
+
row (int, optional): The row coordinate of the widget to remove.
|
420
|
+
col (int, optional): The column coordinate of the widget to remove.
|
421
|
+
coordinates (tuple[int, int], optional): The (row, col) coordinates of the widget to remove.
|
422
|
+
|
423
|
+
Raises:
|
424
|
+
ValueError: If the widget to remove is not found.
|
425
|
+
"""
|
426
|
+
if coordinates:
|
427
|
+
row, col = coordinates
|
428
|
+
widget = self.get_widget(row, col)
|
429
|
+
if widget is None:
|
430
|
+
raise ValueError(f"No widget found at coordinates {coordinates}.")
|
431
|
+
elif row is not None and col is not None:
|
432
|
+
widget = self.get_widget(row, col)
|
433
|
+
if widget is None:
|
434
|
+
raise ValueError(f"No widget found at position ({row}, {col}).")
|
435
|
+
else:
|
436
|
+
raise ValueError(
|
437
|
+
"Must provide either widget_id, coordinates, or both row and col for removal."
|
438
|
+
)
|
439
|
+
|
440
|
+
self.remove_widget(widget)
|
441
|
+
|
442
|
+
def remove_widget(self, widget: QWidget) -> None:
|
443
|
+
"""
|
444
|
+
Remove a widget from the grid and reindex the grid to keep it compact.
|
445
|
+
|
446
|
+
Args:
|
447
|
+
widget (QWidget): The widget to remove.
|
448
|
+
|
449
|
+
Raises:
|
450
|
+
ValueError: If the widget is not found in the layout.
|
451
|
+
"""
|
452
|
+
if widget not in self.widget_positions:
|
453
|
+
raise ValueError("Widget not found in layout.")
|
454
|
+
|
455
|
+
position = self.widget_positions.pop(widget)
|
456
|
+
self.position_widgets.pop((position[0], position[1]))
|
457
|
+
self.layout.removeWidget(widget)
|
458
|
+
widget.setParent(None) # Remove widget from the parent
|
459
|
+
widget.deleteLater()
|
460
|
+
|
461
|
+
# Reindex the grid to maintain compactness
|
462
|
+
if self.auto_reindex:
|
463
|
+
self.reindex_grid()
|
464
|
+
|
465
|
+
def get_widget(self, row: int, col: int) -> QWidget | None:
|
466
|
+
"""
|
467
|
+
Get the widget at the specified position.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
row (int): The row coordinate.
|
471
|
+
col (int): The column coordinate.
|
472
|
+
|
473
|
+
Returns:
|
474
|
+
QWidget | None: The widget at the specified position, or None if empty.
|
475
|
+
"""
|
476
|
+
return self.position_widgets.get((row, col))
|
477
|
+
|
478
|
+
def get_widget_position(self, widget: QWidget) -> Tuple[int, int, int, int] | None:
|
479
|
+
"""
|
480
|
+
Get the position of the specified widget.
|
481
|
+
|
482
|
+
Args:
|
483
|
+
widget (QWidget): The widget to query.
|
484
|
+
|
485
|
+
Returns:
|
486
|
+
Tuple[int, int, int, int] | None: The (row, col, rowspan, colspan) tuple, or None if not found.
|
487
|
+
"""
|
488
|
+
return self.widget_positions.get(widget)
|
489
|
+
|
490
|
+
def change_layout(self, num_rows: int | None = None, num_cols: int | None = None) -> None:
|
491
|
+
"""
|
492
|
+
Change the layout to have a certain number of rows and/or columns,
|
493
|
+
rearranging the widgets accordingly.
|
494
|
+
|
495
|
+
If only one of num_rows or num_cols is provided, the other is calculated automatically
|
496
|
+
based on the number of widgets and the provided constraint.
|
497
|
+
|
498
|
+
If both are provided, num_rows is calculated based on num_cols.
|
499
|
+
|
500
|
+
Args:
|
501
|
+
num_rows (int | None): The new maximum number of rows.
|
502
|
+
num_cols (int | None): The new maximum number of columns.
|
503
|
+
"""
|
504
|
+
if num_rows is None and num_cols is None:
|
505
|
+
return # Nothing to change
|
506
|
+
|
507
|
+
total_widgets = len(self.widget_positions)
|
508
|
+
|
509
|
+
if num_cols is not None:
|
510
|
+
# Calculate num_rows based on num_cols
|
511
|
+
num_rows = math.ceil(total_widgets / num_cols)
|
512
|
+
elif num_rows is not None:
|
513
|
+
# Calculate num_cols based on num_rows
|
514
|
+
num_cols = math.ceil(total_widgets / num_rows)
|
515
|
+
|
516
|
+
# Sort widgets by current position (row-major order)
|
517
|
+
widgets_sorted = sorted(
|
518
|
+
self.widget_positions.items(),
|
519
|
+
key=lambda item: (item[1][0], item[1][1]), # Sort by row, then column
|
520
|
+
)
|
521
|
+
|
522
|
+
# Clear the layout without deleting widgets
|
523
|
+
for widget, _ in widgets_sorted:
|
524
|
+
self.layout.removeWidget(widget)
|
525
|
+
|
526
|
+
# Reset position mappings
|
527
|
+
self.widget_positions.clear()
|
528
|
+
self.position_widgets.clear()
|
529
|
+
|
530
|
+
# Re-add widgets based on new layout constraints
|
531
|
+
current_row, current_col = 0, 0
|
532
|
+
for widget, _ in widgets_sorted:
|
533
|
+
if current_col >= num_cols:
|
534
|
+
current_col = 0
|
535
|
+
current_row += 1
|
536
|
+
self.layout.addWidget(widget, current_row, current_col, 1, 1)
|
537
|
+
self.widget_positions[widget] = (current_row, current_col, 1, 1)
|
538
|
+
self.position_widgets[(current_row, current_col)] = widget
|
539
|
+
current_col += 1
|
540
|
+
|
541
|
+
# Update current_row and current_col for automatic placement
|
542
|
+
self.current_row = current_row
|
543
|
+
self.current_col = current_col
|
544
|
+
|
545
|
+
# Reindex the grid to ensure compactness
|
546
|
+
self.reindex_grid()
|
547
|
+
|
548
|
+
def clear_layout(self) -> None:
|
549
|
+
"""
|
550
|
+
Remove all widgets from the layout without deleting them.
|
551
|
+
"""
|
552
|
+
for widget in list(self.widget_positions):
|
553
|
+
self.layout.removeWidget(widget)
|
554
|
+
self.position_widgets.pop(
|
555
|
+
(self.widget_positions[widget][0], self.widget_positions[widget][1])
|
556
|
+
)
|
557
|
+
self.widget_positions.pop(widget)
|
558
|
+
widget.setParent(None) # Optionally hide/remove the widget
|
559
|
+
|
560
|
+
self.current_row = 0
|
561
|
+
self.current_col = 0
|
562
|
+
|
563
|
+
def reindex_grid(self) -> None:
|
564
|
+
"""
|
565
|
+
Reindex the grid to remove empty rows and columns, ensuring that
|
566
|
+
widget coordinates are contiguous and start from (0, 0).
|
567
|
+
"""
|
568
|
+
# Step 1: Collect all occupied positions
|
569
|
+
occupied_positions = sorted(self.position_widgets.keys())
|
570
|
+
|
571
|
+
if not occupied_positions:
|
572
|
+
# No widgets to reindex
|
573
|
+
self.clear_layout()
|
574
|
+
return
|
575
|
+
|
576
|
+
# Step 2: Determine the new mapping by eliminating empty columns and rows
|
577
|
+
# Find unique rows and columns
|
578
|
+
unique_rows = sorted(set(pos[0] for pos in occupied_positions))
|
579
|
+
unique_cols = sorted(set(pos[1] for pos in occupied_positions))
|
580
|
+
|
581
|
+
# Create mappings from old to new indices
|
582
|
+
row_mapping = {old_row: new_row for new_row, old_row in enumerate(unique_rows)}
|
583
|
+
col_mapping = {old_col: new_col for new_col, old_col in enumerate(unique_cols)}
|
584
|
+
|
585
|
+
# Step 3: Collect widgets with their new positions
|
586
|
+
widgets_with_new_positions = []
|
587
|
+
for widget, (row, col, rowspan, colspan) in self.widget_positions.items():
|
588
|
+
new_row = row_mapping[row]
|
589
|
+
new_col = col_mapping[col]
|
590
|
+
widgets_with_new_positions.append((widget, new_row, new_col, rowspan, colspan))
|
591
|
+
|
592
|
+
# Step 4: Clear the layout and reset mappings
|
593
|
+
self.clear_layout()
|
594
|
+
|
595
|
+
# Reset current_row and current_col
|
596
|
+
self.current_row = 0
|
597
|
+
self.current_col = 0
|
598
|
+
|
599
|
+
# Step 5: Re-add widgets with new positions
|
600
|
+
for widget, new_row, new_col, rowspan, colspan in widgets_with_new_positions:
|
601
|
+
self.layout.addWidget(widget, new_row, new_col, rowspan, colspan)
|
602
|
+
self.widget_positions[widget] = (new_row, new_col, rowspan, colspan)
|
603
|
+
self.position_widgets[(new_row, new_col)] = widget
|
604
|
+
|
605
|
+
# Update current position for automatic placement
|
606
|
+
self.current_col = max(self.current_col, new_col + colspan)
|
607
|
+
self.current_row = max(self.current_row, new_row)
|
608
|
+
|
609
|
+
def get_widgets_positions(self) -> Dict[QWidget, Tuple[int, int, int, int]]:
|
610
|
+
"""
|
611
|
+
Get the positions of all widgets in the layout.
|
612
|
+
|
613
|
+
Returns:
|
614
|
+
Dict[QWidget, Tuple[int, int, int, int]]: Mapping of widgets to their (row, col, rowspan, colspan).
|
615
|
+
"""
|
616
|
+
return self.widget_positions.copy()
|
617
|
+
|
618
|
+
def print_all_button_text(self):
|
619
|
+
"""Debug function to print the text of all QPushButton widgets."""
|
620
|
+
print("Coordinates - Button Text")
|
621
|
+
for coord, widget in self.position_widgets.items():
|
622
|
+
if isinstance(widget, QPushButton):
|
623
|
+
print(f"{coord} - {widget.text()}")
|
624
|
+
|
625
|
+
|
626
|
+
####################################################################################################
|
627
|
+
# The following code is for the GUI control panel to interact with the LayoutManagerWidget.
|
628
|
+
# It is not covered by any tests as it serves only as an example for the LayoutManagerWidget class.
|
629
|
+
####################################################################################################
|
630
|
+
|
631
|
+
|
632
|
+
class ControlPanel(QWidget): # pragma: no cover
|
633
|
+
def __init__(self, layout_manager: LayoutManagerWidget):
|
634
|
+
super().__init__()
|
635
|
+
self.layout_manager = layout_manager
|
636
|
+
self.init_ui()
|
637
|
+
|
638
|
+
def init_ui(self):
|
639
|
+
main_layout = QVBoxLayout()
|
640
|
+
|
641
|
+
# Add Widget by Coordinates
|
642
|
+
add_coord_group = QGroupBox("Add Widget by Coordinates")
|
643
|
+
add_coord_layout = QGridLayout()
|
644
|
+
|
645
|
+
add_coord_layout.addWidget(QLabel("Text:"), 0, 0)
|
646
|
+
self.text_input = QLineEdit()
|
647
|
+
add_coord_layout.addWidget(self.text_input, 0, 1)
|
648
|
+
|
649
|
+
add_coord_layout.addWidget(QLabel("Row:"), 1, 0)
|
650
|
+
self.row_input = QSpinBox()
|
651
|
+
self.row_input.setMinimum(0)
|
652
|
+
add_coord_layout.addWidget(self.row_input, 1, 1)
|
653
|
+
|
654
|
+
add_coord_layout.addWidget(QLabel("Column:"), 2, 0)
|
655
|
+
self.col_input = QSpinBox()
|
656
|
+
self.col_input.setMinimum(0)
|
657
|
+
add_coord_layout.addWidget(self.col_input, 2, 1)
|
658
|
+
|
659
|
+
self.add_button = QPushButton("Add at Coordinates")
|
660
|
+
self.add_button.clicked.connect(self.add_at_coordinates)
|
661
|
+
add_coord_layout.addWidget(self.add_button, 3, 0, 1, 2)
|
662
|
+
|
663
|
+
add_coord_group.setLayout(add_coord_layout)
|
664
|
+
main_layout.addWidget(add_coord_group)
|
665
|
+
|
666
|
+
# Add Widget Relative
|
667
|
+
add_rel_group = QGroupBox("Add Widget Relative to Existing")
|
668
|
+
add_rel_layout = QGridLayout()
|
669
|
+
|
670
|
+
add_rel_layout.addWidget(QLabel("Text:"), 0, 0)
|
671
|
+
self.rel_text_input = QLineEdit()
|
672
|
+
add_rel_layout.addWidget(self.rel_text_input, 0, 1)
|
673
|
+
|
674
|
+
add_rel_layout.addWidget(QLabel("Reference Widget:"), 1, 0)
|
675
|
+
self.ref_widget_combo = QComboBox()
|
676
|
+
add_rel_layout.addWidget(self.ref_widget_combo, 1, 1)
|
677
|
+
|
678
|
+
add_rel_layout.addWidget(QLabel("Position:"), 2, 0)
|
679
|
+
self.position_combo = QComboBox()
|
680
|
+
self.position_combo.addItems(["left", "right", "top", "bottom"])
|
681
|
+
add_rel_layout.addWidget(self.position_combo, 2, 1)
|
682
|
+
|
683
|
+
self.add_rel_button = QPushButton("Add Relative")
|
684
|
+
self.add_rel_button.clicked.connect(self.add_relative)
|
685
|
+
add_rel_layout.addWidget(self.add_rel_button, 3, 0, 1, 2)
|
686
|
+
|
687
|
+
add_rel_group.setLayout(add_rel_layout)
|
688
|
+
main_layout.addWidget(add_rel_group)
|
689
|
+
|
690
|
+
# Remove Widget
|
691
|
+
remove_group = QGroupBox("Remove Widget")
|
692
|
+
remove_layout = QGridLayout()
|
693
|
+
|
694
|
+
remove_layout.addWidget(QLabel("Row:"), 0, 0)
|
695
|
+
self.remove_row_input = QSpinBox()
|
696
|
+
self.remove_row_input.setMinimum(0)
|
697
|
+
remove_layout.addWidget(self.remove_row_input, 0, 1)
|
698
|
+
|
699
|
+
remove_layout.addWidget(QLabel("Column:"), 1, 0)
|
700
|
+
self.remove_col_input = QSpinBox()
|
701
|
+
self.remove_col_input.setMinimum(0)
|
702
|
+
remove_layout.addWidget(self.remove_col_input, 1, 1)
|
703
|
+
|
704
|
+
self.remove_button = QPushButton("Remove at Coordinates")
|
705
|
+
self.remove_button.clicked.connect(self.remove_widget)
|
706
|
+
remove_layout.addWidget(self.remove_button, 2, 0, 1, 2)
|
707
|
+
|
708
|
+
remove_group.setLayout(remove_layout)
|
709
|
+
main_layout.addWidget(remove_group)
|
710
|
+
|
711
|
+
# Change Layout
|
712
|
+
change_layout_group = QGroupBox("Change Layout")
|
713
|
+
change_layout_layout = QGridLayout()
|
714
|
+
|
715
|
+
change_layout_layout.addWidget(QLabel("Number of Rows:"), 0, 0)
|
716
|
+
self.change_rows_input = QSpinBox()
|
717
|
+
self.change_rows_input.setMinimum(1)
|
718
|
+
self.change_rows_input.setValue(1) # Default value
|
719
|
+
change_layout_layout.addWidget(self.change_rows_input, 0, 1)
|
720
|
+
|
721
|
+
change_layout_layout.addWidget(QLabel("Number of Columns:"), 1, 0)
|
722
|
+
self.change_cols_input = QSpinBox()
|
723
|
+
self.change_cols_input.setMinimum(1)
|
724
|
+
self.change_cols_input.setValue(1) # Default value
|
725
|
+
change_layout_layout.addWidget(self.change_cols_input, 1, 1)
|
726
|
+
|
727
|
+
self.change_layout_button = QPushButton("Apply Layout Change")
|
728
|
+
self.change_layout_button.clicked.connect(self.change_layout)
|
729
|
+
change_layout_layout.addWidget(self.change_layout_button, 2, 0, 1, 2)
|
730
|
+
|
731
|
+
change_layout_group.setLayout(change_layout_layout)
|
732
|
+
main_layout.addWidget(change_layout_group)
|
733
|
+
|
734
|
+
# Remove All Widgets
|
735
|
+
self.clear_all_button = QPushButton("Clear All Widgets")
|
736
|
+
self.clear_all_button.clicked.connect(self.clear_all_widgets)
|
737
|
+
main_layout.addWidget(self.clear_all_button)
|
738
|
+
|
739
|
+
# Refresh Reference Widgets and Print Button
|
740
|
+
self.refresh_button = QPushButton("Refresh Reference Widgets")
|
741
|
+
self.refresh_button.clicked.connect(self.refresh_references)
|
742
|
+
self.print_button = QPushButton("Print All Button Text")
|
743
|
+
self.print_button.clicked.connect(self.layout_manager.print_all_button_text)
|
744
|
+
main_layout.addWidget(self.refresh_button)
|
745
|
+
main_layout.addWidget(self.print_button)
|
746
|
+
|
747
|
+
main_layout.addStretch()
|
748
|
+
self.setLayout(main_layout)
|
749
|
+
self.refresh_references()
|
750
|
+
|
751
|
+
def refresh_references(self):
|
752
|
+
self.ref_widget_combo.clear()
|
753
|
+
widgets = self.layout_manager.get_widgets_positions()
|
754
|
+
for widget in widgets:
|
755
|
+
if isinstance(widget, QPushButton):
|
756
|
+
self.ref_widget_combo.addItem(widget.text(), widget)
|
757
|
+
|
758
|
+
def add_at_coordinates(self):
|
759
|
+
text = self.text_input.text()
|
760
|
+
row = self.row_input.value()
|
761
|
+
col = self.col_input.value()
|
762
|
+
|
763
|
+
if not text:
|
764
|
+
QMessageBox.warning(self, "Input Error", "Please enter text for the button.")
|
765
|
+
return
|
766
|
+
|
767
|
+
button = QPushButton(text)
|
768
|
+
try:
|
769
|
+
self.layout_manager.add_widget(widget=button, row=row, col=col)
|
770
|
+
self.refresh_references()
|
771
|
+
except Exception as e:
|
772
|
+
QMessageBox.critical(self, "Error", str(e))
|
773
|
+
|
774
|
+
def add_relative(self):
|
775
|
+
text = self.rel_text_input.text()
|
776
|
+
ref_index = self.ref_widget_combo.currentIndex()
|
777
|
+
ref_widget = self.ref_widget_combo.itemData(ref_index)
|
778
|
+
position = self.position_combo.currentText()
|
779
|
+
|
780
|
+
if not text:
|
781
|
+
QMessageBox.warning(self, "Input Error", "Please enter text for the button.")
|
782
|
+
return
|
783
|
+
|
784
|
+
if ref_widget is None:
|
785
|
+
QMessageBox.warning(self, "Input Error", "Please select a reference widget.")
|
786
|
+
return
|
787
|
+
|
788
|
+
button = QPushButton(text)
|
789
|
+
try:
|
790
|
+
self.layout_manager.add_widget_relative(
|
791
|
+
widget=button, reference_widget=ref_widget, position=position
|
792
|
+
)
|
793
|
+
self.refresh_references()
|
794
|
+
except Exception as e:
|
795
|
+
QMessageBox.critical(self, "Error", str(e))
|
796
|
+
|
797
|
+
def remove_widget(self):
|
798
|
+
row = self.remove_row_input.value()
|
799
|
+
col = self.remove_col_input.value()
|
800
|
+
|
801
|
+
try:
|
802
|
+
widget = self.layout_manager.get_widget(row, col)
|
803
|
+
if widget is None:
|
804
|
+
QMessageBox.warning(self, "Not Found", f"No widget found at ({row}, {col}).")
|
805
|
+
return
|
806
|
+
self.layout_manager.remove_widget(widget)
|
807
|
+
self.refresh_references()
|
808
|
+
except Exception as e:
|
809
|
+
QMessageBox.critical(self, "Error", str(e))
|
810
|
+
|
811
|
+
def change_layout(self):
|
812
|
+
num_rows = self.change_rows_input.value()
|
813
|
+
num_cols = self.change_cols_input.value()
|
814
|
+
|
815
|
+
try:
|
816
|
+
self.layout_manager.change_layout(num_rows=num_rows, num_cols=num_cols)
|
817
|
+
self.refresh_references()
|
818
|
+
except Exception as e:
|
819
|
+
QMessageBox.critical(self, "Error", str(e))
|
820
|
+
|
821
|
+
def clear_all_widgets(self):
|
822
|
+
reply = QMessageBox.question(
|
823
|
+
self,
|
824
|
+
"Confirm Clear",
|
825
|
+
"Are you sure you want to remove all widgets?",
|
826
|
+
QMessageBox.Yes | QMessageBox.No,
|
827
|
+
QMessageBox.No,
|
828
|
+
)
|
829
|
+
|
830
|
+
if reply == QMessageBox.Yes:
|
831
|
+
try:
|
832
|
+
self.layout_manager.clear_layout()
|
833
|
+
self.refresh_references()
|
834
|
+
except Exception as e:
|
835
|
+
QMessageBox.critical(self, "Error", str(e))
|
836
|
+
|
837
|
+
|
838
|
+
class MainWindow(QMainWindow): # pragma: no cover
|
839
|
+
def __init__(self):
|
840
|
+
super().__init__()
|
841
|
+
self.setWindowTitle("Layout Manager Demo")
|
842
|
+
self.resize(800, 600)
|
843
|
+
self.init_ui()
|
844
|
+
|
845
|
+
def init_ui(self):
|
846
|
+
central_widget = QWidget()
|
847
|
+
main_layout = QHBoxLayout()
|
848
|
+
|
849
|
+
# Layout Area GroupBox
|
850
|
+
layout_group = QGroupBox("Layout Area")
|
851
|
+
layout_group.setMinimumSize(400, 400)
|
852
|
+
layout_layout = QVBoxLayout()
|
853
|
+
|
854
|
+
self.layout_manager = LayoutManagerWidget()
|
855
|
+
layout_layout.addWidget(self.layout_manager)
|
856
|
+
|
857
|
+
layout_group.setLayout(layout_layout)
|
858
|
+
|
859
|
+
# Splitter
|
860
|
+
splitter = QSplitter()
|
861
|
+
splitter.addWidget(layout_group)
|
862
|
+
|
863
|
+
# Control Panel
|
864
|
+
control_panel = ControlPanel(self.layout_manager)
|
865
|
+
control_group = QGroupBox("Control Panel")
|
866
|
+
control_layout = QVBoxLayout()
|
867
|
+
control_layout.addWidget(control_panel)
|
868
|
+
control_layout.addStretch()
|
869
|
+
control_group.setLayout(control_layout)
|
870
|
+
splitter.addWidget(control_group)
|
871
|
+
|
872
|
+
main_layout.addWidget(splitter)
|
873
|
+
central_widget.setLayout(main_layout)
|
874
|
+
self.setCentralWidget(central_widget)
|
875
|
+
|
876
|
+
|
877
|
+
if __name__ == "__main__": # pragma: no cover
|
878
|
+
app = QApplication(sys.argv)
|
879
|
+
window = MainWindow()
|
880
|
+
window.show()
|
881
|
+
sys.exit(app.exec_())
|
@@ -2,11 +2,11 @@
|
|
2
2
|
.gitlab-ci.yml,sha256=bAWGX_NR9rQZmv_bmyLXkEMRreWp0JzVNpsNTxk0NwE,8637
|
3
3
|
.pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
|
4
4
|
.readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
|
5
|
-
CHANGELOG.md,sha256=
|
5
|
+
CHANGELOG.md,sha256=IKF4CsJODVEwtIjKM2zTTS9m_JRHCY3XRYlUdJ4DZLs,7686
|
6
6
|
LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
|
7
|
-
PKG-INFO,sha256=
|
7
|
+
PKG-INFO,sha256=hICH58v6kexJp6j2db_OOpSQQlaMAO57Af9ScSPDgSg,1309
|
8
8
|
README.md,sha256=Od69x-RS85Hph0-WwWACwal4yUd67XkEn4APEfHhHFw,2649
|
9
|
-
pyproject.toml,sha256=
|
9
|
+
pyproject.toml,sha256=D6UU8tgbCO_3ovaiysqzl6EAp0qc_nHHOBav0gKDFvY,2587
|
10
10
|
.git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
|
11
11
|
.gitlab/issue_templates/bug_report_template.md,sha256=gAuyEwl7XlnebBrkiJ9AqffSNOywmr8vygUFWKTuQeI,386
|
12
12
|
.gitlab/issue_templates/documentation_update_template.md,sha256=FHLdb3TS_D9aL4CYZCjyXSulbaW5mrN2CmwTaeLPbNw,860
|
@@ -36,7 +36,7 @@ bec_widgets/examples/general_app/general_app.py,sha256=PoFCTuA_1yqrpgthASpYFgH7J
|
|
36
36
|
bec_widgets/examples/general_app/general_app.ui,sha256=TsejkM3Z8znnixyqxoj4SwhIIpIzTAuGAMkGXSS1aT8,10479
|
37
37
|
bec_widgets/examples/general_app/web_links.py,sha256=d5OgzgI9zb-NAC0pOGanOtJX3nZoe4x8QuQTw-_hK_8,434
|
38
38
|
bec_widgets/examples/jupyter_console/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
39
|
-
bec_widgets/examples/jupyter_console/jupyter_console_window.py,sha256=
|
39
|
+
bec_widgets/examples/jupyter_console/jupyter_console_window.py,sha256=mXU6G2PgbxOIEnHxn-PHJMkthAAh9kgVCiOwHQNozl0,7075
|
40
40
|
bec_widgets/examples/plugin_example_pyside/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
41
|
bec_widgets/examples/plugin_example_pyside/main.py,sha256=zDP5wO7wb3BVsQ15HOCRT1nNmCujIVRvSXZ3Txje8L0,549
|
42
42
|
bec_widgets/examples/plugin_example_pyside/registertictactoe.py,sha256=cVhBnP0qx5j9Jft-VeKvlTFE-bX58hbP45CX0f4r5pM,535
|
@@ -57,7 +57,7 @@ bec_widgets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
57
57
|
bec_widgets/tests/utils.py,sha256=D1v3JLzzbnX3HXBQCjoFlNz5cYhjqrRkFcjx3yptMJA,6687
|
58
58
|
bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
|
59
59
|
bec_widgets/utils/bec_connector.py,sha256=g40KPxhfuSHLlAa6dyO1PtC-9cQpN8ZGgn_IAEU_xD4,10070
|
60
|
-
bec_widgets/utils/bec_designer.py,sha256=
|
60
|
+
bec_widgets/utils/bec_designer.py,sha256=XBy38NbNMoRDpvRx5lGP2XnJNG34YKZ7I-ARFkn-gzs,5017
|
61
61
|
bec_widgets/utils/bec_dispatcher.py,sha256=OFmkx9vOz4pA4Sdc14QreyDZ870QYskJ4B5daVVeYg4,6325
|
62
62
|
bec_widgets/utils/bec_signal_proxy.py,sha256=PKJ7v8pKrAaqA9XNDMZZBlhVtEInX-ae6_0m2cQhiEw,2107
|
63
63
|
bec_widgets/utils/bec_table.py,sha256=nA2b8ukSeUfquFMAxGrUVOqdrzMoDYD6O_4EYbOG2zk,717
|
@@ -108,6 +108,8 @@ bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py,sha
|
|
108
108
|
bec_widgets/widgets/containers/figure/plots/waveform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
109
109
|
bec_widgets/widgets/containers/figure/plots/waveform/waveform.py,sha256=6j-3hg0tVtpCnDgbYObTYwiNI7ciuWgQ5L1TlAN0Kg8,57543
|
110
110
|
bec_widgets/widgets/containers/figure/plots/waveform/waveform_curve.py,sha256=9rOFHIxRjz0-G6f-mpw0FNmX846ZPwGn8yrJ3FpxlVc,8725
|
111
|
+
bec_widgets/widgets/containers/layout_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
112
|
+
bec_widgets/widgets/containers/layout_manager/layout_manager.py,sha256=98G91ffPynoZTTi2IjklS9TwZh_JZbJX1_MeXAB2eC4,33766
|
111
113
|
bec_widgets/widgets/containers/main_window/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
112
114
|
bec_widgets/widgets/containers/main_window/main_window.py,sha256=A2IAux-lqupMCVZOQu7AUt1AEaeIG3Eza85eN0bknlI,272
|
113
115
|
bec_widgets/widgets/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -315,8 +317,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=Z
|
|
315
317
|
bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
|
316
318
|
bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
|
317
319
|
bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
|
318
|
-
bec_widgets-1.
|
319
|
-
bec_widgets-1.
|
320
|
-
bec_widgets-1.
|
321
|
-
bec_widgets-1.
|
322
|
-
bec_widgets-1.
|
320
|
+
bec_widgets-1.10.0.dist-info/METADATA,sha256=hICH58v6kexJp6j2db_OOpSQQlaMAO57Af9ScSPDgSg,1309
|
321
|
+
bec_widgets-1.10.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
322
|
+
bec_widgets-1.10.0.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
|
323
|
+
bec_widgets-1.10.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
|
324
|
+
bec_widgets-1.10.0.dist-info/RECORD,,
|
pyproject.toml
CHANGED
File without changes
|
File without changes
|
File without changes
|