bec-widgets 1.9.1__py3-none-any.whl → 1.11.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.
@@ -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: int | None = None,
412
+ col: int | None = None,
413
+ coordinates: Tuple[int, int] | None = 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_())