-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpluginTemplateGUI.py
More file actions
executable file
·275 lines (225 loc) · 11.4 KB
/
Copy pathpluginTemplateGUI.py
File metadata and controls
executable file
·275 lines (225 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
"""
This is a template for a plugin GUI implementation in pyIVLS
This file should provide
- functions for interaction with other plugins (those that will be exported on get_functions hook call, these should not start with "_")
- functions that will implement functionality of the hooks (see pyIVLS_pluginTemplate)
- GUI functionality - code that interracts with Qt GUI elements from widgets
The standard implementation may (but not must) include
- GUI a Qt widget implementation
- GUI functionality (e.g. pluginTemplateGUI.py) - code that interracts with Qt GUI elements from widgets
- plugin core implementation - a set of functions that may be used outside of GUI
"""
import os
from MplCanvas import MplCanvas
from plugin_components import (
CloseLockSignalProvider,
DependencyManager,
LoggingHelper,
get_public_methods,
load_widget,
public,
)
from pluginTemplate import pluginTemplate
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QWidget
# this is loade from components directory that contains shared classes
class pluginTemplateGUI(QObject):
@property
def settingsWidget(self) -> QWidget:
if not hasattr(self, "_settingsWidget"):
raise NotImplementedError("Settings widget not implemented, remove this function if settings widget is not needed.")
if self._settingsWidget is None:
raise RuntimeError("Settings widget not initialized.")
return self._settingsWidget
@property
def MDIWidget(self) -> QWidget:
if not hasattr(self, "_mdiWidget"):
raise NotImplementedError("MDI widget not implemented, remove this function if MDI widget is not needed.")
if self._mdiWidget is None:
raise RuntimeError("MDI widget not initialized.")
return self._mdiWidget
def notify_user(self, message: str):
"""Utility to create popup and corresponding log entry for events that should be clearly visible"""
self.logger.log_info(message)
self.logger.info_popup(message)
update_gui_signal = pyqtSignal()
########Functions
def __init__(self):
super().__init__() ### this is needed if the class is a child of QObject
self.path = os.path.dirname(__file__) + os.path.sep
# remove load_widget if no widgets are needed
self._settingsWidget, self._mdiWidget = load_widget(settings=True, mdi=True, path=self.path)
# Initialize the functionality core that should be independent on GUI
self.templateFunctionality = pluginTemplate()
# init settings
self.settings = {}
# connect signals and slots
self._connect_signals()
# create plot
self._create_plt()
# initialize logger
self.logger = LoggingHelper(self)
# initialize dependency manager
self.dm = DependencyManager(
"pluginTemplate",
{"camera": ["parse_settings_widget", "setSettings", "set_gui_from_settings"]},
)
# initialize closelock if needed
self.cl = CloseLockSignalProvider()
# prepare GUI
self.settingsWidget.doubleSpinBox_float.setRange(0, 1)
self.settingsWidget.comboBox_categorical.addItems(["option1", "option2", "option3"])
# HOX: things such as adding items for comboboxes MUST be done here to prevent duplicate entries
# when initGUI is repeatedly called from pyIVLS_container when plugin list is updated.
def _connect_signals(self):
self.settingsWidget.pushButton_doStuff.clicked.connect(self._exampleAction)
self.update_gui_signal.connect(self._update_gui)
def _create_plt(self):
self.sc = MplCanvas(self, width=5, height=4, dpi=100)
self.axes = self.sc.fig.add_subplot(111)
self.axes.set_xlabel("time (HH:MM)")
self.axes.set_ylabel(r"Temperature ($^\circ$C)")
# self.MDIWidget.previewForm.addWidget(self.sc._create_toolbar(self.MDIWidget))
# self.MDIWidget.previewForm.addWidget(self.sc)
########Functions
########GUI Slots
# This section should contain functions that should react to GUI events.
@pyqtSlot()
def _exampleAction(self):
# do something
print("Button clicked!!")
@pyqtSlot()
def _update_gui(self):
# update GUI elements based on the internal state of the plugin. No type checking here since the internal is always safe.
self.settingsWidget.doubleSpinBox_float.setValue(self.settings["float"])
self.settingsWidget.spinBox_integer.setValue(self.settings["int"])
self.settingsWidget.lineEdit_str.setText(self.settings["str"])
self.settingsWidget.comboBox_categorical.setCurrentText(self.settings["category"])
# update combobox for deps
self.dm.initialize_dependency_selection(self.settings)
self._refresh_dependency_comboboxes(self.settings)
def _refresh_dependency_comboboxes(self, settings: dict | None = None):
available = self.dm.get_available_dependency_plugins().get("camera", [])
self.settingsWidget.camBox.clear()
self.settingsWidget.camBox.addItems(available)
preferred = ""
if settings is not None:
preferred = settings.get("camera", "")
if not preferred:
preferred = self.settings.get("camera", "") if hasattr(self, "settings") else ""
if preferred and preferred in available:
self.settingsWidget.camBox.setCurrentText(preferred)
@pyqtSlot()
def _update_plot(self, x, y):
self.axes.clear()
self.axes.plot(x, y)
self.sc.draw()
########Functions
################################### internal
def _validate_settings(self, settings: dict) -> tuple[int, str]:
"""Validate settings dict and convert values to correct dtype
Args:
settings (dict): settings dict with correct data types, so not the initial .ini dict
Returns:
tuple[int, str]: status code, error message (empty if no error)
"""
try:
settings["float"] = float(settings["float"])
settings["int"] = int(settings["int"])
if not (0 <= settings["float"] <= 1):
return (1, "Float value should be between 0 and 1.")
if settings["category"] not in ["option1", "option2", "option3"]:
return (1, "Category should be one of the following: option1, option2, option3.")
if settings["int"] < 0:
return (1, "Integer value should be non-negative.")
if len(settings["str"]) == 0:
return (1, "String value should not be empty.")
return (0, "")
# catch conversion errors if values read from gui
except ValueError as e:
return (1, f"Invalid data type in settings: {e}")
########Functions
###############GUI setting up
def _initGUI(
self,
plugin_info: dict,
):
"""Initialize the GUI components with the provided plugin information.
This should not set the internal state of the plugin, but only set the GUI elements.
It should be possible to call this function multiple times without
side effects. This will in fact be called multiple times, since pyIVLS_container calls get_setup_interface
every time the pluginlist is updated.
Args:
plugin_info (dict): dictionary with settings obtained from plugin_data in pyIVLS_
"""
# initialize dependency manager with the provided settings to initialize combobox
self.dm.initialize_dependency_selection(plugin_info)
self._refresh_dependency_comboboxes(plugin_info)
# These are unguarded, meaning that a user messing around in the .ini file
# can cause an unhandled exception here.
# IMO this is the best way to handle wrong settings on startup, since the crash happens early and before data loss and
# the error is fairly descriptive. Besides, handling these errors would involve
# guessing what the value should actually be which could just lead to more pain down the line.
# set actual values to GUI from plugin info
self.settingsWidget.doubleSpinBox_float.setValue(float(plugin_info["float"]))
self.settingsWidget.spinBox_integer.setValue(int(plugin_info["int"]))
self.settingsWidget.lineEdit_str.setText(plugin_info["str"])
self.settingsWidget.comboBox_categorical.setCurrentText(plugin_info["category"])
########Functions
###############GUI react to change
########Functions
########plugins interraction
def _get_public_methods(self):
return get_public_methods(self)
@public
def setSettings(self, settings: dict):
"""Set the settings for the templatePlugin.
Args:
settings (dict): dictionary with settings for the templatePlugin.
"""
status, error_message = self._validate_settings(settings)
if status != 0:
return (1, {"Error message": error_message})
self.settings = settings
# update settings for deps:
self.dm.set_dependency_settings(settings)
return (0, {"Error message": "ok"})
@public
def set_gui_from_settings(self):
"""Set the GUI elements from the internal settings. This can be used after settings have been updated from an external plugin to update the GUI accordingly."""
# Here we can assume that self.settings contains the values in correct datatype since they are checked before writing to settings.
self.update_gui_signal.emit()
self.dm.update_dep_guis()
########Functions to be used externally
########Public API
@public
def parse_settings_widget(self) -> tuple[int, dict]:
"""Parses the settings widget for the templatePlugin. Extracts current values.
Checks if values are allowed. Provides settings of template plugin to an external plugin
Returns (status, settings_dict):
status: 0 - no error, ~0 - error (add error code later on if needed)
self.settings
"""
ts = {} # init temporary settings dict to store the values before writing to internal
ts["float"] = self.settingsWidget.doubleSpinBox_float.value() # doublespinbox returns float
ts["int"] = self.settingsWidget.spinBox_integer.value() # spinbox returns int
ts["str"] = self.settingsWidget.lineEdit_str.text() # lineedit returns str
ts["category"] = self.settingsWidget.comboBox_categorical.currentText() # combobox returns str
ts["camera"] = self.settingsWidget.camBox.currentText()
self.dm.set_selected_dependency_plugins({"camera": ts["camera"]})
# checking may not be necessary if widgets are selected/designed in a way that they do not allow wrong values
# but checking here might be useful.
status, error_message = self._validate_settings(ts)
if status != 0:
self.notify_user(f"Error in settings: {error_message}")
return (status, {"Error message": error_message})
# parse dependency settings:
status, ts = self.dm.parse_dependencies(ts)
print("Dependency parsing result: ", ts)
if status != 0:
# non zero status means error in parsing dependencies, error message is included in ts dict
self.notify_user(f"Error in dependency settings: {ts['Error message']}")
return status, ts
# all good, write to internal and return
self.settings = ts
return (0, ts)