bec-widgets 2.3.0__py3-none-any.whl → 2.5.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.
Files changed (46) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.md +26 -0
  2. .github/ISSUE_TEMPLATE/feature_request.md +48 -0
  3. .github/workflows/check_pr.yml +28 -0
  4. .github/workflows/ci.yml +36 -0
  5. .github/workflows/end2end-conda.yml +48 -0
  6. .github/workflows/formatter.yml +61 -0
  7. .github/workflows/generate-cli-check.yml +49 -0
  8. .github/workflows/pytest-matrix.yml +49 -0
  9. .github/workflows/pytest.yml +65 -0
  10. .github/workflows/semantic_release.yml +103 -0
  11. CHANGELOG.md +1726 -1546
  12. LICENSE +1 -1
  13. PKG-INFO +2 -1
  14. README.md +11 -0
  15. bec_widgets/cli/client.py +346 -0
  16. bec_widgets/examples/jupyter_console/jupyter_console_window.py +8 -8
  17. bec_widgets/tests/utils.py +3 -3
  18. bec_widgets/utils/entry_validator.py +13 -3
  19. bec_widgets/utils/side_panel.py +65 -39
  20. bec_widgets/utils/toolbar.py +79 -0
  21. bec_widgets/widgets/containers/layout_manager/layout_manager.py +34 -1
  22. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +1 -1
  23. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +27 -31
  24. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +1 -1
  25. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +1 -1
  26. bec_widgets/widgets/editors/dict_backed_table.py +7 -0
  27. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +1 -0
  28. bec_widgets/widgets/editors/web_console/register_web_console.py +15 -0
  29. bec_widgets/widgets/editors/web_console/web_console.py +230 -0
  30. bec_widgets/widgets/editors/web_console/web_console.pyproject +1 -0
  31. bec_widgets/widgets/editors/web_console/web_console_plugin.py +54 -0
  32. bec_widgets/widgets/plots/image/image.py +90 -0
  33. bec_widgets/widgets/plots/roi/__init__.py +0 -0
  34. bec_widgets/widgets/plots/roi/image_roi.py +867 -0
  35. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +11 -46
  36. bec_widgets/widgets/utility/visual/color_button_native/__init__.py +0 -0
  37. bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py +58 -0
  38. bec_widgets/widgets/utility/visual/color_button_native/color_button_native.pyproject +1 -0
  39. bec_widgets/widgets/utility/visual/color_button_native/color_button_native_plugin.py +56 -0
  40. bec_widgets/widgets/utility/visual/color_button_native/register_color_button_native.py +17 -0
  41. {bec_widgets-2.3.0.dist-info → bec_widgets-2.5.0.dist-info}/METADATA +2 -1
  42. {bec_widgets-2.3.0.dist-info → bec_widgets-2.5.0.dist-info}/RECORD +46 -25
  43. {bec_widgets-2.3.0.dist-info → bec_widgets-2.5.0.dist-info}/licenses/LICENSE +1 -1
  44. pyproject.toml +17 -5
  45. {bec_widgets-2.3.0.dist-info → bec_widgets-2.5.0.dist-info}/WHEEL +0 -0
  46. {bec_widgets-2.3.0.dist-info → bec_widgets-2.5.0.dist-info}/entry_points.txt +0 -0
LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2023, bec
3
+ Copyright (c) 2025, Paul Scherrer Institute
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -25,6 +25,7 @@ Requires-Dist: coverage~=7.0; extra == 'dev'
25
25
  Requires-Dist: fakeredis>=2.23.2,~=2.23; extra == 'dev'
26
26
  Requires-Dist: isort>=5.13.2,~=5.13; extra == 'dev'
27
27
  Requires-Dist: pytest-bec-e2e<=4.0,>=2.21.4; extra == 'dev'
28
+ Requires-Dist: pytest-cov~=6.1.1; extra == 'dev'
28
29
  Requires-Dist: pytest-qt~=4.4; extra == 'dev'
29
30
  Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
30
31
  Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
README.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # BEC Widgets
2
2
 
