bec-widgets 1.18.0__py3-none-any.whl → 1.19.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 CHANGED
@@ -1,6 +1,37 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v1.19.0 (2025-01-31)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Enable type checking for BECDispatcher in BECConnector
9
+ ([`50a572d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50a572dacd5dfc29a9ecf1b567aac6822b632f60))
10
+
11
+ ### Documentation
12
+
13
+ - Add docs for LogPanel
14
+ ([`f219c6f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f219c6fb573cf42964f6a7c6f4a0b0b9946fb98d))
15
+
16
+ ### Features
17
+
18
+ - **widget**: Add LogPanel widget
19
+ ([`f048880`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0488802775401319a54a51d05a0ad534292af09))
20
+
21
+
22
+ ## v1.18.1 (2025-01-30)
23
+
24
+ ### Bug Fixes
25
+
26
+ - **signal_combo_box**: Added missing plugin modules for signal line_edit/combobox
27
+ ([`db70442`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/db70442cc21247d20e6f6ad78ad0e1d3aca24bf7))
28
+
29
+ ### Documentation
30
+
31
+ - Add screenshots for device and signal input
32
+ ([`f0c4efe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0c4efefa03bf36ae57bf1a17f6a1b2e4d32c6c4))
33
+
34
+
4
35
  ## v1.18.0 (2025-01-30)
5
36
 
6
37
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.18.0
3
+ Version: 1.19.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
bec_widgets/cli/client.py CHANGED
@@ -31,6 +31,7 @@ class Widgets(str, enum.Enum):
31
31
  DeviceComboBox = "DeviceComboBox"
32
32
  DeviceLineEdit = "DeviceLineEdit"
33
33
  LMFitDialog = "LMFitDialog"
34
+ LogPanel = "LogPanel"
34
35
  Minesweeper = "Minesweeper"
35
36
  PositionIndicator = "PositionIndicator"
36
37
  PositionerBox = "PositionerBox"
@@ -3183,6 +3184,26 @@ class LMFitDialog(RPCBase):
3183
3184
  """
3184
3185
 
3185
3186
 
3187
+ class LogPanel(RPCBase):
3188
+ @rpc_call
3189
+ def set_plain_text(self, text: str) -> None:
3190
+ """
3191
+ Set the plain text of the widget.
3192
+
3193
+ Args:
3194
+ text (str): The text to set.
3195
+ """
3196
+
3197
+ @rpc_call
3198
+ def set_html_text(self, text: str) -> None:
3199
+ """
3200
+ Set the HTML text of the widget.
3201
+
3202
+ Args:
3203
+ text (str): The text to set.
3204
+ """
3205
+
3206
+
3186
3207
  class Minesweeper(RPCBase): ...
3187
3208
 
3188
3209
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import os
5
5
  import time
6
6
  import uuid
7
- from typing import Optional
7
+ from typing import TYPE_CHECKING, Optional
8
8
 
9
9
  from bec_lib.logger import bec_logger
10
10
  from bec_lib.utils.import_utils import lazy_import_from
@@ -17,6 +17,9 @@ from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
17
17
  from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
18
18
  from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
19
19
 
20
+ if TYPE_CHECKING:
21
+ from bec_widgets.utils.bec_dispatcher import BECDispatcher
22
+
20
23
  logger = bec_logger.logger
21
24
  BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
22
25
 
@@ -1 +0,0 @@
1
-
@@ -30,6 +30,7 @@ from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
30
30
  from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
31
31
  from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
32
32
  from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
33
+ from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
33
34
  from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
34
35
 
35
36
 
@@ -139,6 +140,9 @@ class BECDockArea(BECWidget, QWidget):
139
140
  tooltip="Add Circular ProgressBar",
140
141
  filled=True,
141
142
  ),
143
+ "log_panel": MaterialIconAction(
144
+ icon_name=LogPanel.ICON_NAME, tooltip="Add LogPanel", filled=True
145
+ ),
142
146
  },
143
147
  ),
144
148
  "separator_2": SeparatorAction(),
@@ -200,6 +204,9 @@ class BECDockArea(BECWidget, QWidget):
200
204
  self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect(
201
205
  lambda: self.add_dock(widget="RingProgressBar", prefix="progress_bar")
202
206
  )
207
+ self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect(
208
+ lambda: self.add_dock(widget="LogPanel", prefix="log_panel")
209
+ )
203
210
 
204
211
  # Icons
205
212
  self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
@@ -0,0 +1,17 @@
1
+ def main(): # pragma: no cover
2
+ from qtpy import PYSIDE6
3
+
4
+ if not PYSIDE6:
5
+ print("PYSIDE6 is not available in the environment. Cannot patch designer.")
6
+ return
7
+ from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
8
+
9
+ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combo_box_plugin import (
10
+ SignalComboBoxPlugin,
11
+ )
12
+
13
+ QPyDesignerCustomWidgetCollection.addCustomWidget(SignalComboBoxPlugin())
14
+
15
+
16
+ if __name__ == "__main__": # pragma: no cover
17
+ main()
@@ -0,0 +1 @@
1
+ {'files': ['signal_combobox.py']}
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+
6
+ from bec_widgets.utils.bec_designer import designer_material_icon
7
+ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='SignalComboBox' name='signal_combo_box'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = SignalComboBox(parent)
24
+ return t
25
+
26
+ def domXml(self):
27
+ return DOM_XML
28
+
29
+ def group(self):
30
+ return "BEC Input Widgets"
31
+
32
+ def icon(self):
33
+ return designer_material_icon(SignalComboBox.ICON_NAME)
34
+
35
+ def includeFile(self):
36
+ return "signal_combo_box"
37
+
38
+ def initialize(self, form_editor):
39
+ self._form_editor = form_editor
40
+
41
+ def isContainer(self):
42
+ return False
43
+
44
+ def isInitialized(self):
45
+ return self._form_editor is not None
46
+
47
+ def name(self):
48
+ return "SignalComboBox"
49
+
50
+ def toolTip(self):
51
+ return "Signal ComboBox Example for BEC Widgets with autocomplete."
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()
@@ -50,7 +50,7 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
50
50
  return "SignalLineEdit"
51
51
 
52
52
  def toolTip(self):
53
- return ""
53
+ return "Signal LineEdit Example for BEC Widgets with autocomplete."
54
54
 
55
55
  def whatsThis(self):
56
56
  return self.toolTip()
@@ -5,9 +5,9 @@ from html.parser import HTMLParser
5
5
 
6
6
  from bec_lib.logger import bec_logger
7
7
  from pydantic import Field
8
- from qtpy.QtCore import Property, Slot
9
8
  from qtpy.QtWidgets import QTextEdit, QVBoxLayout, QWidget
10
9
 
10
+ from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
11
11
  from bec_widgets.utils.bec_connector import ConnectionConfig
12
12
  from bec_widgets.utils.bec_widget import BECWidget
13
13
 
@@ -66,7 +66,7 @@ class TextBox(BECWidget, QWidget):
66
66
  else:
67
67
  self.set_html_text(DEFAULT_TEXT)
68
68
 
69
- @Slot(str)
69
+ @SafeSlot(str)
70
70
  def set_plain_text(self, text: str) -> None:
71
71
  """Set the plain text of the widget.
72
72
 
@@ -77,7 +77,7 @@ class TextBox(BECWidget, QWidget):
77
77
  self.config.text = text
78
78
  self.config.is_html = False
79
79
 
80
- @Slot(str)
80
+ @SafeSlot(str)
81
81
  def set_html_text(self, text: str) -> None:
82
82
  """Set the HTML text of the widget.
83
83
 
@@ -88,7 +88,7 @@ class TextBox(BECWidget, QWidget):
88
88
  self.config.text = text
89
89
  self.config.is_html = True
90
90
 
91
- @Property(str)
91
+ @SafeProperty(str)
92
92
  def plain_text(self) -> str:
93
93
  """Get the text of the widget.
94
94
 
@@ -106,7 +106,7 @@ class TextBox(BECWidget, QWidget):
106
106
  """
107
107
  self.set_plain_text(text)
108
108
 
109
- @Property(str)
109
+ @SafeProperty(str)
110
110
  def html_text(self) -> str:
111
111
  """Get the HTML text of the widget.
112
112
 
@@ -0,0 +1,3 @@
1
+ from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
2
+
3
+ __ALL__ = ["LogPanel"]
@@ -0,0 +1,58 @@
1
+ """ Utilities for filtering and formatting in the LogPanel"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections import deque
7
+ from typing import Callable, Iterator
8
+
9
+ from bec_lib.logger import LogLevel
10
+ from bec_lib.messages import LogMessage
11
+ from qtpy.QtCore import QDateTime
12
+
13
+ LinesHtmlFormatter = Callable[[deque[LogMessage]], Iterator[str]]
14
+ LineFormatter = Callable[[LogMessage], str]
15
+ LineFilter = Callable[[LogMessage], bool] | None
16
+
17
+ ANSI_ESCAPE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
18
+
19
+
20
+ def replace_escapes(s: str):
21
+ s = ANSI_ESCAPE_REGEX.sub("", s)
22
+ return s.replace(" ", "&nbsp;").replace("\n", "<br />").replace("\t", " ")
23
+
24
+
25
+ def level_filter(msg: LogMessage, thresh: int):
26
+ return LogLevel[msg.content["log_type"].upper()].value >= thresh
27
+
28
+
29
+ def noop_format(line: LogMessage):
30
+ _textline = line.log_msg if isinstance(line.log_msg, str) else line.log_msg["text"]
31
+ return replace_escapes(_textline.strip()) + "<br />"
32
+
33
+
34
+ def simple_color_format(line: LogMessage, colors: dict[LogLevel, str]):
35
+ color = colors.get(LogLevel[line.content["log_type"].upper()]) or colors[LogLevel.INFO]
36
+ return f'<font color="{color}">{noop_format(line)}</font>'
37
+
38
+
39
+ def create_formatter(line_format: LineFormatter, line_filter: LineFilter) -> LinesHtmlFormatter:
40
+ def _formatter(data: deque[LogMessage]):
41
+ if line_filter is not None:
42
+ return (line_format(line) for line in data if line_filter(line))
43
+ else:
44
+ return (line_format(line) for line in data)
45
+
46
+ return _formatter
47
+
48
+
49
+ def log_txt(line):
50
+ return line.log_msg if isinstance(line.log_msg, str) else line.log_msg["text"]
51
+
52
+
53
+ def log_time(line):
54
+ return QDateTime.fromMSecsSinceEpoch(int(line.log_msg["record"]["time"]["timestamp"] * 1000))
55
+
56
+
57
+ def log_svc(line):
58
+ return line.log_msg["service_name"]
@@ -0,0 +1 @@
1
+ {'files': ['logpanel.py']}
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+
6
+ from bec_widgets.utils.bec_designer import designer_material_icon
7
+ from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='LogPanel' name='log_panel'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class LogPanelPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = LogPanel(parent)
24
+ return t
25
+
26
+ def domXml(self):
27
+ return DOM_XML
28
+
29
+ def group(self):
30
+ return "BEC Utils"
31
+
32
+ def icon(self):
33
+ return designer_material_icon(LogPanel.ICON_NAME)
34
+
35
+ def includeFile(self):
36
+ return "log_panel"
37
+
38
+ def initialize(self, form_editor):
39
+ self._form_editor = form_editor
40
+
41
+ def isContainer(self):
42
+ return False
43
+
44
+ def isInitialized(self):
45
+ return self._form_editor is not None
46
+
47
+ def name(self):
48
+ return "LogPanel"
49
+
50
+ def toolTip(self):
51
+ return "Displays a log panel"
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()
@@ -0,0 +1,516 @@
1
+ """ Module for a LogPanel widget to display BEC log messages """
2
+
3
+ from __future__ import annotations
4
+
5
+ import operator
6
+ import os
7
+ import re
8
+ from collections import deque
9
+ from functools import partial, reduce
10
+ from re import Pattern
11
+ from typing import TYPE_CHECKING, Literal
12
+
13
+ from bec_lib.client import BECClient
14
+ from bec_lib.connector import ConnectorBase
15
+ from bec_lib.endpoints import MessageEndpoints
16
+ from bec_lib.logger import LogLevel, bec_logger
17
+ from bec_lib.messages import LogMessage, StatusMessage
18
+ from qtpy.QtCore import QDateTime, Qt, Signal # type: ignore
19
+ from qtpy.QtGui import QFont
20
+ from qtpy.QtWidgets import (
21
+ QApplication,
22
+ QCheckBox,
23
+ QComboBox,
24
+ QDateTimeEdit,
25
+ QDialog,
26
+ QGridLayout,
27
+ QHBoxLayout,
28
+ QLabel,
29
+ QLineEdit,
30
+ QPushButton,
31
+ QScrollArea,
32
+ QTextEdit,
33
+ QVBoxLayout,
34
+ QWidget,
35
+ )
36
+
37
+ from bec_widgets.qt_utils.error_popups import SafeSlot
38
+ from bec_widgets.utils.colors import get_theme_palette, set_theme
39
+ from bec_widgets.widgets.editors.text_box.text_box import TextBox
40
+ from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECServiceStatusMixin
41
+ from bec_widgets.widgets.utility.logpanel._util import (
42
+ LineFilter,
43
+ LineFormatter,
44
+ LinesHtmlFormatter,
45
+ create_formatter,
46
+ level_filter,
47
+ log_svc,
48
+ log_time,
49
+ log_txt,
50
+ noop_format,
51
+ simple_color_format,
52
+ )
53
+
54
+ if TYPE_CHECKING:
55
+ from qtpy.QtCore import pyqtBoundSignal # type: ignore
56
+
57
+ logger = bec_logger.logger
58
+
59
+ MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
60
+
61
+ # TODO: improve log color handling
62
+ DEFAULT_LOG_COLORS = {
63
+ LogLevel.INFO: "#FFFFFF",
64
+ LogLevel.SUCCESS: "#00FF00",
65
+ LogLevel.WARNING: "#FFCC00",
66
+ LogLevel.ERROR: "#FF0000",
67
+ LogLevel.DEBUG: "#0000CC",
68
+ }
69
+
70
+
71
+ class BecLogsQueue:
72
+ """Manages getting logs from BEC Redis and formatting them for display"""
73
+
74
+ def __init__(
75
+ self,
76
+ conn: ConnectorBase,
77
+ new_message_signal: pyqtBoundSignal,
78
+ maxlen: int = 1000,
79
+ line_formatter: LineFormatter = noop_format,
80
+ ) -> None:
81
+ self._timestamp_start: QDateTime | None = None
82
+ self._timestamp_end: QDateTime | None = None
83
+ self._conn = conn
84
+ self._new_message_signal: pyqtBoundSignal | None = new_message_signal
85
+ self._max_length = maxlen
86
+ self._data: deque[LogMessage] = deque([], self._max_length)
87
+ self._display_queue: deque[str] = deque([], self._max_length)
88
+ self._log_level: str | None = None
89
+ self._search_query: Pattern | str | None = None
90
+ self._selected_services: set[str] | None = None
91
+ self._set_formatter_and_update_filter(line_formatter)
92
+ self._conn.register([MessageEndpoints.log()], None, self._process_incoming_log_msg)
93
+
94
+ def disconnect(self):
95
+ self._conn.unregister([MessageEndpoints.log()], None, self._process_incoming_log_msg)
96
+ self._new_message_signal = None
97
+
98
+ def _process_incoming_log_msg(self, msg: dict):
99
+ _msg: LogMessage = msg["data"]
100
+ self._data.append(_msg)
101
+ if self.filter is None or self.filter(_msg):
102
+ self._display_queue.append(self._line_formatter(_msg))
103
+ if self._new_message_signal:
104
+ self._new_message_signal.emit()
105
+
106
+ def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
107
+ self._line_formatter: LineFormatter = line_formatter
108
+ self._queue_formatter: LinesHtmlFormatter = create_formatter(
109
+ self._line_formatter, self.filter
110
+ )
111
+
112
+ def _combine_filters(self, *args: LineFilter):
113
+ return lambda msg: reduce(operator.and_, [filt(msg) for filt in args if filt is not None])
114
+
115
+ def _create_re_filter(self) -> LineFilter:
116
+ if self._search_query is None:
117
+ return None
118
+ elif isinstance(self._search_query, str):
119
+ return lambda line: self._search_query in log_txt(line)
120
+ return lambda line: self._search_query.match(log_txt(line)) is not None
121
+
122
+ def _create_service_filter(self):
123
+ return (
124
+ lambda line: self._selected_services is None or log_svc(line) in self._selected_services
125
+ )
126
+
127
+ def _create_timestamp_filter(self) -> LineFilter:
128
+ s, e = self._timestamp_start, self._timestamp_end
129
+ if s is e is None:
130
+ return lambda msg: True
131
+
132
+ def _time_filter(msg):
133
+ msg_time = log_time(msg)
134
+ if s is None:
135
+ return msg_time <= e
136
+ if e is None:
137
+ return s <= msg_time
138
+ return s <= msg_time <= e
139
+
140
+ return _time_filter
141
+
142
+ @property
143
+ def filter(self) -> LineFilter:
144
+ thresh = LogLevel[self._log_level].value if self._log_level is not None else 0
145
+ return self._combine_filters(
146
+ partial(level_filter, thresh=thresh),
147
+ self._create_re_filter(),
148
+ self._create_timestamp_filter(),
149
+ self._create_service_filter(),
150
+ )
151
+
152
+ def update_level_filter(self, level: str):
153
+ if level not in [l.name for l in LogLevel]:
154
+ logger.error(f"Logging level {level} unrecognized for filter!")
155
+ return
156
+ self._log_level = level
157
+ self._set_formatter_and_update_filter(self._line_formatter)
158
+
159
+ def update_search_filter(self, search_query: Pattern | str | None = None):
160
+ self._search_query = search_query
161
+ self._set_formatter_and_update_filter(self._line_formatter)
162
+
163
+ def update_time_filter(self, start: QDateTime | None, end: QDateTime | None):
164
+ self._timestamp_start = start
165
+ self._timestamp_end = end
166
+ self._set_formatter_and_update_filter(self._line_formatter)
167
+
168
+ def update_service_filter(self, services: set[str]):
169
+ self._selected_services = services
170
+ self._set_formatter_and_update_filter(self._line_formatter)
171
+
172
+ def update_line_formatter(self, line_formatter: LineFormatter):
173
+ self._set_formatter_and_update_filter(line_formatter)
174
+
175
+ def display_all(self) -> str:
176
+ return "\n".join(self._queue_formatter(self._data.copy()))
177
+
178
+ def format_new(self):
179
+ res = "\n".join(self._display_queue)
180
+ self._display_queue = deque([], self._max_length)
181
+ return res
182
+
183
+ def clear_logs(self):
184
+ self._data = deque([])
185
+ self._display_queue = deque([])
186
+
187
+ def fetch_history(self):
188
+ self._data = deque(
189
+ item["data"]
190
+ for item in self._conn.xread(
191
+ MessageEndpoints.log().endpoint, from_start=True, count=self._max_length
192
+ )
193
+ )
194
+
195
+ def unique_service_names_from_history(self) -> set[str]:
196
+ return set(msg.log_msg["service_name"] for msg in self._data)
197
+
198
+
199
+ class LogPanelToolbar(QWidget):
200
+
201
+ services_selected: pyqtBoundSignal = Signal(set)
202
+
203
+ def __init__(self, parent: QWidget | None = None) -> None:
204
+ super().__init__(parent)
205
+
206
+ # in unix time
207
+ self._timestamp_start: QDateTime | None = None
208
+ self._timestamp_end: QDateTime | None = None
209
+
210
+ self._unique_service_names: set[str] = set()
211
+ self._services_selected: set[str] | None = None
212
+
213
+ self.layout = QHBoxLayout(self) # type: ignore
214
+
215
+ self.service_choice_button = QPushButton("Select services", self)
216
+ self.layout.addWidget(self.service_choice_button)
217
+ self.service_choice_button.clicked.connect(self._open_service_filter_dialog)
218
+
219
+ self.filter_level_dropdown = self._log_level_box()
220
+ self.layout.addWidget(self.filter_level_dropdown)
221
+
222
+ self.clear_button = QPushButton("Clear all", self)
223
+ self.layout.addWidget(self.clear_button)
224
+ self.fetch_button = QPushButton("Fetch history", self)
225
+ self.layout.addWidget(self.fetch_button)
226
+
227
+ self._string_search_box()
228
+
229
+ self.timerange_button = QPushButton("Set time range", self)
230
+ self.layout.addWidget(self.timerange_button)
231
+
232
+ @property
233
+ def time_start(self):
234
+ return self._timestamp_start
235
+
236
+ @property
237
+ def time_end(self):
238
+ return self._timestamp_end
239
+
240
+ def _string_search_box(self):
241
+ self.layout.addWidget(QLabel("Search: "))
242
+ self.search_textbox = QLineEdit()
243
+ self.layout.addWidget(self.search_textbox)
244
+ self.layout.addWidget(QLabel("Use regex: "))
245
+ self.regex_enabled = QCheckBox()
246
+ self.layout.addWidget(self.regex_enabled)
247
+ self.update_re_button = QPushButton("Update search", self)
248
+ self.layout.addWidget(self.update_re_button)
249
+
250
+ def _log_level_box(self):
251
+ box = QComboBox()
252
+ box.setToolTip("Display logs with equal or greater significance to the selected level.")
253
+ [box.addItem(l.name) for l in LogLevel]
254
+ return box
255
+
256
+ def _current_ts(self, selection_type: Literal["start", "end"]):
257
+ if selection_type == "start":
258
+ return self._timestamp_start
259
+ elif selection_type == "end":
260
+ return self._timestamp_end
261
+ else:
262
+ raise ValueError(f"timestamps can only be for the start or end, not {selection_type}")
263
+
264
+ def _open_datetime_dialog(self):
265
+ """Open dialog window for timestamp filter selection"""
266
+ self._dt_dialog = QDialog(self)
267
+ self._dt_dialog.setWindowTitle("Time range selection")
268
+ layout = QVBoxLayout()
269
+
270
+ label_start = QLabel()
271
+ label_end = QLabel()
272
+
273
+ def date_button_set(selection_type: Literal["start", "end"], label: QLabel):
274
+ dt = self._current_ts(selection_type)
275
+ _layout = QHBoxLayout()
276
+ layout.addLayout(_layout)
277
+ date_button = QPushButton(f"Time {selection_type}")
278
+ _layout.addWidget(date_button)
279
+ label.setText(dt.toString() if dt else "not selected")
280
+ _layout.addWidget(label)
281
+ date_button.clicked.connect(partial(self._open_cal_dialog, selection_type, label))
282
+ date_clear_button = QPushButton("clear")
283
+ date_clear_button.clicked.connect(
284
+ lambda: (
285
+ partial(self._update_time, selection_type)(None),
286
+ label.setText("not selected"),
287
+ )
288
+ )
289
+ _layout.addWidget(date_clear_button)
290
+
291
+ for v in [("start", label_start), ("end", label_end)]:
292
+ date_button_set(*v)
293
+
294
+ close_button = QPushButton("Close")
295
+ close_button.clicked.connect(self._dt_dialog.accept)
296
+ layout.addWidget(close_button)
297
+ self._dt_dialog.setLayout(layout)
298
+ self._dt_dialog.exec()
299
+ self._dt_dialog = None
300
+
301
+ def _open_cal_dialog(self, selection_type: Literal["start", "end"], label: QLabel):
302
+ """Open dialog window for timestamp filter selection"""
303
+ dt = self._current_ts(selection_type) or QDateTime.currentDateTime()
304
+ label.setText(dt.toString() if dt else "not selected")
305
+ if selection_type == "start":
306
+ self._timestamp_start = dt
307
+ else:
308
+ self._timestamp_end = dt
309
+ self._cal_dialog = QDialog(self)
310
+ self._cal_dialog.setWindowTitle(f"Select time range {selection_type}")
311
+ layout = QVBoxLayout()
312
+ cal = QDateTimeEdit()
313
+ cal.setCalendarPopup(True)
314
+ cal.setDateTime(dt)
315
+ cal.setDisplayFormat("yyyy-MM-dd HH:mm:ss.zzz")
316
+ cal.dateTimeChanged.connect(partial(self._update_time, selection_type))
317
+ layout.addWidget(cal)
318
+ close_button = QPushButton("Close")
319
+ close_button.clicked.connect(self._cal_dialog.accept)
320
+ layout.addWidget(close_button)
321
+ self._cal_dialog.setLayout(layout)
322
+ self._cal_dialog.exec()
323
+ self._cal_dialog = None
324
+
325
+ def _update_time(self, selection_type: Literal["start", "end"], dt: QDateTime | None):
326
+ if selection_type == "start":
327
+ self._timestamp_start = dt
328
+ else:
329
+ self._timestamp_end = dt
330
+
331
+ @SafeSlot(dict, set)
332
+ def service_list_update(
333
+ self, services_info: dict[str, StatusMessage], services_from_history: set[str], *_, **__
334
+ ):
335
+ self._unique_service_names = set([s.split("/")[0] for s in services_info.keys()])
336
+ self._unique_service_names |= services_from_history
337
+ if self._services_selected is None:
338
+ self._services_selected = self._unique_service_names
339
+
340
+ @SafeSlot()
341
+ def _open_service_filter_dialog(self):
342
+ if len(self._unique_service_names) == 0 or self._services_selected is None:
343
+ return
344
+ self._svc_dialog = QDialog(self)
345
+ self._svc_dialog.setWindowTitle(f"Select services to show logs from")
346
+ layout = QVBoxLayout()
347
+ service_cb_grid = QGridLayout(parent=self)
348
+ layout.addLayout(service_cb_grid)
349
+
350
+ def check_box(name: str, checked: Qt.CheckState):
351
+ if checked == Qt.CheckState.Checked:
352
+ self._services_selected.add(name)
353
+ else:
354
+ if name in self._services_selected:
355
+ self._services_selected.remove(name)
356
+ self.services_selected.emit(self._services_selected)
357
+
358
+ for i, svc in enumerate(self._unique_service_names):
359
+ service_cb_grid.addWidget(QLabel(svc), i, 0)
360
+ cb = QCheckBox()
361
+ cb.setChecked(svc in self._services_selected)
362
+ cb.checkStateChanged.connect(partial(check_box, svc))
363
+ service_cb_grid.addWidget(cb, i, 1)
364
+
365
+ close_button = QPushButton("Close")
366
+ close_button.clicked.connect(self._svc_dialog.accept)
367
+ layout.addWidget(close_button)
368
+ self._svc_dialog.setLayout(layout)
369
+ self._svc_dialog.exec()
370
+ self._svc_dialog = None
371
+
372
+
373
+ class LogPanel(TextBox):
374
+ """Displays a log panel"""
375
+
376
+ ICON_NAME = "terminal"
377
+ _new_messages = Signal()
378
+ service_list_update = Signal(dict, set)
379
+
380
+ def __init__(self, parent=None, client: BECClient | None = None, **kwargs):
381
+ """Initialize the LogPanel widget."""
382
+ super().__init__(parent=parent, client=client, **kwargs)
383
+ self._update_colors()
384
+ self._service_status = BECServiceStatusMixin(self, client=self.client) # type: ignore
385
+ self._log_manager = BecLogsQueue(
386
+ self.client.connector, # type: ignore
387
+ new_message_signal=self._new_messages,
388
+ line_formatter=partial(simple_color_format, colors=self._colors),
389
+ )
390
+
391
+ self.toolbar = LogPanelToolbar(parent=parent)
392
+ self.toolbar_area = QScrollArea()
393
+ self.toolbar_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
394
+ self.toolbar_area.setSizeAdjustPolicy(QScrollArea.SizeAdjustPolicy.AdjustToContents)
395
+ self.toolbar_area.setFixedHeight(int(self.toolbar.clear_button.height() * 2))
396
+ self.toolbar_area.setWidget(self.toolbar)
397
+
398
+ self.layout.addWidget(self.toolbar_area)
399
+ self.toolbar.clear_button.clicked.connect(self._on_clear)
400
+ self.toolbar.fetch_button.clicked.connect(self._on_fetch)
401
+ self.toolbar.update_re_button.clicked.connect(self._on_re_update)
402
+ self.toolbar.search_textbox.returnPressed.connect(self._on_re_update)
403
+ self.toolbar.regex_enabled.checkStateChanged.connect(self._on_re_update)
404
+ self.toolbar.filter_level_dropdown.currentTextChanged.connect(self._set_level_filter)
405
+ self._new_messages.connect(self._on_append)
406
+
407
+ self.toolbar.timerange_button.clicked.connect(self._choose_datetime)
408
+ self._service_status.services_update.connect(self._update_service_list)
409
+ self.service_list_update.connect(self.toolbar.service_list_update)
410
+ self.toolbar.services_selected.connect(self._update_service_filter)
411
+
412
+ self.text_box_text_edit.setFont(QFont("monospace", 12))
413
+ self.text_box_text_edit.setHtml("")
414
+ self.text_box_text_edit.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
415
+
416
+ self._connect_to_theme_change()
417
+
418
+ @SafeSlot(set)
419
+ def _update_service_filter(self, services: set[str]):
420
+ self._log_manager.update_service_filter(services)
421
+ self._on_redraw()
422
+
423
+ @SafeSlot(dict, dict)
424
+ def _update_service_list(self, services_info: dict[str, StatusMessage], *_, **__):
425
+ self.service_list_update.emit(
426
+ services_info, self._log_manager.unique_service_names_from_history()
427
+ )
428
+
429
+ @SafeSlot()
430
+ def _choose_datetime(self):
431
+ self.toolbar._open_datetime_dialog()
432
+ self._set_time_filter()
433
+
434
+ def _connect_to_theme_change(self):
435
+ """Connect to the theme change signal."""
436
+ qapp = QApplication.instance()
437
+ if hasattr(qapp, "theme_signal"):
438
+ qapp.theme_signal.theme_updated.connect(self._on_redraw) # type: ignore
439
+
440
+ def _update_colors(self):
441
+ self._colors = DEFAULT_LOG_COLORS.copy()
442
+ self._colors.update({LogLevel.INFO: get_theme_palette().text().color().name()})
443
+
444
+ def _cursor_to_end(self):
445
+ c = self.text_box_text_edit.textCursor()
446
+ c.movePosition(c.MoveOperation.End)
447
+ self.text_box_text_edit.setTextCursor(c)
448
+
449
+ @SafeSlot()
450
+ @SafeSlot(str)
451
+ def _on_redraw(self, *_):
452
+ self._update_colors()
453
+ self._log_manager.update_line_formatter(partial(simple_color_format, colors=self._colors))
454
+ self.set_html_text(self._log_manager.display_all())
455
+ self._cursor_to_end()
456
+
457
+ @SafeSlot()
458
+ def _on_append(self):
459
+ self._cursor_to_end()
460
+ self.text_box_text_edit.insertHtml(self._log_manager.format_new())
461
+
462
+ @SafeSlot()
463
+ def _on_clear(self):
464
+ self._log_manager.clear_logs()
465
+ self.set_html_text(self._log_manager.display_all())
466
+ self._cursor_to_end()
467
+
468
+ @SafeSlot()
469
+ @SafeSlot(Qt.CheckState)
470
+ def _on_re_update(self, *_):
471
+ if self.toolbar.regex_enabled.isChecked():
472
+ try:
473
+ search_query = re.compile(self.toolbar.search_textbox.text())
474
+ except Exception as e:
475
+ logger.warning(f"Failed to compile search regex with error {e}")
476
+ search_query = None
477
+ logger.info(f"Setting LogPanel search regex to {search_query}")
478
+ else:
479
+ search_query = self.toolbar.search_textbox.text()
480
+ logger.info(f'Setting LogPanel search string to "{search_query}"')
481
+ self._log_manager.update_search_filter(search_query)
482
+ self.set_html_text(self._log_manager.display_all())
483
+ self._cursor_to_end()
484
+
485
+ @SafeSlot()
486
+ def _on_fetch(self):
487
+ self._log_manager.fetch_history()
488
+ self.set_html_text(self._log_manager.display_all())
489
+ self._cursor_to_end()
490
+
491
+ @SafeSlot(str)
492
+ def _set_level_filter(self, level: str):
493
+ self._log_manager.update_level_filter(level)
494
+ self._on_redraw()
495
+
496
+ @SafeSlot()
497
+ def _set_time_filter(self):
498
+ self._log_manager.update_time_filter(self.toolbar.time_start, self.toolbar.time_end)
499
+ self._on_redraw()
500
+
501
+ def cleanup(self):
502
+ self._log_manager.disconnect()
503
+ self._new_messages.disconnect(self._on_append)
504
+
505
+
506
+ if __name__ == "__main__": # pragma: no cover
507
+ import sys
508
+
509
+ from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
510
+
511
+ app = QApplication(sys.argv)
512
+ set_theme("dark")
513
+ widget = LogPanel()
514
+
515
+ widget.show()
516
+ sys.exit(app.exec())
@@ -0,0 +1,15 @@
1
+ def main(): # pragma: no cover
2
+ from qtpy import PYSIDE6
3
+
4
+ if not PYSIDE6:
5
+ print("PYSIDE6 is not available in the environment. Cannot patch designer.")
6
+ return
7
+ from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
8
+
9
+ from bec_widgets.widgets.utility.logpanel.log_panel_plugin import LogPanelPlugin
10
+
11
+ QPyDesignerCustomWidgetCollection.addCustomWidget(LogPanelPlugin())
12
+
13
+
14
+ if __name__ == "__main__": # pragma: no cover
15
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.18.0
3
+ Version: 1.19.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
@@ -2,11 +2,11 @@
2
2
  .gitlab-ci.yml,sha256=PuL-FmkTHm7qs467Mh9D8quWcEj4tgEA-UUGDieMuWk,8774
3
3
  .pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
4
4
  .readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
5
- CHANGELOG.md,sha256=B98wCPZX2DlBZgFN7_lKsUbqRTtbKP6e00Ix0CRm97M,225868
5
+ CHANGELOG.md,sha256=JsoUKOxpbxaYNfvXfDwnvrhGlQXreYGQaAM95trzq7o,226776
6
6
  LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
7
- PKG-INFO,sha256=RnI9kd_-U9csdK3_V2XwZRXeI4YpJgvxYDOXcSbdRyM,1219
7
+ PKG-INFO,sha256=ktXzXlqQyM2uA8iQbeKqLNvXeM6bWqZFjAKtb1DYVUQ,1219
8
8
  README.md,sha256=KgdKusjlvEvFtdNZCeDMO91y77MWK2iDcYMDziksOr4,2553
9
- pyproject.toml,sha256=qxhkAQWpSc-KQsj-Q57XvePUTKf0jVZfMdcp-r8-THs,2549
9
+ pyproject.toml,sha256=dBH-z_r3T8L74-Hq11fjOl-FowARiFQVUmkB2LiISB4,2549
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
@@ -24,7 +24,7 @@ bec_widgets/assets/app_icons/alignment_1d.png,sha256=5VouaWieb4lVv3wUBNHaO5ovUW2
24
24
  bec_widgets/assets/app_icons/bec_widgets_icon.png,sha256=K8dgGwIjalDh9PRHUsSQBqgdX7a00nM3igZdc20pkYM,1747017
25
25
  bec_widgets/cli/__init__.py,sha256=d0Q6Fn44e7wFfLabDOBxpcJ1DPKWlFunGYDUBmO-4hA,22
26
26
  bec_widgets/cli/auto_updates.py,sha256=Pj8OHlSlKN3JOAmuuBC5oUMzdfC8TYRY7QKT5BQ2cZo,5171
27
- bec_widgets/cli/client.py,sha256=QI27Eb6ehePBpFVhNzEFRfxxEOKTMFoCN9LNexsi8-g,99850
27
+ bec_widgets/cli/client.py,sha256=k1dSiZy4CBHb9pgfpV4ygTNaDUeqtWUbigKMxk9gtsY,100272
28
28
  bec_widgets/cli/client_utils.py,sha256=DXHHw1LNkKVCApbWmOAdZqU_qdvvqx28QiWV-Uf-jos,12207
29
29
  bec_widgets/cli/generate_cli.py,sha256=lT-eEXEPpzk9cPrfFVf0U5gxQg7HF2oulxDH16T-SBk,6849
30
30
  bec_widgets/cli/server.py,sha256=Hzhhzhc9PQhTtjpNeu8Jmbmahad0DbyieutaRp7Vejc,10485
@@ -59,7 +59,7 @@ bec_widgets/qt_utils/toolbar.py,sha256=YY_-UGc7uZhahYn7xnTvBGbalmTkpTa4WLikpsHwn
59
59
  bec_widgets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  bec_widgets/tests/utils.py,sha256=GbQtN7qf9n-8FoAfNddZ4aAqA7oBo_hGAlnKELd6Xzw,6943
61
61
  bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
62
- bec_widgets/utils/bec_connector.py,sha256=78QAlC7uAtxUW-7KTxoMUJu2aFve7IqnGkURYYNGFDo,10074
62
+ bec_widgets/utils/bec_connector.py,sha256=xMHOn0Rq2OShd0ONYEEZCJZR3oYGnORqMgxjDqgpLMU,10171
63
63
  bec_widgets/utils/bec_designer.py,sha256=XBy38NbNMoRDpvRx5lGP2XnJNG34YKZ7I-ARFkn-gzs,5017
64
64
  bec_widgets/utils/bec_dispatcher.py,sha256=OFmkx9vOz4pA4Sdc14QreyDZ870QYskJ4B5daVVeYg4,6325
65
65
  bec_widgets/utils/bec_signal_proxy.py,sha256=soKdA4pJL8S0d-93C0QqcIUxLA4rfb1-B1jyRXHmMxk,3011
@@ -87,11 +87,11 @@ bec_widgets/utils/widget_state_manager.py,sha256=tzrxVmnGa6IHSEdeh-R68aQ934BsuS9
87
87
  bec_widgets/utils/yaml_dialog.py,sha256=T6UyGNGdmpXW74fa_7Nk6b99T5pp2Wvyw3AOauRc8T8,2407
88
88
  bec_widgets/utils/plugin_templates/plugin.template,sha256=DWtJdHpdsVtbiTTOniH3zBe5a40ztQ20o_-Hclyu38s,1266
89
89
  bec_widgets/utils/plugin_templates/register.template,sha256=XyL3OZPT_FTArLAM8tHd5qMqv2ZuAbJAZLsNNnHcagU,417
90
- bec_widgets/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
90
+ bec_widgets/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
91
  bec_widgets/widgets/containers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  bec_widgets/widgets/containers/dock/__init__.py,sha256=B7foHt02gnhM7mFksa7GJVwT7n0j_JvYDCt6wc6XR5g,61
93
93
  bec_widgets/widgets/containers/dock/dock.py,sha256=_cw5bbqCLZxaD5LmosHNmm_eXelMDoTyxFdTnSWkoOI,10379
94
- bec_widgets/widgets/containers/dock/dock_area.py,sha256=9325BpsIR_0Del2O__iCIKjxsa_rJsnTzlkG8m21W1o,17943
94
+ bec_widgets/widgets/containers/dock/dock_area.py,sha256=5OZHapVkxdjWU9RwDxwscgvUJbA-khoIRg21vTQdavU,18354
95
95
  bec_widgets/widgets/containers/dock/dock_area.pyproject,sha256=URW0UrDXCnkzk80rbQmUMgF6Uqay2TjHsq8Dq0g1j-c,37
96
96
  bec_widgets/widgets/containers/dock/dock_area_plugin.py,sha256=fDVXKPZuHr85B2fLfAYf_Ic5d9mZQpnZrODTDquzZpM,1331
97
97
  bec_widgets/widgets/containers/dock/register_dock_area.py,sha256=L7BL4qknCjtqsDP-RMQzk2qRPpcYuzXWlb7sJB_0DDM,475
@@ -184,12 +184,15 @@ bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.pypro
184
184
  bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit_plugin.py,sha256=LoG1VyO21pZ9dbnDVU03xzqgP8P1oEmdeotlkYs_pE8,1466
185
185
  bec_widgets/widgets/control/device_input/device_line_edit/register_device_line_edit.py,sha256=NTB3HghW5S7NvUlPe_k_uFYQLWPYgjgln2bAYipfkpM,527
186
186
  bec_widgets/widgets/control/device_input/signal_combobox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
187
+ bec_widgets/widgets/control/device_input/signal_combobox/register_signal_combo_box.py,sha256=VEdFRUfLph7JE2arcnzHw8etsE-4wZkwyzlNLMJBsZk,526
188
+ bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box.pyproject,sha256=xod6iyRD-WD0Uk6LWXjSxFJCQy-831pvTkKcw2FAdnM,33
189
+ bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box_plugin.py,sha256=sstqm2KtyR5wwOIYJRbzOqHMq5_9ExKP-YS5qV5ACrA,1373
187
190
  bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py,sha256=0aOAq3e9MdZMXTGJQY7WqQSvl-oPnqwrfHjBKGzeRAI,4572
188
191
  bec_widgets/widgets/control/device_input/signal_line_edit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
189
192
  bec_widgets/widgets/control/device_input/signal_line_edit/register_signal_line_edit.py,sha256=aQLTy_3gbji0vq5VvvAddHFimpwGGaMYJy5iGgX23aM,527
190
193
  bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py,sha256=B1xnzP0XYf8PiOLj8pu7kVMhq3ZtFPRbQb84nXsKHI0,5174
191
194
  bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.pyproject,sha256=3NBnjBB6JRuF2W9-SR6x09KO1C2oB1IEV3VW__miIgI,34
192
- bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit_plugin.py,sha256=wIaGuYgMYOHswkC6cA4ZyacpfQsjMXn4PPAfvsJoVfU,1326
195
+ bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit_plugin.py,sha256=t2VBGsbysCL6154Z5Ny5Nk2UWcURMGS-ibVKiRvYs6Y,1384
193
196
  bec_widgets/widgets/control/scan_control/__init__.py,sha256=IOfHl15vxb_uC6KN62-PeUzbBha_vQyqkkXbJ2HU674,38
194
197
  bec_widgets/widgets/control/scan_control/register_scan_control.py,sha256=j7KrYSn9O6wp6ay2Yb7BWDjdbWzpkSLcCI0neeZi048,483
195
198
  bec_widgets/widgets/control/scan_control/scan_control.py,sha256=Nixsb7NrEn0eQzeax8jd2o2_omlpKN5s6IcWRsL_JhQ,18109
@@ -219,7 +222,7 @@ bec_widgets/widgets/editors/jupyter_console/__init__.py,sha256=47DEQpj8HBSa-_TIm
219
222
  bec_widgets/widgets/editors/jupyter_console/jupyter_console.py,sha256=-e7HQOECeH5eDrJYh4BFIzRL78LDkooU4otabyN0aX4,2343
220
223
  bec_widgets/widgets/editors/text_box/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
221
224
  bec_widgets/widgets/editors/text_box/register_text_box.py,sha256=xRgVugvjLhX3iKb-vaAxflE6pWpal7pVFWDaUSUZLyE,467
222
- bec_widgets/widgets/editors/text_box/text_box.py,sha256=7Pb6GMeAoh8caIEd2FRDsQyJtXxHlxZ473UuIPLYFJk,4265
225
+ bec_widgets/widgets/editors/text_box/text_box.py,sha256=F_BdWKPwEjltgfAsPmGHipOs5sPtI4o0Y0EO68s3Og0,4311
223
226
  bec_widgets/widgets/editors/text_box/text_box.pyproject,sha256=XohO1BIe2hrpU-z_KHKRgjcUkXru7jeFte31j2TPbNk,26
224
227
  bec_widgets/widgets/editors/text_box/text_box_plugin.py,sha256=0sgOZ_2Z0tpLGHYJyRlDGcPHWR79vzj38sQ6V9lnpjc,1310
225
228
  bec_widgets/widgets/editors/vscode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -310,6 +313,12 @@ bec_widgets/widgets/services/device_browser/register_device_browser.py,sha256=Or
310
313
  bec_widgets/widgets/services/device_browser/device_item/__init__.py,sha256=VGY-uNVCnpcY-q-gijteB2N8KxFNgYR-qQ209MVu1QI,36
311
314
  bec_widgets/widgets/services/device_browser/device_item/device_item.py,sha256=u3CTgXJBqwNP3uxrIWYGacEuoOu1a_DxfXYZ3Q4sK_M,1901
312
315
  bec_widgets/widgets/utility/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
316
+ bec_widgets/widgets/utility/logpanel/__init__.py,sha256=HldSvPLYgrqBjCgIQj0f7Wa4slkSMksk4bsRJOQi__Y,91
317
+ bec_widgets/widgets/utility/logpanel/_util.py,sha256=4vkN2KDJI8CHgwo_R8QjN76Lqg8zKoWAa_pWyFgLP94,1837
318
+ bec_widgets/widgets/utility/logpanel/log_panel.pyproject,sha256=2ncs1bsu-wICstR1gOYwFFdr0UuZmrBQEpwhvNKVFMY,26
319
+ bec_widgets/widgets/utility/logpanel/log_panel_plugin.py,sha256=KY7eS1uGZzLYtDAdBv6S2mw8UjcDGVt3UklN_D5M06A,1250
320
+ bec_widgets/widgets/utility/logpanel/logpanel.py,sha256=VxFXoy5kUS8AI42PzwZc556NrT-HNGD07_yc9zmiPyQ,19171
321
+ bec_widgets/widgets/utility/logpanel/register_log_panel.py,sha256=LFUE5JzCYvIwJQtTqZASLVAHYy3gO1nrHzPVH_kpCEY,470
313
322
  bec_widgets/widgets/utility/spinner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
314
323
  bec_widgets/widgets/utility/spinner/register_spinner_widget.py,sha256=96A13dEcyTgXfc9G0sTdlXYCDcVav8Z2P2eDC95bESQ,484
315
324
  bec_widgets/widgets/utility/spinner/spinner.py,sha256=6c0fN7mdGzELg4mf_yG08ubses3svb6w0EqMeHDFkIw,2651
@@ -341,8 +350,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=Z
341
350
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
342
351
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
343
352
  bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
344
- bec_widgets-1.18.0.dist-info/METADATA,sha256=RnI9kd_-U9csdK3_V2XwZRXeI4YpJgvxYDOXcSbdRyM,1219
345
- bec_widgets-1.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
346
- bec_widgets-1.18.0.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
347
- bec_widgets-1.18.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
348
- bec_widgets-1.18.0.dist-info/RECORD,,
353
+ bec_widgets-1.19.0.dist-info/METADATA,sha256=ktXzXlqQyM2uA8iQbeKqLNvXeM6bWqZFjAKtb1DYVUQ,1219
354
+ bec_widgets-1.19.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
355
+ bec_widgets-1.19.0.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
356
+ bec_widgets-1.19.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
357
+ bec_widgets-1.19.0.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "1.18.0"
7
+ version = "1.19.0"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [