File: link_equation.py

package info (click to toggle)
glueviz 0.9.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 17,180 kB
  • ctags: 6,728
  • sloc: python: 37,111; makefile: 134; sh: 60
file content (307 lines) | stat: -rw-r--r-- 10,331 bytes parent folder | download
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
from __future__ import absolute_import, division, print_function

import os
from inspect import getargspec

from qtpy import QtWidgets
from qtpy import PYSIDE
from glue import core
from glue.config import link_function, link_helper
from glue.utils import nonpartial
from glue.utils.qt import load_ui, messagebox_on_error, update_combobox
from glue.utils.qt.widget_properties import CurrentComboTextProperty, CurrentComboDataProperty

__all__ = ['LinkEquation']


def get_function_name(item):
    if hasattr(item, 'display') and item.display is not None:
        return item.display
    else:
        return item.__name__


def function_label(function):
    """ Provide a label for a function

    :param function: A member from the glue.config.link_function registry
    """
    args = getargspec(function.function)[0]
    args = ', '.join(args)
    output = function.output_labels
    output = ', '.join(output)
    label = "Link from %s to %s" % (args, output)
    return label


def helper_label(helper):
    """ Provide a label for a link helper

    :param helper: A member from the glue.config.link_helper registry
    """
    return helper.info


class ArgumentWidget(QtWidgets.QWidget):

    def __init__(self, argument, parent=None):
        super(ArgumentWidget, self).__init__(parent)
        self.layout = QtWidgets.QHBoxLayout()
        self.layout.setContentsMargins(1, 0, 1, 1)
        self.setLayout(self.layout)
        label = QtWidgets.QLabel(argument)
        self._label = label
        self._component_id = None
        self.layout.addWidget(label)
        self.editor = QtWidgets.QLineEdit()
        self.editor.setReadOnly(True)
        try:
            self.editor.setPlaceholderText("Drag a component from above")
        except AttributeError:  # feature added in Qt 4.7
            pass
        self.layout.addWidget(self.editor)
        self.setAcceptDrops(True)

    @property
    def component_id(self):
        return self._component_id

    @component_id.setter
    def component_id(self, cid):
        self._component_id = cid
        self.editor.setText(str(cid))

    @property
    def label(self):
        return self._label.text()

    @label.setter
    def label(self, label):
        self._label.setText(label)

    @property
    def editor_text(self):
        return self.editor.text()

    def clear(self):
        self.component_id = None
        self.editor.clear()

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('application/py_instance'):
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        obj = event.mimeData().data('application/py_instance')
        if isinstance(obj, list):
            obj = obj[0]
        if not isinstance(obj, core.data.ComponentID):
            event.ignore()
            return
        self.component_id = obj
        event.accept()