3
+
4
+ [![CI](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml/badge.svg)](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
5
+ [![badge](https://img.shields.io/pypi/v/bec-widgets)](https://pypi.org/project/bec-widgets/)
6
+ [![License](https://img.shields.io/github/license/bec-project/bec_widgets)](./LICENSE)
7
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
8
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue?logo=python&logoColor=white)](https://www.python.org)
9
+ [![PySide6](https://img.shields.io/badge/PySide6-blue?logo=qt&logoColor=white)](https://doc.qt.io/qtforpython/)
10
+ [![Conventional Commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
11
+ [![codecov](https://codecov.io/gh/bec-project/bec_widgets/graph/badge.svg?token=0Z9IQRJKMY)](https://codecov.io/gh/bec-project/bec_widgets)
12
+
13
+
3
14
  **⚠️ Important Notice:**
4
15
 
5
16
  🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
bec_widgets/cli/client.py CHANGED
@@ -55,6 +55,7 @@ _Widgets = {
55
55
  "TextBox": "TextBox",
56
56
  "VSCodeEditor": "VSCodeEditor",
57
57
  "Waveform": "Waveform",
58
+ "WebConsole": "WebConsole",
58
59
  "WebsiteWidget": "WebsiteWidget",
59
60
  }
60
61
 
@@ -503,6 +504,204 @@ class BECStatusBox(RPCBase):
503
504
  """
504
505
 
505
506
 
507
+ class BaseROI(RPCBase):
508
+ """Base class for all Region of Interest (ROI) implementations."""
509
+
510
+ @property
511
+ @rpc_call
512
+ def label(self) -> "str":
513
+ """
514
+ Gets the display name of this ROI.
515
+
516
+ Returns:
517
+ str: The current name of the ROI.
518
+ """
519
+
520
+ @label.setter
521
+ @rpc_call
522
+ def label(self) -> "str":
523
+ """
524
+ Gets the display name of this ROI.
525
+
526
+ Returns:
527
+ str: The current name of the ROI.
528
+ """
529
+
530
+ @property
531
+ @rpc_call
532
+ def line_color(self) -> "str":
533
+ """
534
+ Gets the current line color of the ROI.
535
+
536
+ Returns:
537
+ str: The current line color as a string (e.g., hex color code).
538
+ """
539
+
540
+ @line_color.setter
541
+ @rpc_call
542
+ def line_color(self) -> "str":
543
+ """
544
+ Gets the current line color of the ROI.
545
+
546
+ Returns:
547
+ str: The current line color as a string (e.g., hex color code).
548
+ """
549
+
550
+ @property
551
+ @rpc_call
552
+ def line_width(self) -> "int":
553
+ """
554
+ Gets the current line width of the ROI.
555
+
556
+ Returns:
557
+ int: The current line width in pixels.
558
+ """
559
+
560
+ @line_width.setter
561
+ @rpc_call
562
+ def line_width(self) -> "int":
563
+ """
564
+ Gets the current line width of the ROI.
565
+
566
+ Returns:
567
+ int: The current line width in pixels.
568
+ """
569
+
570
+ @rpc_call
571
+ def get_coordinates(self):
572
+ """
573
+ Gets the coordinates that define this ROI's position and shape.
574
+
575
+ This is an abstract method that must be implemented by subclasses.
576
+ Implementations should return either a dictionary with descriptive keys
577
+ or a tuple of coordinates, depending on the value of self.description.
578
+
579
+ Returns:
580
+ dict or tuple: The coordinates defining the ROI's position and shape.
581
+
582
+ Raises:
583
+ NotImplementedError: This method must be implemented by subclasses.
584
+ """
585
+
586
+ @rpc_call
587
+ def get_data_from_image(
588
+ self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
589
+ ):
590
+ """
591
+ Wrapper around `pyqtgraph.ROI.getArrayRegion`.
592
+
593
+ Args:
594
+ image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
595
+ the first `ImageItem` in the same GraphicsScene as this ROI.
596
+ returnMappedCoords (bool): If True, also returns the coordinate array generated by
597
+ *getArrayRegion*.
598
+ **kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
599
+ such as `axes`, `order`, `shape`, etc.
600
+
601
+ Returns:
602
+ ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
603
+ """
604
+
605
+
606
+ class CircularROI(RPCBase):
607
+ """Circular Region of Interest with center/diameter tracking and auto-labeling."""
608
+
609
+ @property
610
+ @rpc_call
611
+ def label(self) -> "str":
612
+ """
613
+ Gets the display name of this ROI.
614
+
615
+ Returns:
616
+ str: The current name of the ROI.
617
+ """
618
+
619
+ @label.setter
620
+ @rpc_call
621
+ def label(self) -> "str":
622
+ """
623
+ Gets the display name of this ROI.
624
+
625
+ Returns:
626
+ str: The current name of the ROI.
627
+ """
628
+
629
+ @property
630
+ @rpc_call
631
+ def line_color(self) -> "str":
632
+ """
633
+ Gets the current line color of the ROI.
634
+
635
+ Returns:
636
+ str: The current line color as a string (e.g., hex color code).
637
+ """
638
+
639
+ @line_color.setter
640
+ @rpc_call
641
+ def line_color(self) -> "str":
642
+ """
643
+ Gets the current line color of the ROI.
644
+
645
+ Returns:
646
+ str: The current line color as a string (e.g., hex color code).
647
+ """
648
+
649
+ @property
650
+ @rpc_call
651
+ def line_width(self) -> "int":
652
+ """
653
+ Gets the current line width of the ROI.
654
+
655
+ Returns:
656
+ int: The current line width in pixels.
657
+ """
658
+
659
+ @line_width.setter
660
+ @rpc_call
661
+ def line_width(self) -> "int":
662
+ """
663
+ Gets the current line width of the ROI.
664
+
665
+ Returns:
666
+ int: The current line width in pixels.
667
+ """
668
+
669
+ @rpc_call
670
+ def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
671
+ """
672
+ Calculates and returns the coordinates and size of an object, either as a
673
+ typed dictionary or as a tuple.
674
+
675
+ Args:
676
+ typed (bool | None): If True, returns coordinates as a dictionary. Defaults
677
+ to None, which utilizes the object's description value.
678
+
679
+ Returns:
680
+ dict: A dictionary with keys 'center_x', 'center_y', 'diameter', and 'radius'
681
+ if `typed` is True.
682
+ tuple: A tuple containing (center_x, center_y, diameter, radius) if `typed` is False.
683
+ """
684
+
685
+ @rpc_call
686
+ def get_data_from_image(
687
+ self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
688
+ ):
689
+ """
690
+ Wrapper around `pyqtgraph.ROI.getArrayRegion`.
691
+
692
+ Args:
693
+ image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
694
+ the first `ImageItem` in the same GraphicsScene as this ROI.
695
+ returnMappedCoords (bool): If True, also returns the coordinate array generated by
696
+ *getArrayRegion*.
697
+ **kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
698
+ such as `axes`, `order`, `shape`, etc.
699
+
700
+ Returns:
701
+ ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
702
+ """
703
+
704
+
506
705
  class Curve(RPCBase):
507
706
  @rpc_call
508
707
  def remove(self):
@@ -1214,6 +1413,44 @@ class Image(RPCBase):
1214
1413
  Access the main image item.
1215
1414
  """
1216
1415
 
1416
+ @rpc_call
1417
+ def add_roi(
1418
+ self,
1419
+ kind: "Literal['rect', 'circle']" = "rect",
1420
+ name: "str | None" = None,
1421
+ line_width: "int | None" = 10,
1422
+ pos: "tuple[float, float] | None" = (10, 10),
1423
+ size: "tuple[float, float] | None" = (50, 50),
1424
+ **pg_kwargs,
1425
+ ) -> "RectangularROI | CircularROI":
1426
+ """
1427
+ Add a ROI to the image.
1428
+
1429
+ Args:
1430
+ kind(str): The type of ROI to add. Options are "rect" or "circle".
1431
+ name(str): The name of the ROI.
1432
+ line_width(int): The line width of the ROI.
1433
+ pos(tuple): The position of the ROI.
1434
+ size(tuple): The size of the ROI.
1435
+ **pg_kwargs: Additional arguments for the ROI.
1436
+
1437
+ Returns:
1438
+ RectangularROI | CircularROI: The created ROI object.
1439
+ """
1440
+
1441
+ @rpc_call
1442
+ def remove_roi(self, roi: "int | str"):
1443
+ """
1444
+ Remove an ROI by index or label via the ROIController.
1445
+ """
1446
+
1447
+ @property
1448
+ @rpc_call
1449
+ def rois(self) -> "list[BaseROI]":
1450
+ """
1451
+ Get the list of ROIs.
1452
+ """
1453
+
1217
1454
 
1218
1455
  class ImageItem(RPCBase):
1219
1456
  @property
@@ -2317,6 +2554,105 @@ class PositionerGroup(RPCBase):
2317
2554
  """
2318
2555
 
2319
2556
 
2557
+ class RectangularROI(RPCBase):
2558
+ """Defines a rectangular Region of Interest (ROI) with additional functionality."""
2559
+
2560
+ @property
2561
+ @rpc_call
2562
+ def label(self) -> "str":
2563
+ """
2564
+ Gets the display name of this ROI.
2565
+
2566
+ Returns:
2567
+ str: The current name of the ROI.
2568
+ """
2569
+
2570
+ @label.setter
2571
+ @rpc_call
2572
+ def label(self) -> "str":
2573
+ """
2574
+ Gets the display name of this ROI.
2575
+
2576
+ Returns:
2577
+ str: The current name of the ROI.
2578
+ """
2579
+
2580
+ @property
2581
+ @rpc_call
2582
+ def line_color(self) -> "str":
2583
+ """
2584
+ Gets the current line color of the ROI.
2585
+
2586
+ Returns:
2587
+ str: The current line color as a string (e.g., hex color code).
2588
+ """
2589
+
2590
+ @line_color.setter
2591
+ @rpc_call
2592
+ def line_color(self) -> "str":
2593
+ """
2594
+ Gets the current line color of the ROI.
2595
+
2596
+ Returns:
2597
+ str: The current line color as a string (e.g., hex color code).
2598
+ """
2599
+
2600
+ @property
2601
+ @rpc_call
2602
+ def line_width(self) -> "int":
2603
+ """
2604
+ Gets the current line width of the ROI.
2605
+
2606
+ Returns:
2607
+ int: The current line width in pixels.
2608
+ """
2609
+
2610
+ @line_width.setter
2611
+ @rpc_call
2612
+ def line_width(self) -> "int":
2613
+ """
2614
+ Gets the current line width of the ROI.
2615
+
2616
+ Returns:
2617
+ int: The current line width in pixels.
2618
+ """
2619
+
2620
+ @rpc_call
2621
+ def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
2622
+ """
2623
+ Returns the coordinates of a rectangle's corners. Supports returning them
2624
+ as either a dictionary with descriptive keys or a tuple of coordinates.
2625
+
2626
+ Args:
2627
+ typed (bool | None): If True, returns coordinates as a dictionary with
2628
+ descriptive keys. If False, returns them as a tuple. Defaults to
2629
+ the value of `self.description`.
2630
+
2631
+ Returns:
2632
+ dict | tuple: The rectangle's corner coordinates, where the format
2633
+ depends on the `typed` parameter.
2634
+ """
2635
+
2636
+ @rpc_call
2637
+ def get_data_from_image(
2638
+ self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
2639
+ ):
2640
+ """
2641
+ Wrapper around `pyqtgraph.ROI.getArrayRegion`.
2642
+
2643
+ Args:
2644
+ image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
2645
+ the first `ImageItem` in the same GraphicsScene as this ROI.
2646
+ returnMappedCoords (bool): If True, also returns the coordinate array generated by
2647
+ *getArrayRegion*.
2648
+ **kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
2649
+ such as `axes`, `order`, `shape`, etc.
2650
+
2651
+ Returns:
2652
+ ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
2653
+ """
2654
+
2655
+
2320
2656
  class ResetButton(RPCBase):
2321
2657
  """A button that resets the scan queue."""
2322
2658
 
@@ -3501,6 +3837,16 @@ class Waveform(RPCBase):
3501
3837
  """
3502
3838
 
3503
3839
 
3840
+ class WebConsole(RPCBase):
3841
+ """A simple widget to display a website"""
3842
+
3843
+ @rpc_call
3844
+ def remove(self):
3845
+ """
3846
+ Cleanup the BECConnector
3847
+ """
3848
+
3849
+
3504
3850
  class WebsiteWidget(RPCBase):
3505
3851
  """A simple widget to display a website"""
3506
3852
 
@@ -43,7 +43,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
43
43
  "pg": pg,
44
44
  "wh": wh,
45
45
  "dock": self.dock,
46
- # "im": self.im,
46
+ "im": self.im,
47
47
  # "mi": self.mi,
48
48
  # "mm": self.mm,
49
49
  # "lm": self.lm,
@@ -112,13 +112,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
112
112
  # tab_widget.addTab(fifth_tab, "Waveform Next Gen")
113
113
  # tab_widget.setCurrentIndex(4)
114
114
  #
115
- # sixth_tab = QWidget()
116
- # sixth_tab_layout = QVBoxLayout(sixth_tab)
117
- # self.im = Image()
118
- # self.mi = self.im.main_image
119
- # sixth_tab_layout.addWidget(self.im)
120
- # tab_widget.addTab(sixth_tab, "Image Next Gen")
121
- # tab_widget.setCurrentIndex(5)
115
+ sixth_tab = QWidget()
116
+ sixth_tab_layout = QVBoxLayout(sixth_tab)
117
+ self.im = Image(popups=False)
118
+ self.mi = self.im.main_image
119
+ sixth_tab_layout.addWidget(self.im)
120
+ tab_widget.addTab(sixth_tab, "Image Next Gen")
121
+ tab_widget.setCurrentIndex(1)
122
122
  #
123
123
  # seventh_tab = QWidget()
124
124
  # seventh_tab_layout = QVBoxLayout(seventh_tab)
@@ -96,9 +96,9 @@ class FakePositioner(BECPositioner):
96
96
  }
97
97
  self._info = {
98
98
  "signals": {
99
- "readback": {"kind_str": "5"}, # hinted
100
- "setpoint": {"kind_str": "1"}, # normal
101
- "velocity": {"kind_str": "2"}, # config
99
+ "readback": {"kind_str": "hinted"}, # hinted
100
+ "setpoint": {"kind_str": "normal"}, # normal
101
+ "velocity": {"kind_str": "config"}, # config
102
102
  }
103
103
  }
104
104
  self.signals = {
@@ -17,13 +17,23 @@ class EntryValidator:
17
17
  raise ValueError(f"Device '{name}' not found in current BEC session")
18
18
 
19
19
  device = self.devices[name]
20
- description = device.describe()
20
+
21
+ # Build list of available signal entries from device._info['signals']
22
+ signals_dict = getattr(device, "_info", {}).get("signals", {})
23
+ available_entries = [
24
+ sig.get("obj_name") for sig in signals_dict.values() if sig.get("obj_name")
25
+ ]
26
+
27
+ # If no signals are found, means device is a signal, use the device name as the entry
28
+ if not available_entries:
29
+ available_entries = [name]
21
30
 
22
31
  if entry is None or entry == "":
23
32
  entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
24
- if entry not in description:
33
+ if entry not in available_entries:
25
34
  raise ValueError(
26
- f"Entry '{entry}' not found in device '{name}' signals. Available signals: {description.keys()}"
35
+ f"Entry '{entry}' not found in device '{name}' signals. "
36
+ f"Available signals: '{available_entries}'"
27
37
  )
28
38
 
29
39
  return entry