Completed
Push — pulsed_with_queued_connections ( 8830a7...cddfb2 )
by
unknown
03:09
created

Mapper.revert()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 12
rs 9.4285
c 2
b 0
f 0
1
# -*- coding: utf-8 -*-
2
"""
3
This file contains the Qudi mapper module.
4
5
Qudi is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
Qudi is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
GNU General Public License for more details.
14
15
You should have received a copy of the GNU General Public License
16
along with Qudi. If not, see <http://www.gnu.org/licenses/>.
17
18
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
19
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
20
"""
21
22
23
from qtpy.QtCore import QCoreApplication
24
from qtpy.QtCore import QObject
25
from qtpy.QtCore import QThread
26
from qtpy.QtCore import QTimer
27
from qtpy.QtWidgets import QAbstractButton
28
from qtpy.QtWidgets import QAbstractSlider
29
from qtpy.QtWidgets import QComboBox
30
from qtpy.QtWidgets import QDoubleSpinBox
31
from qtpy.QtWidgets import QLineEdit
32
from qtpy.QtWidgets import QPlainTextEdit
33
from qtpy.QtWidgets import QSpinBox
34
35
import functools
36
37
SUBMIT_POLICY_AUTO = 0
38
"""automatically submit changes"""
39
SUBMIT_POLICY_MANUAL = 1
40
"""wait with submitting changes until submit() is called"""
41
42
43
class Converter():
44
    """
45
    Class for converting data between display and storage (i.e. widget and
46
    model).
47
    """
48
    def widget_to_model(self, data):
49
        """
50
        Converts data in the format given by the widget and converts it
51
        to the model data format.
52
53
        Parameter:
54
        ==========
55
        data: object data to be converted
56
57
        Returns:
58
        ========
59
        out: object converted data
60
        """
61
        return data
62
63
    def model_to_widget(self, data):
64
        """
65
        Converts data in the format given by the model and converts it
66
        to the widget data format.
67
68
        Parameter:
69
        ==========
70
        data: object data to be converted
71
72
        Returns:
73
        ========
74
        out: object converted data
75
        """
76
        return data
77
78
79
class Mapper():
80
    """
81
    The Mapper connects a Qt widget for displaying and editing certain data
82
    types with a model property or setter and getter functions. The model can
83
    be e.g. a logic or a hardware module.
84
85
    Usage Example:
86
    ==============
87
88
    We assume to have a logic module which is connected to our GUI via a
89
    connector and we can access it by the `logic_module` variable. We
90
    further assume that this logic module has a string property called
91
    `some_value` and a signal `some_value_changed` which is emitted when the
92
    property is changed programmatically.
93
    In the GUI module we have defined a QLineEdit, e.g. by
94
    ```
95
    lineedit = QLineEdit()
96
    ```
97
    In the on_activate method of the GUI module, we define the following
98
    mapping between the line edit and the logic property:
99
    ```
100
    def on_activate(self, e):
101
        self.mapper = Mapper()
102
        self.mapper.add_mapping(self.lineedit, self.logic_module,
103
                'some_value', 'some_value_changed')
104
    ```
105
    Now, if the user changes the string in the lineedit, the property of the
106
    logic module is changed. If the logic module's property is changed
107
    programmatically, the change is automatically displayed in the GUI.
108
109
    If the GUI module is deactivated we should delete all mappings:
110
    ```
111
    def on_deactivate(self, e):
112
        self.mapper.clear_mapping()
113
    ```
114
    """
115
116
    def __init__(self, **kwargs):
117
        super().__init__(**kwargs)
118
        self._submit_policy = SUBMIT_POLICY_AUTO
119
        self._mappings = {}
120
121
    def _get_property_from_widget(self, widget):
122
        """
123
        Returns the property name we determined from the widget's type.
124
        """
125
        if isinstance(widget, QAbstractButton):
126
            return 'checked'
127
        elif isinstance(widget, QComboBox):
128
            return 'currentIndex'
129
        elif isinstance(widget, QLineEdit):
130
            return 'text'
