source: orange/docs/extend-widgets/rst/contextsettings.rst @ 11049:f4dd8dbc57bb

Revision 11049:f4dd8dbc57bb, 13.0 KB checked in by Miha Stajdohar <miha.stajdohar@…>, 16 months ago (diff)

From HTML to Sphinx.

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