source: orange/docs/extend-widgets/rst/contextsettings.rst @ 11593:6edc44eb9655

Revision 11593:6edc44eb9655, 10.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Updated Widget development tutorial.

RevLine 
[11049]1##########################
2Context-Dependent Settings
3##########################
4
[11408]5You have already learned about :doc:`storing widget settings <settings>`.
[11049]6But there's more: some settings are context
7dependent. Open Orange Canvas and observe the scatter plot - feed it
8some data, select two attributes for x- and y-axis, select some
9examples... and then give it some other data. Your settings get
10lost. Or do they? Well, change back to the original data and you will
11see the same two attributes on the axes and even the same examples
12selected.
13
14What happens is that Orange remembers the settings (chosen
15attributes etc.) and ties them with the data domain. The next time it
16gets the data from the same (or similar enough) domain, the settings
17will be reused. The history of an arbitrary number of domains can be
18stored in this manner.
19
20To learn how to do it yourself, consider the widget below used for
21selecting a subset of attributes and the class attributes (note that a
22better widget for this task is already included in your Orange
[11593]23installation).
[11049]24
25.. image:: attributesampler.png
26
27The widget gets examples on the input and outputs the same examples
28with the attributes and the class chosen by the user. We'd like to
29somehow store the user's selection.
30
[11593]31Here's the widget's :func:`__init__` function.
[11049]32
[11593]33Part of :download:`OWAttributeSampler.py <OWAttributeSampler.py>`
[11049]34
[11593]35.. literalinclude:: OWAttributeSampler.py
36   :pyobject: OWAttributeSampler.__init__
[11049]37
38Note that we are strictly using controls from OWGUI. As for the
39usual settings, if you use Qt controls directly, their state won't get
40synchronized with the widget's internal variables and vice versa. The
41list box is associated with two variables: :obj:`attributeList`
42contains the attributes (as tuples with the name and the type), and
43:obj:`selectedAttributes` is a list with indices of selected
44attributes. Combo box will put the index of the chosen class attribute
45into :obj:`classAttribute`.
46
47When the widget gets the data, a function :obj:`dataset` is
[11593]48called::
[11049]49
50    def dataset(self, data):
51        self.classAttrCombo.clear()
52        if data:
53            self.attributeList = [(attr.name, attr.varType) for attr in data.domain]
54            self.selectedAttributes = []
55            for attrName, attrType in self.attributeList:
56                self.classAttrCombo.addItem(self.icons[attrType], attrName)
57            self.classAttribute = 0
58        else:
59            self.attributeList = []
60            self.selectedAttributes = []
61            self.classAttrCombo.addItem("")
62
63        self.data = data
64        self.outputData()
65
66
[11593]67.. literalinclude:: OWAttributeSampler.py
68   :pyobject: OWAttributeSampler.outputData
69
[11049]70
71Nothing special here (yet). We fill the list box, deselect all
72attributes and set the last attribute to be the class
73attribute. Output data is put into a separate function because it's
74called by :obj:`dataset` and when the user presses the "Apply"
75button.
76
77The widgets is functionally complete, but it doesn't remember
78anything. You can try to put the three variables
79(:obj:`attributeList`, :obj:`selectedAttributes` and
80:obj:`classAttribute`) in the :obj:`settingsList`, as
81you've seen on the page about settings, but it won't work. It can't:
82settings are saved and loaded only when the widget is created, not
83every time it gets a new signal. Besides, the ordinary settings in the
84:obj:`settingsList` are not context dependent, so the widget
85would usually try to assign, say, the class attribute which doesn't
86exist in the actual domain at all.
87
[11593]88To make the setting dependent on the context, we put
[11049]89
[11593]90.. literalinclude:: OWAttributeSampler.py
91   :start-after: # ~start context handler~
92   :end-before: # ~end context handler~
[11049]93
94at the same place where we usually declare :obj:`settingsList`.
95
96"Contexts" may be defined by different things, but settings most
97commonly depend on the domain of the examples. Such settings are taken
98by a context handler of type :obj:`DomainContextHandler`. We
99tell it about the fields that it should control: the first is
100:obj:`classAttribute`, and the other two form a pair,
101:obj:`attributeList` contains the attributes and
102:obj:`selectedAttributes` is the selection. The latter has the
103flag :obj:`DomainContextHandler.List` which tells the context
104handler that the property in question is a list, not an ordinary
105field.
106
107And what is ":obj:`Required`" and
108":obj:`SelectedRequired`"? These are important in domain
109matching. Say that you loaded the car data, selected attributes
110:obj:`price`, :obj:`maint` and :obj:`lug_boot` and
111set the class attribute to :obj:`acc`. Now you load a modified
112car data in which the attribute :obj:`doors` is missing. Can the
113settings be reused? Sure, :obj:`doors` was not selected, so this
114attribute is not really needed. The new domain is thus not exactly the
115same as the one with which the context was saved, but nothing
116essential is missing so the context is loaded.
117
118A different thing is if the new set misses attributes
119:obj:`price` or :obj:`acc`; in this case, the old settings
120cannot and should not be reused. So, this is the meaning of
121:obj:`DomainContextHandler.Required` and
122:obj:`DomainContextHandler.SelectedRequired`: a stored context
123doesn't match the new data if the data lacks the attribute that the
124context stores as ":obj:`classAttribute`". And, the new data
125also has to have all the attributes that were selected in the stored
126context. If any of the other attributes misses, it doesn't matter, the
127context will still match and be used.
128
129As you have guessed, we can also have optional attributes
130(:obj:`DomainContextHandler.Optional`); sometimes certain
131attribute doesn't really matter, so if it is present in the domain,
[11593]132it's going to be used, otherwise not. And for the list, we could say
[11049]133:obj:`DomainContextHandler.List + DomainContextHandler.Required`
134in which case all the attributes on the list would be required for the
135domain to match.
136
[11593]137The default flag is :obj:`DomainContextHandler.Required`, and there
138are other shortcuts for declaring the context, too. The above code could
139be simplified as ::
[11049]140
[11593]141    contextHandlers = {
142        "": DomainContextHandler(
143            "",
144            ["classAttribute",
145             ContextField("attributeList",
146                          DomainContextHandler.SelectedRequiredList,
147                          selected="selectedAttributes")])}
[11049]148
149Why the dictionary and the empty string as the key? A widget can
150have multiple contexts, depending, usually, on multiple input
151signals. These contexts can be named, but the default name is empty
152string. A case in which we would really need multiple contexts has yet
153to appear, so you shall mostly declare the contexts as above. (Note
154that we gave the name twice - the first empty string is for the key in
155the dictionary and with the second we tell the context handler its own
156name.)
157
158So much for declaration of contexts. The ordinary, context
159independent settings load and save automatically as the widget is
160created and destroyed. Context dependent settings are stored and
161restored when the context changes, usually due to receiving a signal
162with a new data set. This unfortunately cannot be handled
163automatically - you have to add the calls of the appropriate context
164changing functions yourself. Here's what you have to do with the
165function :obj:`dataset`
166
[11593]167.. literalinclude:: OWAttributeSampler.py
168   :pyobject: OWAttributeSampler.dataset
[11049]169
[11593]170We added only two lines. First, before you change any controls in
171the widget, you need to call :obj:`self.closeContext` (the function
172has an optional argument, the context name, but since we use the
173default name, an empty string, we can omit it). This reads the
174data from the widget into the stored context. Then the function
175proceeds as before: the controls (the list box and combo box)
176are filled in as if there were no context handling (this is
177important, so once again: widget should be set up as if there
178were not context dependent settings). When the controls are put
179in a consistent state, we call :obj:`self.openContext`. The first
180argument is the context name and the second is the object from
181which the handler reads the context. In case of
182:obj:`DomainContextHandler` this can be either a domain or the
183data. :obj:`openContext` will make the context handler search
184through the stored context for the one that (best) matches the
185data, and if one is find the widget's state is set accordingly
186(that is, the list boxes are filled, attributes in it are selected
187etc.). If no context is found, a new context is established and the
188data from widget is copied to the context.
[11049]189
190What can be stored as a context dependent setting? Anything, even
191the state of check boxes if you want to. But don't do that. Make
192some of your checkboxes context dependent (so that they will
193change when the new data arrives) and the use of the widget will be
194completely chaotic since nobody will know what changes and what stays
195the same. Make all your controls context dependent and the
196widget will become useless as it will reset to the defaults every time
197some new data arrives. Bottom line, regarding to controls, make as
198little context dependent settings as possible - the context dependent
199controls will usually be limited to list boxes and combo boxes that
200store attribute names.
201
202But there are other things that you can put into the context. Just
203remember the scatter plot's ability to remember the example selection
204- which is surely not stored in a simple list box. How does it do it?
205Here are two methods it defines::
206
207    def settingsFromWidgetCallback(self, handler, context):
208        context.selectionPolygons = []
209        for key in self.graph.selectionCurveKeyList:
210            curve = self.graph.curve(key)
211            xs = [curve.x(i) for i in range(curve.dataSize())]
212            ys = [curve.y(i) for i in range(curve.dataSize())]
213            context.selectionPolygons.append((xs, ys))
214
215    def settingsToWidgetCallback(self, handler, context):
216        selections = context.selectionPolygons
217        for (xs, ys) in selections:
218            c = SelectionCurve(self.graph)
219            c.setData(xs,ys)
220            key = self.graph.insertCurve(c)
221            self.graph.selectionCurveKeyList.append(key)
222
223:obj:`settingsFromWidgetCallback` is called by the context
224handler to copy the settings from the widget to the context, and
225:obj:`settingsToWidgetCallback` writes the settings back to the
226widget. Their arguments, besides :obj:`self`, are the context
227handler and the context. Whatever
228:obj:`settingsFromWidgetCallback` stores into the
229:obj:`context`, stays there, gets saved when the canvas is
230closed and loaded when it's opened
231again. :obj:`setttingsToWidgetCallback` can read these fields
232and restore the widget's state (the example selection, in this case)
233accordingly.
234
235:obj:`selectionPolygons` is not registered by the context
236handler the way we registered :obj:`attributeList`,
237:obj:`selectedAttributes` and :obj:`classAttribute` above,
238since the context handler doesn't need to know and care about
239:obj:`selectionPolygons`.
240
241When writing such callback functions make sure that the data you
242store is picklable and short enough, so you won't blow up the .ini
243files that store these settings.
Note: See TracBrowser for help on using the repository browser.