131
        elif (isinstance(widget, (QSpinBox, QDoubleSpinBox, QAbstractSlider)):
132
            return 'value'
133
        elif isinstance(widget, QPlainTextEdit):
134
            return 'plainText'
135
        else:
136
            raise Exception('Property of widget {0} could not be '
137
                            'determined.'.format(repr(widget)))
138
139
    def add_mapping(self,
140
                    widget,
141
                    model,
142
                    model_getter,
143
                    model_property_notifier=None,
144
                    model_setter=None,
145
                    widget_property_name='',
146
                    widget_property_notifier=None,
147
                    converter=None):
148
        """
149
        Adds a mapping.
150
151
        Parameters
152
        ==========
153
        widget QtWidget A widget displaying some data. You want to map this
154
                        widget to model data
155
        model  object   Instance of a class holding model data (e.g. a logic
156
                        or hardware module)
157
        model_getter property/callable either a property holding the data to
158
                                       be displayed in widget or a getter
159
                                       method to retrieve data from the model
160
                                       was changed.
161
        model_property_notifier Signal A signal that is fired when the data
162
                                       was changed.
163
                                       Default: None. If None then data
164
                                       changes are not monitored and the
165
                                       widget is not updated.
166
        model_setter callable A setter method which is called to set data to
167
                              the model.
168
                              If model_getter is a property the setter can be
169
                              determined from this property and model_setter
170
                              is ignored if it is None. If it is not None
171
                              always this callable is used.
172
                              Default: None
173
        widget_property_name str The name of the pyqtProperty of the widget
174
                                 used to map the data.
175
                                 Default: ''
176
                                 If it is an empty string the relevant
177
                                 property is guessed from the widget's type.
178
        widget_property_notifier Signal Notifier signal which is fired by the
179
                                        widget when the data changed. If None
180
                                        this is determined directly from the
181
                                        property.
182
                                        Example usage:
183
                                        QLineEdit().editingFinished
184
                                        Default: None
185
        converter Converter converter instance for converting data between
186
                            widget display and model.
187
                            Default: None
188
189
        """
190
        # guess widget property if not specified
191
        if widget_property_name == '':
192
            widget_property_name = self._get_property_from_widget(widget)
193
194
        # define key of mapping
195
        key = (widget, widget_property_name)
196
197
        # check if already exists
198
        if key in self._mappings:
199
            raise Exception('Property {0} of widget {1} already mapped.'
200
                            ''.format(widget_property_name, repr(widget)))
201
202
        # check if widget property is available
203
        index = widget.metaObject().indexOfProperty(widget_property_name)
204
        if index == -1:
205
            raise Exception('Property ''{0}'' of widget ''{1}'' not '
206
                            'available.'.format(widget_property_name,
207
                                                widget.__class__.__name__))
208
209
        meta_property = widget.metaObject().property(index)
210
211
        # widget property notifier
212
        if widget_property_notifier is None:
213
            # check that widget property as a notify signal
214
            if not meta_property.hasNotifySignal():
215
                raise Exception('Property ''{0}'' of widget ''{1}'' has '
216
                                'no notify signal.'.format(
217
                                    widget_property_name,
218
                                    widget.__class__.__name__))
219
            widget_property_notifier = getattr(
220
                widget,
221
                meta_property.notifySignal().name().data().decode('utf8'))
222
223
        # check that widget property is readable
224
        if not meta_property.isReadable():
225
            raise Exception('Property ''{0}'' of widget ''{1}'' is not '
226
                            'readable.'.format(widget_property_name,
227
                                               widget.__class__.__name__))
228
        widget_property_getter = meta_property.read
229
        # check that widget property is writable if requested
230
        if not meta_property.isWritable():
231
            raise Exception('Property ''{0}'' of widget ''{1}'' is not '
232
                            'writable.'.format(widget_property_name,
233
                                               widget.__class__.__name__))
234
        widget_property_setter = meta_property.write
235
236
        if isinstance(model_getter, str):
237
            # check if it is a property
238
            attr = getattr(model.__class__, model_getter, None)
239
            if attr is None:
240
                raise Exception('Model has no attribute {0}'.format(
241
                    model_getter))
242
            if isinstance(attr, property):
243
                # retrieve getter from property
244
                model_property_name = model_getter
245
                model_getter = functools.partial(attr.fget, model)
246
                # if no setter was specified, get it from the property
247
                if model_setter is None:
248
                    model_setter = functools.partial(attr.fset, model)
249
                    if model_getter is None:
250
                        raise Exception('Attribute {0} of model is readonly.'
251
                                        ''.format(model_property_name))
252
        if isinstance(model_setter, str):
253
            model_setter_name = model_setter
254
            model_setter = getattr(model, model_setter)
255
            if not callable(model_setter):
256
                raise Exception('{0} is not callable'.format(
257
                    model_setter_name))
258
        if isinstance(model_property_notifier, str):
259
            model_property_notifier = getattr(model, model_property_notifier)
260
261
        # connect to widget property notifier
262
        widget_property_notifier_slot = functools.partial(
263
            self._on_widget_property_notification, key)
264
        widget_property_notifier.connect(widget_property_notifier_slot)
265
266
        # if model_notify_signal was specified, connect to it
267
        model_property_notifier_slot = None
268
        if model_property_notifier is not None:
269
            model_property_notifier_slot = functools.partial(
270
                self._on_model_notification, key)
271
            model_property_notifier.connect(model_property_notifier_slot)
272
        # save mapping
273
        self._mappings[key] = {
274
            'widget_property_name': widget_property_name,
275
            'widget_property_getter': widget_property_getter,
276
            'widget_property_setter': widget_property_setter,
277
            'widget_property_notifier': widget_property_notifier,
278
            'widget_property_notifier_slot': widget_property_notifier_slot,
279
            'widget_property_notifications_disabled': False,
280
            'model': model,
281
            'model_property_setter': model_setter,
282
            'model_property_getter': model_getter,
283
            'model_property_notifier': model_property_notifier,
284
            'model_property_notifier_slot': model_property_notifier_slot,
285
            'model_property_notifications_disabled': False,
286
            'converter': converter}
287
288
    def _on_widget_property_notification(self, key, *args):
289
        """
290
        Event handler for widget property change notification. Used with
291
        functools.partial to get the widget as first parameter.
292
293
        Parameters
294
        ==========
295
        key: (QtWidget, str) The key consisting of widget and property name,
296
                             the notification signal was emitted from.
297
        args*: List list of event parameters
298
        """
299
        widget, widget_property_name = key
300
        if self._mappings[key]['widget_property_notifications_disabled']:
301
            return
302
        if self._submit_policy == SUBMIT_POLICY_AUTO:
303
            self._mappings[key][
304
                'model_property_notifications_disabled'] = True
305
            try:
306
                # get value
307
                value = self._mappings[key]['widget_property_getter'](
308
                    widget)
309
                # convert it if requested
310
                if self._mappings[key]['converter'] is not None:
311
                    value = self._mappings[key][
312
                        'converter'].widget_to_model(value)
313
                # set it to model
314
                self._mappings[key]['model_property_setter'](value)
315
            finally:
316
                self._mappings[key][
317
                    'model_property_notifications_disabled'] = False
318
        else:
319
            pass
320
321
    def _on_model_notification(self, key, *args):
322
        """
323
        Event handler for model data change notification. Used with
324
        functools.partial to get the widget as first parameter.
325
326
        Parameters
327
        ==========
328
        key: (QtWidget, str) The key consisting of widget and property name
329
                             the notification signal was emitted from.
330
        args*: List list of event parameters
331
        """
332
        widget, widget_property_name = key
333
        mapping = self._mappings[key]
334
        # get value from model
335
        value = self._mappings[key]['model_property_getter']()
336
337
        # are updates disabled?
338
        if self._mappings[key]['model_property_notifications_disabled']:
339
            # but check if value has changed first
340
            # get value from widget
341
            value_widget = self._mappings[key]['widget_property_getter'](
342
                widget)
343
            # convert it if requested
344
            if self._mappings[key]['converter'] is not None:
345
                value_widget = self._mappings[key][
346
                    'converter'].widget_to_model(value_widget)
347
            # accept changes, stop if nothing has changed
348
            if (value == value_widget):
349
                return
350
351
        # convert value if requested
352
        if self._mappings[key]['converter'] is not None:
353
            value = self._mappings[key]['converter'].model_to_widget(value)
354
355
        # update widget
356
        self._mappings[key][
357
            'widget_property_notifications_disabled'] = True
358
        try:
359
            self._mappings[key]['widget_property_setter'](widget, value)
360
        finally:
361
            self._mappings[key][
362
                'widget_property_notifications_disabled'] = False
363
364
    def clear_mapping(self):
365
        """
366
        Clears all mappings.
367
        """
368
        # convert iterator to list because the _mappings dictionary will
369
        # change its size during iteration
370
        for key in list(self._mappings.keys()):
371
            self.remove_mapping(key)
372
373
    def remove_mapping(self, widget, widget_property_name=''):
374
        """
375
        Removes the mapping which maps the QtWidget widget to some model data.
376
377
        Parameters
378
        ==========
379
        widget: QtWidget/(QtWidget, str) widget the mapping is attached to
380
                                  or a tuple containing the widget and the
381
                                  widget's property name
382
        widget_property_name: str name of the property of the widget we are
383
                                  dealing with. If '' it will be determined
384
                                  from the widget.
385
                                  Default: ''
386
        """
387
        if isinstance(widget, tuple):
388
            widget, widget_property_name = widget
389
        # guess widget property if not specified
390
        if widget_property_name == '':
391
            widget_property_name = self._get_property_from_widget(widget)
392
        # define key
393
        key = (widget, widget_property_name)
394
        # check that key has a mapping
395
        if not key in self._mappings:
396
            raise Exception('Widget {0} is not mapped.'.format(repr(widget)))
397
        # disconnect signals
398
        self._mappings[key]['widget_property_notifier'].disconnect(
399
            self._mappings[key]['widget_property_notifier_slot'])
400
        if self._mappings[key]['model_property_notifier'] is not None:
401
            self._mappings[key]['model_property_notifier'].disconnect(
402
                self._mappings[key]['model_property_notifier_slot'])
403
        # remove from dictionary
404
        del self._mappings[key]
405
406
    @property
407
    def submit_policy(self):
408
        """
409
        Returns the submit policy.
410
        """
411
        return self._submit_policy
412
413
    @submit_policy.setter
414
    def submit_policy(self, policy):
415
        """
416
        Sets submit policy.
417
418
        Submit policy can either be SUBMIT_POLICY_AUTO or
419
        SUBMIT_POLICY_MANUAL. If the submit policy is auto then changes in
420
        the widgets are automatically submitted to the model. If manual
421
        call submit() to submit it.
422
423
        Parameters
424
        ==========
425
        policy: enum submit policy
426
        """
427
        if policy not in [SUBMIT_POLICY_AUTO, SUBMIT_POLICY_MANUAL]:
428
            raise Exception('Unknown submit policy ''{0}'''.format(policy))
429
        self._submit_policy = policy
430
431
    def submit(self):
432
        """
433
        Submits the current values stored in the widgets to the models.
434
        """
435
        # make sure it is called from main thread
436
        if (not QThread.currentThread() == QCoreApplication.instance(
437
        ).thread()):
438
            QTimer.singleShot(0, self.submit)
439
            return
440
441
        submit_policy = self._submit_policy
442
        self.submit_policy = SUBMIT_POLICY_AUTO
443
        try:
444
            for key in self._mappings:
445
                self._on_widget_property_notification(key)
446
        finally:
447
            self.submit_policy = submit_policy
448
449
    def revert(self):
450
        """
451
        Takes the data stored in the models and displays them in the widgets.
452
        """
453
        # make sure it is called from main thread
454
        if (not QThread.currentThread() == QCoreApplication.instance(
455
        ).thread()):
456
            QTimer.singleShot(0, self.revert)
457
            return
458
459
        for key in self._mappings:
460
            self._on_model_notification(key)
461