source: orange/docs/extend-widgets/rst/contextsettings.rst @ 11408:c2d2400b6a90

Revision 11408:c2d2400b6a90, 12.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixes for Widgets Development documentation.

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
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
[11408]33Part of :download:`OWAttributeSampler.py <OWAttributeSampler.py>`::
[11049]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
[11408]68Part of :download:`OWAttributeSampler.py`::
[11049]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
170Why the dictionary and the empty string as the key? A widget can
171have multiple contexts, depending, usually, on multiple input
172signals. These contexts can be named, but the default name is empty
173string. A case in which we would really need multiple contexts has yet
174to appear, so you shall mostly declare the contexts as above. (Note
175that we gave the name twice - the first empty string is for the key in
176the dictionary and with the second we tell the context handler its own
177name.)
178
179So much for declaration of contexts. The ordinary, context
180independent settings load and save automatically as the widget is
181created and destroyed. Context dependent settings are stored and
182restored when the context changes, usually due to receiving a signal
183with a new data set. This unfortunately cannot be handled
184automatically - you have to add the calls of the appropriate context
185changing functions yourself. Here's what you have to do with the
186function :obj:`dataset`
187
[11408]188Part of :download:`OWAttributeSampler.py`::
[11049]189
190    def dataset(self, data):
191        self.closeContext()
192   
193        self.classAttrCombo.clear()
194        if data:
195            self.attributeList = [(attr.name, attr.varType) for attr in data.domain]
196            self.selectedAttributes = []
197            for attrName, attrType in self.attributeList:
198                self.classAttrCombo.addItem(self.icons[attrType], attrName)
199            self.classAttribute = 0
200        else:
201            self.attributeList = []
202            self.selectedAttributes = []
203            self.classAttrCombo.addItem("")
204   
205        self.openContext("", data)
206   
207        self.data = data
208        self.outputData()
209
210We 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.
211
212What can be stored as a context dependent setting? Anything, even
213the state of check boxes if you want to. But don't do that. Make
214some of your checkboxes context dependent (so that they will
215change when the new data arrives) and the use of the widget will be
216completely chaotic since nobody will know what changes and what stays
217the same. Make all your controls context dependent and the
218widget will become useless as it will reset to the defaults every time
219some new data arrives. Bottom line, regarding to controls, make as
220little context dependent settings as possible - the context dependent
221controls will usually be limited to list boxes and combo boxes that
222store attribute names.
223
224But there are other things that you can put into the context. Just
225remember the scatter plot's ability to remember the example selection
226- which is surely not stored in a simple list box. How does it do it?
227Here are two methods it defines::
228
229    def settingsFromWidgetCallback(self, handler, context):
230        context.selectionPolygons = []
231        for key in self.graph.selectionCurveKeyList:
232            curve = self.graph.curve(key)
233            xs = [curve.x(i) for i in range(curve.dataSize())]
234            ys = [curve.y(i) for i in range(curve.dataSize())]
235            context.selectionPolygons.append((xs, ys))
236
237    def settingsToWidgetCallback(self, handler, context):
238        selections = context.selectionPolygons
239        for (xs, ys) in selections:
240            c = SelectionCurve(self.graph)
241            c.setData(xs,ys)
242            key = self.graph.insertCurve(c)
243            self.graph.selectionCurveKeyList.append(key)
244
245:obj:`settingsFromWidgetCallback` is called by the context
246handler to copy the settings from the widget to the context, and
247:obj:`settingsToWidgetCallback` writes the settings back to the
248widget. Their arguments, besides :obj:`self`, are the context
249handler and the context. Whatever
250:obj:`settingsFromWidgetCallback` stores into the
251:obj:`context`, stays there, gets saved when the canvas is
252closed and loaded when it's opened
253again. :obj:`setttingsToWidgetCallback` can read these fields
254and restore the widget's state (the example selection, in this case)
255accordingly.
256
257:obj:`selectionPolygons` is not registered by the context
258handler the way we registered :obj:`attributeList`,
259:obj:`selectedAttributes` and :obj:`classAttribute` above,
260since the context handler doesn't need to know and care about
261:obj:`selectionPolygons`.
262
263When writing such callback functions make sure that the data you
264store is picklable and short enough, so you won't blow up the .ini
265files that store these settings.
Note: See TracBrowser for help on using the repository browser.