bec-widgets 1.0.1__py3-none-any.whl → 1.1.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 +26 -24
- PKG-INFO +1 -1
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +60 -84
- bec_widgets/cli/client.py +56 -0
- bec_widgets/tests/__init__.py +0 -0
- bec_widgets/tests/utils.py +226 -0
- bec_widgets/utils/filter_io.py +156 -0
- bec_widgets/utils/widget_io.py +12 -9
- bec_widgets/widgets/base_classes/device_input_base.py +331 -62
- bec_widgets/widgets/base_classes/device_signal_input_base.py +280 -0
- bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py +1 -1
- bec_widgets/widgets/device_combobox/device_combo_box_plugin.py +1 -1
- bec_widgets/widgets/device_combobox/device_combobox.py +118 -41
- bec_widgets/widgets/device_line_edit/device_line_edit.py +122 -59
- bec_widgets/widgets/device_line_edit/device_line_edit_plugin.py +1 -1
- bec_widgets/widgets/image/image_widget.py +7 -1
- bec_widgets/widgets/motor_map/motor_map_widget.py +4 -2
- bec_widgets/widgets/positioner_box/positioner_box.py +4 -1
- bec_widgets/widgets/scan_control/scan_control.py +2 -3
- bec_widgets/widgets/scan_control/scan_group_box.py +3 -1
- bec_widgets/widgets/signal_combobox/__init__.py +0 -0
- bec_widgets/widgets/signal_combobox/register_signal_combobox.py +15 -0
- bec_widgets/widgets/signal_combobox/signal_combobox.py +115 -0
- bec_widgets/widgets/signal_combobox/signal_combobox.pyproject +1 -0
- bec_widgets/widgets/signal_combobox/signal_combobox_plugin.py +54 -0
- bec_widgets/widgets/signal_line_edit/__init__.py +0 -0
- bec_widgets/widgets/signal_line_edit/register_signal_line_edit.py +15 -0
- bec_widgets/widgets/signal_line_edit/signal_line_edit.py +140 -0
- bec_widgets/widgets/signal_line_edit/signal_line_edit.pyproject +1 -0
- bec_widgets/widgets/signal_line_edit/signal_line_edit_plugin.py +54 -0
- {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/METADATA +1 -1
- {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/RECORD +36 -22
- pyproject.toml +1 -1
- {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
from bec_lib.callback_handler import EventType
|
2
|
+
from bec_lib.device import Signal
|
3
|
+
from bec_lib.logger import bec_logger
|
4
|
+
from ophyd import Kind
|
5
|
+
from qtpy.QtCore import Property, Slot
|
6
|
+
|
7
|
+
from bec_widgets.utils import ConnectionConfig
|
8
|
+
from bec_widgets.utils.bec_widget import BECWidget
|
9
|
+
from bec_widgets.utils.filter_io import FilterIO
|
10
|
+
from bec_widgets.utils.widget_io import WidgetIO
|
11
|
+
|
12
|
+
logger = bec_logger.logger
|
13
|
+
|
14
|
+
|
15
|
+
class DeviceSignalInputBaseConfig(ConnectionConfig):
|
16
|
+
"""Configuration class for DeviceSignalInputBase."""
|
17
|
+
|
18
|
+
signal_filter: str | list[str] | None = None
|
19
|
+
default: str | None = None
|
20
|
+
arg_name: str | None = None
|
21
|
+
device: str | None = None
|
22
|
+
signals: list[str] | None = None
|
23
|
+
|
24
|
+
|
25
|
+
class DeviceSignalInputBase(BECWidget):
|
26
|
+
"""
|
27
|
+
Mixin base class for device signal input widgets.
|
28
|
+
Mixin class for device signal input widgets. This class provides methods to get the device signal list and device
|
29
|
+
signal object based on the current text of the widget.
|
30
|
+
"""
|
31
|
+
|
32
|
+
_filter_handler = {
|
33
|
+
Kind.hinted: "include_hinted_signals",
|
34
|
+
Kind.normal: "include_normal_signals",
|
35
|
+
Kind.config: "include_config_signals",
|
36
|
+
}
|
37
|
+
|
38
|
+
def __init__(self, client=None, config=None, gui_id: str = None):
|
39
|
+
if config is None:
|
40
|
+
config = DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
|
41
|
+
else:
|
42
|
+
if isinstance(config, dict):
|
43
|
+
config = DeviceSignalInputBaseConfig(**config)
|
44
|
+
self.config = config
|
45
|
+
super().__init__(client=client, config=config, gui_id=gui_id)
|
46
|
+
|
47
|
+
self._device = None
|
48
|
+
self.get_bec_shortcuts()
|
49
|
+
self._signal_filter = []
|
50
|
+
self._signals = []
|
51
|
+
self._hinted_signals = []
|
52
|
+
self._normal_signals = []
|
53
|
+
self._config_signals = []
|
54
|
+
self.bec_dispatcher.client.callbacks.register(
|
55
|
+
EventType.DEVICE_UPDATE, self.update_signals_from_filters
|
56
|
+
)
|
57
|
+
|
58
|
+
### Qt Slots ###
|
59
|
+
|
60
|
+
@Slot(str)
|
61
|
+
def set_signal(self, signal: str):
|
62
|
+
"""
|
63
|
+
Set the signal.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
signal (str): signal name.
|
67
|
+
"""
|
68
|
+
if self.validate_signal(signal) is True:
|
69
|
+
WidgetIO.set_value(widget=self, value=signal)
|
70
|
+
self.config.default = signal
|
71
|
+
else:
|
72
|
+
logger.warning(
|
73
|
+
f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}."
|
74
|
+
)
|
75
|
+
|
76
|
+
@Slot(str)
|
77
|
+
def set_device(self, device: str | None):
|
78
|
+
"""
|
79
|
+
Set the device. If device is not valid, device will be set to None which happpens
|
80
|
+
|
81
|
+
Args:
|
82
|
+
device(str): device name.
|
83
|
+
"""
|
84
|
+
if self.validate_device(device) is False:
|
85
|
+
self._device = None
|
86
|
+
else:
|
87
|
+
self._device = device
|
88
|
+
self.update_signals_from_filters()
|
89
|
+
|
90
|
+
@Slot(dict, dict)
|
91
|
+
@Slot()
|
92
|
+
def update_signals_from_filters(
|
93
|
+
self, content: dict | None = None, metadata: dict | None = None
|
94
|
+
):
|
95
|
+
"""Update the filters for the device signals based on list in self.signal_filter.
|
96
|
+
In addition, store the hinted, normal and config signals in separate lists to allow
|
97
|
+
customisation within QLineEdit.
|
98
|
+
|
99
|
+
Note:
|
100
|
+
Signal and ComputedSignals have no signals. The naming convention follows the device name.
|
101
|
+
"""
|
102
|
+
self.config.signal_filter = self.signal_filter
|
103
|
+
# pylint: disable=protected-access
|
104
|
+
self._hinted_signals = []
|
105
|
+
self._normal_signals = []
|
106
|
+
self._config_signals = []
|
107
|
+
if self.validate_device(self._device) is False:
|
108
|
+
self._device = None
|
109
|
+
self.config.device = self._device
|
110
|
+
return
|
111
|
+
device = self.get_device_object(self._device)
|
112
|
+
# See above convention for Signals and ComputedSignals
|
113
|
+
if isinstance(device, Signal):
|
114
|
+
self._signals = [self._device]
|
115
|
+
FilterIO.set_selection(widget=self, selection=[self._device])
|
116
|
+
return
|
117
|
+
device_info = device._info["signals"]
|
118
|
+
if Kind.hinted in self.signal_filter:
|
119
|
+
hinted_signals = [
|
120
|
+
signal
|
121
|
+
for signal, signal_info in device_info.items()
|
122
|
+
if (signal_info.get("kind_str", None) == str(Kind.hinted.value))
|
123
|
+
]
|
124
|
+
self._hinted_signals = hinted_signals
|
125
|
+
if Kind.normal in self.signal_filter:
|
126
|
+
normal_signals = [
|
127
|
+
signal
|
128
|
+
for signal, signal_info in device_info.items()
|
129
|
+
if (signal_info.get("kind_str", None) == str(Kind.normal.value))
|
130
|
+
]
|
131
|
+
self._normal_signals = normal_signals
|
132
|
+
if Kind.config in self.signal_filter:
|
133
|
+
config_signals = [
|
134
|
+
signal
|
135
|
+
for signal, signal_info in device_info.items()
|
136
|
+
if (signal_info.get("kind_str", None) == str(Kind.config.value))
|
137
|
+
]
|
138
|
+
self._config_signals = config_signals
|
139
|
+
self._signals = self._hinted_signals + self._normal_signals + self._config_signals
|
140
|
+
FilterIO.set_selection(widget=self, selection=self.signals)
|
141
|
+
|
142
|
+
### Qt Properties ###
|
143
|
+
|
144
|
+
@Property(str)
|
145
|
+
def device(self) -> str:
|
146
|
+
"""Get the selected device."""
|
147
|
+
if self._device is None:
|
148
|
+
return ""
|
149
|
+
return self._device
|
150
|
+
|
151
|
+
@device.setter
|
152
|
+
def device(self, value: str):
|
153
|
+
"""Set the device and update the filters, only allow devices present in the devicemanager."""
|
154
|
+
self._device = value
|
155
|
+
self.config.device = value
|
156
|
+
self.update_signals_from_filters()
|
157
|
+
|
158
|
+
@Property(bool)
|
159
|
+
def include_hinted_signals(self):
|
160
|
+
"""Include hinted signals in filters."""
|
161
|
+
return Kind.hinted in self.signal_filter
|
162
|
+
|
163
|
+
@include_hinted_signals.setter
|
164
|
+
def include_hinted_signals(self, value: bool):
|
165
|
+
if value:
|
166
|
+
self._signal_filter.append(Kind.hinted)
|
167
|
+
else:
|
168
|
+
self._signal_filter.remove(Kind.hinted)
|
169
|
+
self.update_signals_from_filters()
|
170
|
+
|
171
|
+
@Property(bool)
|
172
|
+
def include_normal_signals(self):
|
173
|
+
"""Include normal signals in filters."""
|
174
|
+
return Kind.normal in self.signal_filter
|
175
|
+
|
176
|
+
@include_normal_signals.setter
|
177
|
+
def include_normal_signals(self, value: bool):
|
178
|
+
if value:
|
179
|
+
self._signal_filter.append(Kind.normal)
|
180
|
+
else:
|
181
|
+
self._signal_filter.remove(Kind.normal)
|
182
|
+
self.update_signals_from_filters()
|
183
|
+
|
184
|
+
@Property(bool)
|
185
|
+
def include_config_signals(self):
|
186
|
+
"""Include config signals in filters."""
|
187
|
+
return Kind.config in self.signal_filter
|
188
|
+
|
189
|
+
@include_config_signals.setter
|
190
|
+
def include_config_signals(self, value: bool):
|
191
|
+
if value:
|
192
|
+
self._signal_filter.append(Kind.config)
|
193
|
+
else:
|
194
|
+
self._signal_filter.remove(Kind.config)
|
195
|
+
self.update_signals_from_filters()
|
196
|
+
|
197
|
+
### Properties and Methods ###
|
198
|
+
|
199
|
+
@property
|
200
|
+
def signals(self) -> list[str]:
|
201
|
+
"""
|
202
|
+
Get the list of device signals for the applied filters.
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
list[str]: List of device signals.
|
206
|
+
"""
|
207
|
+
return self._signals
|
208
|
+
|
209
|
+
@signals.setter
|
210
|
+
def signals(self, value: list[str]):
|
211
|
+
self._signals = value
|
212
|
+
self.config.signals = value
|
213
|
+
FilterIO.set_selection(widget=self, selection=value)
|
214
|
+
|
215
|
+
@property
|
216
|
+
def signal_filter(self) -> list[str]:
|
217
|
+
"""Get the list of filters to apply on the device signals."""
|
218
|
+
return self._signal_filter
|
219
|
+
|
220
|
+
def get_available_filters(self) -> list[str]:
|
221
|
+
"""Get the available filters."""
|
222
|
+
return [entry for entry in self._filter_handler]
|
223
|
+
|
224
|
+
def set_filter(self, filter_selection: str | list[str]):
|
225
|
+
"""
|
226
|
+
Set the device filter. If None, all devices are included.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
filter_selection (str | list[str]): Device filters from BECDeviceFilter and BECReadoutPriority.
|
230
|
+
"""
|
231
|
+
filters = None
|
232
|
+
if isinstance(filter_selection, list):
|
233
|
+
filters = [self._filter_handler.get(entry) for entry in filter_selection]
|
234
|
+
if isinstance(filter_selection, str):
|
235
|
+
filters = [self._filter_handler.get(filter_selection)]
|
236
|
+
if filters is None:
|
237
|
+
return
|
238
|
+
for entry in filters:
|
239
|
+
setattr(self, entry, True)
|
240
|
+
|
241
|
+
def get_device_object(self, device: str) -> object | None:
|
242
|
+
"""
|
243
|
+
Get the device object based on the device name.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
device(str): Device name.
|
247
|
+
|
248
|
+
Returns:
|
249
|
+
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
250
|
+
"""
|
251
|
+
self.validate_device(device)
|
252
|
+
dev = getattr(self.dev, device.lower(), None)
|
253
|
+
if dev is None:
|
254
|
+
logger.warning(f"Device {device} not found in devicemanager.")
|
255
|
+
return None
|
256
|
+
return dev
|
257
|
+
|
258
|
+
def validate_device(self, device: str | None, raise_on_false: bool = False) -> bool:
|
259
|
+
"""
|
260
|
+
Validate the device if it is present in current BEC instance.
|
261
|
+
|
262
|
+
Args:
|
263
|
+
device(str): Device to validate.
|
264
|
+
"""
|
265
|
+
if device in self.dev:
|
266
|
+
return True
|
267
|
+
if raise_on_false is True:
|
268
|
+
raise ValueError(f"Device {device} not found in devicemanager.")
|
269
|
+
return False
|
270
|
+
|
271
|
+
def validate_signal(self, signal: str) -> bool:
|
272
|
+
"""
|
273
|
+
Validate the signal if it is present in the device signals.
|
274
|
+
|
275
|
+
Args:
|
276
|
+
signal(str): Signal to validate.
|
277
|
+
"""
|
278
|
+
if signal in self.signals:
|
279
|
+
return True
|
280
|
+
return False
|
@@ -1,86 +1,163 @@
|
|
1
|
-
from
|
1
|
+
from bec_lib.callback_handler import EventType
|
2
|
+
from bec_lib.device import ReadoutPriority
|
3
|
+
from qtpy.QtCore import QSize, Signal, Slot
|
4
|
+
from qtpy.QtGui import QPainter, QPaintEvent, QPen
|
5
|
+
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
2
6
|
|
3
|
-
from
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
from bec_widgets.utils.colors import get_accent_colors
|
8
|
+
from bec_widgets.widgets.base_classes.device_input_base import (
|
9
|
+
BECDeviceFilter,
|
10
|
+
DeviceInputBase,
|
11
|
+
DeviceInputConfig,
|
12
|
+
)
|
9
13
|
|
10
14
|
|
11
15
|
class DeviceComboBox(DeviceInputBase, QComboBox):
|
12
16
|
"""
|
13
|
-
|
17
|
+
Combobox widget for device input with autocomplete for device names.
|
14
18
|
|
15
19
|
Args:
|
16
20
|
parent: Parent widget.
|
17
21
|
client: BEC client object.
|
18
22
|
config: Device input configuration.
|
19
23
|
gui_id: GUI ID.
|
20
|
-
device_filter: Device filter, name of the device class.
|
24
|
+
device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
|
21
25
|
default: Default device name.
|
22
26
|
arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
|
23
27
|
"""
|
24
28
|
|
25
29
|
ICON_NAME = "list_alt"
|
26
30
|
|
31
|
+
device_selected = Signal(str)
|
32
|
+
device_config_update = Signal()
|
33
|
+
|
27
34
|
def __init__(
|
28
35
|
self,
|
29
36
|
parent=None,
|
30
37
|
client=None,
|
31
38
|
config: DeviceInputConfig = None,
|
32
39
|
gui_id: str | None = None,
|
33
|
-
device_filter:
|
40
|
+
device_filter: BECDeviceFilter | list[BECDeviceFilter] | None = None,
|
41
|
+
readout_priority_filter: (
|
42
|
+
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
|
43
|
+
) = None,
|
44
|
+
available_devices: list[str] | None = None,
|
34
45
|
default: str | None = None,
|
35
46
|
arg_name: str | None = None,
|
36
47
|
):
|
37
48
|
super().__init__(client=client, config=config, gui_id=gui_id)
|
38
49
|
QComboBox.__init__(self, parent=parent)
|
39
|
-
self.setMinimumSize(125, 26)
|
40
|
-
self.populate_combobox()
|
41
|
-
|
42
50
|
if arg_name is not None:
|
43
51
|
self.config.arg_name = arg_name
|
52
|
+
self.arg_name = arg_name
|
53
|
+
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
54
|
+
self.setMinimumSize(QSize(100, 0))
|
55
|
+
self._callback_id = None
|
56
|
+
self._is_valid_input = False
|
57
|
+
self._accent_colors = get_accent_colors()
|
58
|
+
# We do not consider the config that is passed here, this produced problems
|
59
|
+
# with QtDesigner, since config and input arguments may differ and resolve properly
|
60
|
+
# Implementing this logic and config recoverage is postponed.
|
61
|
+
# Set available devices if passed
|
62
|
+
if available_devices is not None:
|
63
|
+
self.set_available_devices(available_devices)
|
64
|
+
# Set readout priority filter default is all
|
65
|
+
if readout_priority_filter is not None:
|
66
|
+
self.set_readout_priority_filter(readout_priority_filter)
|
67
|
+
else:
|
68
|
+
self.set_readout_priority_filter(
|
69
|
+
[
|
70
|
+
ReadoutPriority.MONITORED,
|
71
|
+
ReadoutPriority.BASELINE,
|
72
|
+
ReadoutPriority.ASYNC,
|
73
|
+
ReadoutPriority.CONTINUOUS,
|
74
|
+
ReadoutPriority.ON_REQUEST,
|
75
|
+
]
|
76
|
+
)
|
77
|
+
# Device filter default is None
|
44
78
|
if device_filter is not None:
|
45
79
|
self.set_device_filter(device_filter)
|
80
|
+
# Set default device if passed
|
46
81
|
if default is not None:
|
47
|
-
self.
|
48
|
-
|
49
|
-
|
82
|
+
self.set_device(default)
|
83
|
+
self._callback_id = self.bec_dispatcher.client.callbacks.register(
|
84
|
+
EventType.DEVICE_UPDATE, self.on_device_update
|
85
|
+
)
|
86
|
+
self.device_config_update.connect(self.update_devices_from_filters)
|
87
|
+
self.currentTextChanged.connect(self.check_validity)
|
88
|
+
self.check_validity(self.currentText())
|
89
|
+
|
90
|
+
def on_device_update(self, action: str, content: dict) -> None:
|
50
91
|
"""
|
51
|
-
|
92
|
+
Callback for device update events. Triggers the device_update signal.
|
52
93
|
|
53
94
|
Args:
|
54
|
-
|
95
|
+
action (str): The action that triggered the event.
|
96
|
+
content (dict): The content of the config update.
|
55
97
|
"""
|
56
|
-
|
57
|
-
|
98
|
+
if action in ["add", "remove", "reload"]:
|
99
|
+
self.device_config_update.emit()
|
100
|
+
|
101
|
+
def cleanup(self):
|
102
|
+
"""Cleanup the widget."""
|
103
|
+
if self._callback_id is not None:
|
104
|
+
self.bec_dispatcher.client.callbacks.remove(self._callback_id)
|
58
105
|
|
59
|
-
def
|
106
|
+
def get_current_device(self) -> object:
|
60
107
|
"""
|
61
|
-
|
108
|
+
Get the current device object based on the current value.
|
62
109
|
|
63
|
-
|
64
|
-
|
110
|
+
Returns:
|
111
|
+
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
65
112
|
"""
|
66
|
-
|
67
|
-
self.
|
113
|
+
dev_name = self.currentText()
|
114
|
+
return self.get_device_object(dev_name)
|
68
115
|
|
69
|
-
def
|
70
|
-
"""
|
71
|
-
self.devices = self.get_device_list(self.config.device_filter)
|
72
|
-
self.clear()
|
73
|
-
self.addItems(self.devices)
|
116
|
+
def paintEvent(self, event: QPaintEvent) -> None:
|
117
|
+
"""Extend the paint event to set the border color based on the validity of the input.
|
74
118
|
|
75
|
-
|
119
|
+
Args:
|
120
|
+
event (PySide6.QtGui.QPaintEvent) : Paint event.
|
76
121
|
"""
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
122
|
+
# logger.info(f"Received paint event: {event} in {self.__class__}")
|
123
|
+
super().paintEvent(event)
|
124
|
+
|
125
|
+
if self._is_valid_input is False and self.isEnabled() is True:
|
126
|
+
painter = QPainter(self)
|
127
|
+
pen = QPen()
|
128
|
+
pen.setWidth(2)
|
129
|
+
pen.setColor(self._accent_colors.emergency)
|
130
|
+
painter.setPen(pen)
|
131
|
+
painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
|
132
|
+
painter.end()
|
133
|
+
|
134
|
+
@Slot(str)
|
135
|
+
def check_validity(self, input_text: str) -> None:
|
136
|
+
"""
|
137
|
+
Check if the current value is a valid device name.
|
81
138
|
"""
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
139
|
+
if self.validate_device(input_text) is True:
|
140
|
+
self._is_valid_input = True
|
141
|
+
self.device_selected.emit(input_text.lower())
|
142
|
+
else:
|
143
|
+
self._is_valid_input = False
|
144
|
+
self.update()
|
145
|
+
|
146
|
+
|
147
|
+
if __name__ == "__main__": # pragma: no cover
|
148
|
+
# pylint: disable=import-outside-toplevel
|
149
|
+
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
150
|
+
|
151
|
+
from bec_widgets.utils.colors import set_theme
|
152
|
+
|
153
|
+
app = QApplication([])
|
154
|
+
set_theme("dark")
|
155
|
+
widget = QWidget()
|
156
|
+
widget.setFixedSize(200, 200)
|
157
|
+
layout = QVBoxLayout()
|
158
|
+
widget.setLayout(layout)
|
159
|
+
combo = DeviceComboBox()
|
160
|
+
combo.devices = ["samx", "dev1", "dev2", "dev3", "dev4"]
|
161
|
+
layout.addWidget(combo)
|
162
|
+
widget.show()
|
163
|
+
app.exec_()
|