forked from Ulm-IQO/qudi
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mapper.py
473 lines (420 loc) · 18.9 KB
/
mapper.py
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# -*- coding: utf-8 -*-
"""
This file contains the Qudi mapper module.
Qudi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Qudi is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Qudi. If not, see <http://www.gnu.org/licenses/>.
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
"""
from qtpy.QtCore import QCoreApplication
from qtpy.QtCore import QThread
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QAbstractButton
from qtpy.QtWidgets import QAbstractSlider
from qtpy.QtWidgets import QComboBox
from qtpy.QtWidgets import QDoubleSpinBox
from qtpy.QtWidgets import QLineEdit
from qtpy.QtWidgets import QPlainTextEdit
from qtpy.QtWidgets import QSpinBox
from qtwidgets.scientific_spinbox import ScienDSpinBox, ScienSpinBox
import functools
SUBMIT_POLICY_AUTO = 0
"""automatically submit changes"""
SUBMIT_POLICY_MANUAL = 1
"""wait with submitting changes until submit() is called"""
class Converter:
"""
Class for converting data between display and storage (i.e. widget and
model).
"""
def widget_to_model(self, data):
"""
Converts data in the format given by the widget and converts it
to the model data format.
Parameter:
==========
data: object data to be converted
Returns:
========
out: object converted data
"""
return data
def model_to_widget(self, data):
"""
Converts data in the format given by the model and converts it
to the widget data format.
Parameter:
==========
data: object data to be converted
Returns:
========
out: object converted data
"""
return data
class Mapper:
"""
The Mapper connects a Qt widget for displaying and editing certain data
types with a model property or setter and getter functions. The model can
be e.g. a logic or a hardware module.
Usage Example:
==============
We assume to have a logic module which is connected to our GUI via a
connector and we can access it by the `logic_module` variable. We
further assume that this logic module has a string property called
`some_value` and a signal `some_value_changed` which is emitted when the
property is changed programmatically.
In the GUI module we have defined a QLineEdit, e.g. by
```
lineedit = QLineEdit()
```
In the on_activate method of the GUI module, we define the following
mapping between the line edit and the logic property:
```
def on_activate(self):
self.mapper = Mapper()
self.mapper.add_mapping(self.lineedit, self.logic_module,
'some_value', 'some_value_changed')
```
Now, if the user changes the string in the lineedit, the property of the
logic module is changed. If the logic module's property is changed
programmatically, the change is automatically displayed in the GUI.
If the GUI module is deactivated we should delete all mappings:
```
def on_deactivate(self):
self.mapper.clear_mapping()
```
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._submit_policy = SUBMIT_POLICY_AUTO
self._mappings = {}
def _get_property_from_widget(self, widget):
"""
Returns the property name we determined from the widget's type.
"""
if isinstance(widget, QAbstractButton):
return 'checked'
elif isinstance(widget, QComboBox):
return 'currentIndex'
elif isinstance(widget, QLineEdit):
return 'text'
elif isinstance(widget, (QSpinBox, QDoubleSpinBox, QAbstractSlider)):
return 'value'
elif isinstance(widget, ScienDSpinBox):
return 'value_float'
elif isinstance(widget, ScienSpinBox):
return 'value_int'
elif isinstance(widget, QPlainTextEdit):
return 'plainText'
else:
raise Exception('Property of widget {0} could not be '
'determined.'.format(repr(widget)))
def add_mapping(self,
widget,
model,
model_getter,
model_property_notifier=None,
model_setter=None,
widget_property_name='',
widget_property_notifier=None,
converter=None):
"""
Adds a mapping.
Parameters
==========
widget QtWidget A widget displaying some data. You want to map this
widget to model data
model object Instance of a class holding model data (e.g. a logic
or hardware module)
model_getter property/callable either a property holding the data to
be displayed in widget or a getter
method to retrieve data from the model
was changed.
model_property_notifier Signal A signal that is fired when the data
was changed.
Default: None. If None then data
changes are not monitored and the
widget is not updated.
model_setter callable A setter method which is called to set data to
the model.
If model_getter is a property the setter can be
determined from this property and model_setter
is ignored if it is None. If it is not None
always this callable is used.
Default: None
widget_property_name str The name of the pyqtProperty of the widget
used to map the data.
Default: ''
If it is an empty string the relevant
property is guessed from the widget's type.
widget_property_notifier Signal Notifier signal which is fired by the
widget when the data changed. If None
this is determined directly from the
property.
Example usage:
QLineEdit().editingFinished
Default: None
converter Converter converter instance for converting data between
widget display and model.
Default: None
"""
# guess widget property if not specified
if widget_property_name == '':
widget_property_name = self._get_property_from_widget(widget)
# define key of mapping
key = (widget, widget_property_name)
# check if already exists
if key in self._mappings:
raise Exception('Property {0} of widget {1} already mapped.'
''.format(widget_property_name, repr(widget)))
# check if widget property is available
index = widget.metaObject().indexOfProperty(widget_property_name)
if index == -1:
raise Exception('Property ''{0}'' of widget ''{1}'' not '
'available.'.format(widget_property_name,
widget.__class__.__name__))
meta_property = widget.metaObject().property(index)
# widget property notifier
if widget_property_notifier is None:
# check that widget property as a notify signal
if not meta_property.hasNotifySignal():
raise Exception('Property ''{0}'' of widget ''{1}'' has '
'no notify signal.'.format(
widget_property_name,
widget.__class__.__name__))
widget_property_notifier = getattr(
widget,
meta_property.notifySignal().name().data().decode('utf8'))
# check that widget property is readable
if not meta_property.isReadable():
raise Exception('Property ''{0}'' of widget ''{1}'' is not '
'readable.'.format(widget_property_name,
widget.__class__.__name__))
widget_property_getter = meta_property.read
# check that widget property is writable if requested
if not meta_property.isWritable():
raise Exception('Property ''{0}'' of widget ''{1}'' is not '
'writable.'.format(widget_property_name,
widget.__class__.__name__))
widget_property_setter = meta_property.write
if isinstance(model_getter, str):
# check if it is a property
attr = getattr(model.__class__, model_getter, None)
if attr is None:
raise Exception('Model has no attribute {0}'.format(
model_getter))
if isinstance(attr, property):
# retrieve getter from property
model_property_name = model_getter
model_getter = functools.partial(attr.fget, model)
# if no setter was specified, get it from the property
if model_setter is None:
model_setter = functools.partial(attr.fset, model)
if model_getter is None:
raise Exception('Attribute {0} of model is readonly.'
''.format(model_property_name))
else:
# getter is not a property. Check if it is a callable.
model_getter_name = model_getter
model_getter = getattr(model, model_getter)
if not callable(model_getter):
raise Exception('Attribute {0} of model is not callable.'.format(
model_getter_name))
if isinstance(model_setter, str):
model_setter_name = model_setter
model_setter = getattr(model, model_setter)
if not callable(model_setter):
raise Exception('Attribute {0} of model is not callable'.format(
model_setter_name))
if isinstance(model_property_notifier, str):
model_property_notifier = getattr(model, model_property_notifier)
# connect to widget property notifier
widget_property_notifier_slot = functools.partial(
self._on_widget_property_notification, key)
widget_property_notifier.connect(widget_property_notifier_slot)
# if model_notify_signal was specified, connect to it
model_property_notifier_slot = None
if model_property_notifier is not None:
model_property_notifier_slot = functools.partial(
self._on_model_notification, key)
model_property_notifier.connect(model_property_notifier_slot)
# save mapping
self._mappings[key] = {
'widget_property_name': widget_property_name,
'widget_property_getter': widget_property_getter,
'widget_property_setter': widget_property_setter,
'widget_property_notifier': widget_property_notifier,
'widget_property_notifier_slot': widget_property_notifier_slot,
'widget_property_notifications_disabled': False,
'model': model,
'model_property_setter': model_setter,
'model_property_getter': model_getter,
'model_property_notifier': model_property_notifier,
'model_property_notifier_slot': model_property_notifier_slot,
'model_property_notifications_disabled': False,
'converter': converter}
self._on_model_notification(key) # update widget right away
def _on_widget_property_notification(self, key, *args):
"""
Event handler for widget property change notification. Used with
functools.partial to get the widget as first parameter.
Parameters
==========
key: (QtWidget, str) The key consisting of widget and property name,
the notification signal was emitted from.
args*: List list of event parameters
"""
widget, widget_property_name = key
if self._mappings[key]['widget_property_notifications_disabled']:
return
if self._submit_policy == SUBMIT_POLICY_AUTO:
self._mappings[key][
'model_property_notifications_disabled'] = True
try:
# get value
value = self._mappings[key]['widget_property_getter'](
widget)
# convert it if requested
if self._mappings[key]['converter'] is not None:
value = self._mappings[key][
'converter'].widget_to_model(value)
# set it to model
self._mappings[key]['model_property_setter'](value)
finally:
self._mappings[key][
'model_property_notifications_disabled'] = False
else:
pass
def _on_model_notification(self, key, *args):
"""
Event handler for model data change notification. Used with
functools.partial to get the widget as first parameter.
Parameters
==========
key: (QtWidget, str) The key consisting of widget and property name
the notification signal was emitted from.
args*: List list of event parameters
"""
widget, widget_property_name = key
mapping = self._mappings[key]
# get value from model
value = self._mappings[key]['model_property_getter']()
# are updates disabled?
if self._mappings[key]['model_property_notifications_disabled']:
# but check if value has changed first
# get value from widget
value_widget = self._mappings[key]['widget_property_getter'](
widget)
# convert it if requested
if self._mappings[key]['converter'] is not None:
value_widget = self._mappings[key][
'converter'].widget_to_model(value_widget)
# accept changes, stop if nothing has changed
if value == value_widget:
return
# convert value if requested
if self._mappings[key]['converter'] is not None:
value = self._mappings[key]['converter'].model_to_widget(value)
# update widget
self._mappings[key][
'widget_property_notifications_disabled'] = True
try:
self._mappings[key]['widget_property_setter'](widget, value)
finally:
self._mappings[key][
'widget_property_notifications_disabled'] = False
def clear_mapping(self):
"""
Clears all mappings.
"""
# convert iterator to list because the _mappings dictionary will
# change its size during iteration
for key in list(self._mappings.keys()):
self.remove_mapping(key)
def remove_mapping(self, widget, widget_property_name=''):
"""
Removes the mapping which maps the QtWidget widget to some model data.
Parameters
==========
widget: QtWidget/(QtWidget, str) widget the mapping is attached to
or a tuple containing the widget and the
widget's property name
widget_property_name: str name of the property of the widget we are
dealing with. If '' it will be determined
from the widget.
Default: ''
"""
if isinstance(widget, tuple):
widget, widget_property_name = widget
# guess widget property if not specified
if widget_property_name == '':
widget_property_name = self._get_property_from_widget(widget)
# define key
key = (widget, widget_property_name)
# check that key has a mapping
if not key in self._mappings:
raise Exception('Widget {0} is not mapped.'.format(repr(widget)))
# disconnect signals
self._mappings[key]['widget_property_notifier'].disconnect(
self._mappings[key]['widget_property_notifier_slot'])
if self._mappings[key]['model_property_notifier'] is not None:
self._mappings[key]['model_property_notifier'].disconnect(
self._mappings[key]['model_property_notifier_slot'])
# remove from dictionary
del self._mappings[key]
@property
def submit_policy(self):
"""
Returns the submit policy.
"""
return self._submit_policy
@submit_policy.setter
def submit_policy(self, policy):
"""
Sets submit policy.
Submit policy can either be SUBMIT_POLICY_AUTO or
SUBMIT_POLICY_MANUAL. If the submit policy is auto then changes in
the widgets are automatically submitted to the model. If manual
call submit() to submit it.
Parameters
==========
policy: enum submit policy
"""
if policy not in [SUBMIT_POLICY_AUTO, SUBMIT_POLICY_MANUAL]:
raise Exception('Unknown submit policy ''{0}'''.format(policy))
self._submit_policy = policy
def submit(self):
"""
Submits the current values stored in the widgets to the models.
"""
# make sure it is called from main thread
if (not QThread.currentThread() == QCoreApplication.instance(
).thread()):
QTimer.singleShot(0, self.submit)
return
submit_policy = self._submit_policy
self.submit_policy = SUBMIT_POLICY_AUTO
try:
for key in self._mappings:
self._on_widget_property_notification(key)
finally:
self.submit_policy = submit_policy
def revert(self):
"""
Takes the data stored in the models and displays them in the widgets.
"""
# make sure it is called from main thread
if (not QThread.currentThread() == QCoreApplication.instance(
).thread()):
QTimer.singleShot(0, self.revert)
return
for key in self._mappings:
self._on_model_notification(key)