class LinkEquation(QtWidgets.QWidget):

    """ Interactively define ComponentLinks from existing functions

    This widget inspects the calling signatures of helper functions,
    and presents the user with an interface for assigning componentIDs
    to the input and output arguments. It also generates ComponentLinks
    from this information.

    ComponentIDs are assigned to arguments via drag and drop. This
    widget is used within the LinkEditor dialog

    Usage::

       widget = LinkEquation()
    """

    category = CurrentComboTextProperty('_ui.category')
    function = CurrentComboDataProperty('_ui.function')

    def __init__(self, parent=None):
        super(LinkEquation, self).__init__(parent)

        # Set up mapping of function/helper name -> function/helper tuple. For the helpers, we use the 'display' name if available.
        self._argument_widgets = []
        self.spacer = None
        self._output_widget = ArgumentWidget("")

        # pyqt4 can't take self as second argument here
        # for some reason. Manually embed
        self._ui = load_ui('link_equation.ui', None,
                           directory=os.path.dirname(__file__))
        l = QtWidgets.QHBoxLayout()
        l.addWidget(self._ui)
        self.setLayout(l)

        self._init_widgets()
        self._populate_category_combo()
        self.category = 'General'
        self._populate_function_combo()
        self._connect()
        self._setup_editor()

    def set_result_visible(self, state):
        self._ui.output_canvas.setVisible(state)
        self._ui.output_label.setVisible(state)

    def is_helper(self):
        return self.function is not None and \
            type(self.function).__name__ == 'LinkHelper'

    def is_function(self):
        return self.function is not None and \
            type(self.function).__name__ == 'LinkFunction'

    def _init_widgets(self):
        layout = QtWidgets.QVBoxLayout()
        layout.setSpacing(1)
        self._ui.input_canvas.setLayout(layout)
        layout = QtWidgets.QVBoxLayout()
        layout.setContentsMargins(1, 0, 1, 1)
        self._ui.output_canvas.setLayout(layout)
        layout.addWidget(self._output_widget)
        spacer = QtWidgets.QSpacerItem(5, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        layout.addItem(spacer)

    @property
    def add_button(self):
        return self._ui.addButton

    @property
    def signature(self):
        """ Returns the ComponentIDs assigned to the input and output arguments

        :rtype: tuple of (input, output). Input is a list of ComponentIDs.
                output is a ComponentID
        """
        inp = [a.component_id for a in self._argument_widgets]
        out = self._output_widget.component_id
        return inp, out

    @signature.setter
    def signature(self, inout):
        inp, out = inout
        for i, a in zip(inp, self._argument_widgets):
            a.component_id = i
        self._output_widget.component_id = out

    @messagebox_on_error("Failed to create links")
    def links(self):
        """ Create ComponentLinks from the state of the widget

        :rtype: list of ComponentLinks that can be created.

        If no links can be created (e.g. because of missing input),
        the empty list is returned
        """
        inp, out = self.signature
        if self.is_function():
            using = self.function.function
            if not all(inp) or not out:
                return []
            link = core.component_link.ComponentLink(inp, out, using)
            return [link]
        if self.is_helper():
            helper = self.function.helper
            if not all(inp):
                return []
            return helper(*inp)

    def _update_add_enabled(self):
        state = True
        for a in self._argument_widgets:
            state = state and a.component_id is not None
        if self.is_function():
            state = state and self._output_widget.component_id is not None
        self._ui.addButton.setEnabled(state)

    def _connect(self):
        signal = self._ui.function.currentIndexChanged
        signal.connect(nonpartial(self._setup_editor))
        signal.connect(nonpartial(self._update_add_enabled))
        self._output_widget.editor.textChanged.connect(nonpartial(self._update_add_enabled))
        self._ui.category.currentIndexChanged.connect(self._populate_function_combo)

    def clear_inputs(self):
        for w in self._argument_widgets:
            w.clear()
        self._output_widget.clear()

    def _setup_editor(self):
        if self.is_function():
            self._setup_editor_function()
        else:
            self._setup_editor_helper()

    def _setup_editor_function(self):
        """ Prepare the widget for the active function."""
        assert self.is_function()
        self.set_result_visible(True)
        func = self.function.function
        args = getargspec(func)[0]
        label = function_label(self.function)
        self._ui.info.setText(label)
        self._output_widget.label = self.function.output_labels[0]
        self._clear_input_canvas()
        for a in args:
            self._add_argument_widget(a)

        self.spacer = QtWidgets.QSpacerItem(5, 5, QtWidgets.QSizePolicy.Minimum,
                                        QtWidgets.QSizePolicy.Expanding)
        self._ui.input_canvas.layout().addItem(self.spacer)

    def _setup_editor_helper(self):
        """Setup the editor for the selected link helper"""
        assert self.is_helper()
        self.set_result_visible(False)
        label = helper_label(self.function)
        args = self.function.input_labels
        self._ui.info.setText(label)

        self._clear_input_canvas()
        for a in args:
            self._add_argument_widget(a)

        self.spacer = QtWidgets.QSpacerItem(5, 5, QtWidgets.QSizePolicy.Minimum,
                                        QtWidgets.QSizePolicy.Expanding)
        self._ui.input_canvas.layout().addItem(self.spacer)

    def _add_argument_widget(self, argument):
        """ Create and add a single argument widget to the input canvas
        :param arguement: The argument name (string)
        """
        widget = ArgumentWidget(argument)
        widget.editor.textChanged.connect(nonpartial(self._update_add_enabled))
        self._ui.input_canvas.layout().addWidget(widget)
        self._argument_widgets.append(widget)

    def _clear_input_canvas(self):
        """ Remove all widgets from the input canvas """
        layout = self._ui.input_canvas.layout()
        for a in self._argument_widgets:
            layout.removeWidget(a)
            a.close()

        if not PYSIDE:
            # PySide crashing here
            layout.removeItem(self.spacer)

        self._argument_widgets = []

    def _populate_category_combo(self):
        f = [f for f in link_function.members if len(f.output_labels) == 1]
        categories = sorted(set(l.category for l in f + link_helper.members))
        update_combobox(self._ui.category, list(zip(categories, categories)))

    def _populate_function_combo(self):
        """ Add name of functions to function combo box """
        f = [f for f in link_function.members if len(f.output_labels) == 1]
        functions = ((get_function_name(l[0]), l) for l in f + link_helper.members if l.category == self.category)
        update_combobox(self._ui.function, functions)