source: orange/Orange/OrangeWidgets/Data/OWEditDomain.py @ 10046:207f641d0485

Revision 10046:207f641d0485, 21.5 KB checked in by markotoplak, 2 years ago (diff)

Shortened hoerarchical clustering documentation. Some data.variable -> feature.

Line 
1"""
2<name>Edit Domain</name>
3<description>Edit domain variables</description>
4<icon>icons/EditDomain.png</icon>
5<contact>Ales Erjavec (ales.erjavec(@ at @)fri.uni-lj.si)</contact>
6<priority>3125</priority>
7<keywords>change,name,variable,domain</keywords>
8"""
9
10from OWWidget import *
11from OWItemModels import VariableListModel, PyListModel
12
13import OWGUI
14
15import Orange
16
17def is_discrete(var):
18    return isinstance(var, Orange.feature.Discrete)
19
20def is_continuous(var):
21    return isinstance(var, Orange.feature.Continuous)
22
23def get_qualified(module, name):
24    """ Return a qualified module member ``name`` inside the named
25    ``module``.
26   
27    The module (or package) firts gets imported and the name
28    is retrieved from the module's global namespace.
29     
30    """
31    module = __import__(module)
32    return getattr(module, name)
33
34def variable_description(var):
35    """ Return a variable descriptor.
36   
37    A descriptor is a hashable tuple which should uniquely define
38    the variable i.e. (module, type_name, variable_name,
39    any_kwargs, attributes-labels).
40   
41    """
42    var_type = type(var)
43    if is_discrete(var):
44        return (var_type.__module__,
45                var_type.__name__,
46                var.name, 
47                (("values", tuple(var.values)),), 
48                tuple(sorted(var.attributes.items())))
49    else:
50        return (var_type.__module__,
51                var_type.__name__,
52                var.name, 
53                (), 
54                tuple(sorted(var.attributes.items())))
55
56def variable_from_description(description):
57    """ Construct a variable from its description
58    (:ref:`variable_description`).
59   
60    """
61    module, type_name, name, kwargs, attrs = description
62    type = get_qualified(module, type_name)
63    var = type(name, **dict(list(kwargs)))
64    var.attributes.update(attrs)
65    return var
66   
67from PyQt4 import QtCore, QtGui
68
69QtCore.Slot = QtCore.pyqtSlot
70QtCore.Signal = QtCore.pyqtSignal
71
72class PyStandardItem(QStandardItem):
73    def __lt__(self, other):
74        return id(self) < id(other)
75   
76class DictItemsModel(QStandardItemModel):
77    """ A Qt Item Model class displaying the contents of a python
78    dictionary.
79   
80    """
81    # Implement a proper model with in-place editing.
82    # (Maybe it should be a TableModel with 2 columns)
83    def __init__(self, parent=None, dict={}):
84        QStandardItemModel.__init__(self, parent)
85        self.setHorizontalHeaderLabels(["Key", "Value"])
86        self.set_dict(dict)
87       
88    def set_dict(self, dict):
89        self._dict = dict
90        self.clear()
91        self.setHorizontalHeaderLabels(["Key", "Value"])
92        for key, value in sorted(dict.items()):
93            key_item = PyStandardItem(QString(key))
94            value_item = PyStandardItem(QString(value))
95            key_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
96            value_item.setFlags(value_item.flags() | Qt.ItemIsEditable)
97            self.appendRow([key_item, value_item])
98           
99    def get_dict(self):
100        dict = {}
101        for row in range(self.rowCount()):
102            key_item = self.item(row, 0)
103            value_item = self.item(row, 1)
104            dict[str(key_item.text())] = str(value_item.text())
105        return dict
106
107class VariableEditor(QWidget):
108    """ An editor widget for a variable.
109   
110    Can edit the variable name, and its attributes dictionary.
111     
112    """
113    def __init__(self, parent=None):
114        QWidget.__init__(self, parent)
115        self.setup_gui()
116       
117    def setup_gui(self):
118        layout = QVBoxLayout()
119        self.setLayout(layout)
120       
121        self.main_form = QFormLayout()
122        self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
123        layout.addLayout(self.main_form)
124       
125        self._setup_gui_name()
126        self._setup_gui_labels()
127       
128    def _setup_gui_name(self):
129        self.name_edit = QLineEdit()
130        self.main_form.addRow("Name", self.name_edit)
131        self.name_edit.editingFinished.connect(self.on_name_changed)
132       
133    def _setup_gui_labels(self):
134        vlayout = QVBoxLayout()
135        vlayout.setContentsMargins(0, 0, 0, 0)
136        vlayout.setSpacing(1)
137       
138        self.labels_edit = QTreeView()
139        self.labels_edit.setEditTriggers(QTreeView.DoubleClicked | \
140                                         QTreeView.EditKeyPressed)
141        self.labels_edit.setRootIsDecorated(False)
142       
143        self.labels_model = DictItemsModel()
144        self.labels_edit.setModel(self.labels_model)
145       
146        self.labels_edit.selectionModel().selectionChanged.connect(\
147                                    self.on_label_selection_changed)
148       
149        # Necessary signals to know when the labels change
150        self.labels_model.dataChanged.connect(self.on_labels_changed)
151        self.labels_model.rowsInserted.connect(self.on_labels_changed)
152        self.labels_model.rowsRemoved.connect(self.on_labels_changed)
153       
154        vlayout.addWidget(self.labels_edit)
155        hlayout = QHBoxLayout()
156        hlayout.setContentsMargins(0, 0, 0, 0)
157        hlayout.setSpacing(1)
158        self.add_label_action = QAction("+", self,
159                        toolTip="Add a new label.",
160                        triggered=self.on_add_label,
161                        enabled=False,
162                        shortcut=QKeySequence(QKeySequence.New))
163
164        self.remove_label_action = QAction("-", self,
165                        toolTip="Remove selected label.",
166                        triggered=self.on_remove_label,
167                        enabled=False,
168                        shortcut=QKeySequence(QKeySequence.Delete))
169       
170        button_size = OWGUI.toolButtonSizeHint()
171        button_size = QSize(button_size, button_size)
172       
173        button = QToolButton(self)
174        button.setFixedSize(button_size)
175        button.setDefaultAction(self.add_label_action)
176        hlayout.addWidget(button)
177       
178        button = QToolButton(self)
179        button.setFixedSize(button_size)
180        button.setDefaultAction(self.remove_label_action)
181        hlayout.addWidget(button)
182        hlayout.addStretch(10)
183        vlayout.addLayout(hlayout)
184       
185        self.main_form.addRow("Labels", vlayout)
186       
187    def set_data(self, var):
188        """ Set the variable to edit.
189        """
190        self.clear()
191        self.var = var
192       
193        if var is not None:
194            self.name_edit.setText(var.name)
195            self.labels_model.set_dict(dict(var.attributes))
196            self.add_label_action.setEnabled(True)
197        else:
198            self.add_label_action.setEnabled(False)
199            self.remove_label_action.setEnabled(False)
200           
201    def get_data(self):
202        """ Retrieve the modified variable.
203        """
204        name = str(self.name_edit.text())
205        labels = self.labels_model.get_dict()
206       
207        # Is the variable actually changed.
208        if not self.is_same():
209            var = type(self.var)(name)
210            var.attributes.update(labels)
211            self.var = var
212        else:
213            var = self.var
214       
215        return var
216   
217    def is_same(self):
218        """ Is the current model state the same as the input.
219        """
220        name = str(self.name_edit.text())
221        labels = self.labels_model.get_dict()
222       
223        return self.var and name == self.var.name and labels == self.var.attributes
224           
225    def clear(self):
226        """ Clear the editor state.
227        """
228        self.var = None
229        self.name_edit.setText("")
230        self.labels_model.set_dict({})
231       
232    def maybe_commit(self):
233        if not self.is_same():
234            self.commit()
235           
236    def commit(self):
237        """ Emit a ``variable_changed()`` signal.
238        """
239        self.emit(SIGNAL("variable_changed()"))
240       
241    @QtCore.Slot()
242    def on_name_changed(self):
243        self.maybe_commit()
244       
245    @QtCore.Slot()
246    def on_labels_changed(self, *args):
247        self.maybe_commit()
248       
249    @QtCore.Slot()
250    def on_add_label(self):
251        self.labels_model.appendRow([PyStandardItem(""), PyStandardItem("")])
252        row = self.labels_model.rowCount() - 1
253        index = self.labels_model.index(row, 0)
254        self.labels_edit.edit(index)
255       
256    @QtCore.Slot()
257    def on_remove_label(self):
258        rows = self.labels_edit.selectionModel().selectedRows()
259        if rows:
260            row = rows[0]
261            self.labels_model.removeRow(row.row())
262   
263    @QtCore.Slot()
264    def on_label_selection_changed(self):
265        selected = self.labels_edit.selectionModel().selectedRows()
266        self.remove_label_action.setEnabled(bool(len(selected)))
267       
268       
269class DiscreteVariableEditor(VariableEditor):
270    """ An editor widget for editing a discrete variable.
271   
272    Extends the :class:`VariableEditor` to enable editing of
273    variables values.
274   
275    """
276    def setup_gui(self):
277        layout = QVBoxLayout()
278        self.setLayout(layout)
279       
280        self.main_form = QFormLayout()
281        self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
282        layout.addLayout(self.main_form)
283       
284        self._setup_gui_name()
285        self._setup_gui_values()
286        self._setup_gui_labels()
287       
288    def _setup_gui_values(self):
289        self.values_edit = QListView()
290        self.values_edit.setEditTriggers(QListView.DoubleClicked | \
291                                         QListView.EditKeyPressed)
292        self.values_model = PyListModel(flags=Qt.ItemIsSelectable | \
293                                        Qt.ItemIsEnabled | Qt.ItemIsEditable)
294        self.values_edit.setModel(self.values_model)
295       
296        self.values_model.dataChanged.connect(self.on_values_changed)
297        self.main_form.addRow("Values", self.values_edit)
298
299    def set_data(self, var):
300        """ Set the variable to edit
301        """
302        VariableEditor.set_data(self, var)
303        self.values_model.wrap([])
304        if var is not None:
305            for v in var.values:
306                self.values_model.append(v)
307               
308    def get_data(self):
309        """ Retrieve the modified variable
310        """
311        name = str(self.name_edit.text())
312        labels = self.labels_model.get_dict()
313        values = map(str, self.values_model)
314       
315        if not self.is_same():
316            var = type(self.var)(name, values=values)
317            var.attributes.update(labels)
318            self.var = var
319        else:
320            var = self.var
321           
322        return var
323           
324    def is_same(self):
325        """ Is the current model state the same as the input.
326        """
327        values = map(str, self.values_model)
328        return VariableEditor.is_same(self) and self.var.values == values
329   
330    def clear(self):
331        """ Clear the model state.
332        """
333        VariableEditor.clear(self)
334        self.values_model.wrap([])
335       
336    @QtCore.Slot()
337    def on_values_changed(self):
338        self.maybe_commit()
339       
340       
341class ContinuousVariableEditor(VariableEditor):
342    #TODO: enable editing of number_of_decimals, scientific format ...
343    pass 
344
345
346class OWEditDomain(OWWidget):
347    contextHandlers = {"": DomainContextHandler("", ["domain_change_hints", "selected_index"])}
348    settingsList = ["auto_commit"]
349   
350    def __init__(self, parent=None, signalManager=None, title="Edit Domain"):
351        OWWidget.__init__(self, parent, signalManager, title)
352       
353        self.inputs = [("Data", Orange.data.Table, self.set_data)]
354        self.outputs = [("Data", Orange.data.Table)]
355       
356        # Settings
357       
358        # Domain change hints maps from input variables description to
359        # the modified variables description as returned by
360        # `variable_description` function
361        self.domain_change_hints = {}
362        self.selected_index = 0
363        self.auto_commit = False
364        self.changed_flag = False
365       
366        self.loadSettings()
367       
368        #####
369        # GUI
370        #####
371   
372        # The list of domain's variables.
373        box = OWGUI.widgetBox(self.controlArea, "Domain Features")
374        self.domain_view = QListView()
375        self.domain_view.setSelectionMode(QListView.SingleSelection)
376       
377        self.domain_model = VariableListModel()
378       
379        self.domain_view.setModel(self.domain_model)
380       
381        self.connect(self.domain_view.selectionModel(),
382                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
383                     self.on_selection_changed)
384       
385        box.layout().addWidget(self.domain_view)
386       
387        # A stack for variable editor widgets.
388        box = OWGUI.widgetBox(self.mainArea, "Edit Feature")
389        self.editor_stack = QStackedWidget()
390        box.layout().addWidget(self.editor_stack)
391       
392       
393        box = OWGUI.widgetBox(self.controlArea, "Reset")
394       
395        OWGUI.button(box, self, "Reset selected",
396                     callback=self.reset_selected,
397                     tooltip="Reset changes made to the selected feature"
398                     )
399       
400        OWGUI.button(box, self, "Reset all",
401                     callback=self.reset_all,
402                     tooltip="Reset all changes made to the domain"
403                     )
404       
405        box = OWGUI.widgetBox(self.controlArea, "Commit")
406       
407        b = OWGUI.button(box, self, "&Commit",
408                         callback=self.commit,
409                         tooltip="Commit the data with the changed domain",
410                         )
411       
412        cb = OWGUI.checkBox(box, self, "auto_commit",
413                            label="Commit automatically",
414                            tooltip="Commit the changed domain on any change",
415                            callback=self.commit_if)
416       
417        OWGUI.setStopper(self, b, cb, "changed_flag",
418                         callback=self.commit)
419       
420        self._editor_cache = {}
421       
422        self.resize(600, 500)
423       
424    def clear(self):
425        """ Clear the widget state.
426        """
427        self.data = None
428        self.domain_model[:] = []
429        self.domain_change_hints = {}
430        self.clear_editor()
431       
432    def clear_editor(self):
433        """ Clear the current editor widget
434        """
435        current = self.editor_stack.currentWidget()
436        if current:
437            QObject.disconnect(current, SIGNAL("variable_changed()"),
438                               self.on_variable_changed)
439            current.set_data(None)
440       
441    def set_data(self, data=None):
442        self.closeContext("")
443        self.clear()
444        self.data = data
445        if data is not None:
446            input_domain = data.domain
447            all_vars = list(input_domain.variables) + \
448                       input_domain.getmetas().values()
449           
450            self.openContext("", data)
451           
452            edited_vars = []
453           
454            # Apply any saved transformations as listed in
455            # `domain_change_hints`
456             
457            for var in all_vars:
458                desc = variable_description(var)
459                changed = self.domain_change_hints.get(desc, None)
460                if changed is not None:
461                    new = variable_from_description(changed)
462                   
463                    # Make sure orange's domain transformations will work.
464                    new.source_variable = var
465                    new.get_value_from = Orange.core.ClassifierFromVar(whichVar=var)
466                    var = new
467                edited_vars.append(var)
468           
469            self.all_vars = all_vars
470            self.input_domain = input_domain
471           
472            # Sets the model to display in the 'Domain Features' view
473            self.domain_model[:] = edited_vars
474           
475            # Try to restore the variable selection
476            index = self.selected_index
477            if self.selected_index >= len(all_vars):
478                index = 0 if len(all_vars) else -1
479            if index >= 0:
480                self.select_variable(index)
481       
482            self.changed_flag = True
483            self.commit_if()
484        else:
485            # To force send None on output
486            self.commit()
487           
488    def on_selection_changed(self, *args):
489        """ When selection in 'Domain Features' view changes.
490        """
491        i = self.selected_var_index()
492        if i is not None:
493            self.open_editor(i)
494            self.selected_index = i
495       
496    def selected_var_index(self):
497        """ Return the selected row in 'Domain Features' view or None
498        if no row is selected.
499       
500        """
501        rows = self.domain_view.selectionModel().selectedRows()
502        if rows:
503            return rows[0].row()
504        else:
505            return None
506       
507    def select_variable(self, index):
508        """ Select the variable with ``index`` in the 'Domain Features'
509        view.
510       
511        """
512        sel_model = self.domain_view.selectionModel()
513        sel_model.select(self.domain_model.index(index, 0),
514                         QItemSelectionModel.ClearAndSelect)
515       
516    def open_editor(self, index):
517        """ Open the editor for variable at ``index`` and move it
518        to the top if the stack.
519       
520        """
521        # First remove and clear the current editor if any
522        self.clear_editor()
523           
524        var = self.domain_model[index]
525       
526        editor = self.editor_for_variable(var)
527        editor.set_data(var)
528        self.edited_variable_index = index
529       
530        QObject.connect(editor, SIGNAL("variable_changed()"),
531                        self.on_variable_changed)
532        self.editor_stack.setCurrentWidget(editor)
533   
534    def editor_for_variable(self, var):
535        """ Return the editor for ``var``'s variable type.
536       
537        The editors are cached and reused by type.
538         
539        """
540        editor = None
541        if is_discrete(var):
542            editor = DiscreteVariableEditor
543        elif is_continuous(var):
544            editor = ContinuousVariableEditor
545        else:
546            editor = VariableEditor
547           
548        if type(var) not in self._editor_cache:
549            editor = editor()
550            self._editor_cache[type(var)] = editor
551            self.editor_stack.addWidget(editor)
552           
553        return self._editor_cache[type(var)]
554   
555    def on_variable_changed(self):
556        """ When the user edited the current variable in editor.
557        """
558        var = self.domain_model[self.edited_variable_index]
559        editor = self.editor_stack.currentWidget()
560        new_var = editor.get_data()
561       
562        # Replace the variable in the 'Domain Features' view/model
563        self.domain_model[self.edited_variable_index] = new_var
564        old_var = self.all_vars[self.edited_variable_index]
565       
566        # Store the transformation hint.
567        self.domain_change_hints[variable_description(old_var)] = \
568                    variable_description(new_var)
569
570        # Make orange's domain transformation work.
571        new_var.source_variable = old_var
572        new_var.get_value_from = Orange.core.ClassifierFromVar(whichVar=old_var)
573       
574        self.commit_if()
575         
576    def reset_all(self):
577        """ Reset all variables to the input state.
578        """
579        self.domain_change_hints = {}
580        if self.data is not None:
581            # To invalidate stored hints
582            self.closeContext("")
583            self.openContext("", self.data)
584            self.domain_model[:] = self.all_vars
585            self.select_variable(self.selected_index)
586            self.commit_if()
587           
588    def reset_selected(self):
589        """ Reset the currently selected variable to its original
590        state.
591         
592        """
593        if self.data is not None:
594            var = self.all_vars[self.selected_index]
595            desc = variable_description(var)
596            if desc in self.domain_change_hints:
597                del self.domain_change_hints[desc]
598           
599            # To invalidate stored hints
600            self.closeContext("")
601            self.openContext("", self.data)
602           
603            self.domain_model[self.selected_index] = var
604            self.editor_stack.currentWidget().set_data(var)
605            self.commit_if()
606           
607    def commit_if(self):
608        if self.auto_commit:
609            self.commit()
610        else:
611            self.changed_flag = True
612       
613    def commit(self):
614        """ Commit the changed data to output.
615        """
616        new_data = None
617        if self.data is not None:
618            new_vars = list(self.domain_model)
619            variables = new_vars[: len(self.input_domain.variables)]
620            class_var = None
621            if self.input_domain.class_var:
622                class_var = variables[-1]
623                variables = variables[:-1]
624           
625            new_metas = new_vars[len(self.input_domain.variables) :]
626            new_domain = Orange.data.Domain(variables, class_var)
627           
628            # Assumes getmetas().items() order has not changed.
629            # TODO: store metaids in set_data method
630            for (mid, _), new in zip(self.input_domain.getmetas().items(), 
631                                       new_metas):
632                new_domain.addmeta(mid, new)
633               
634            new_data = Orange.data.Table(new_domain, self.data)
635       
636        self.send("Data", new_data)
637        self.changed_flag = False
638           
639       
640def main():
641    import sys
642    app = QApplication(sys.argv)
643    w = OWEditDomain()
644    data = Orange.data.Table("iris")
645#    data = Orange.data.Table("rep:GDS636.tab")
646    w.set_data(data)
647    w.show()
648    rval = app.exec_()
649    w.set_data(None)
650    w.saveSettings()
651    return rval
652       
653if __name__ == "__main__":
654    import sys
655    sys.exit(main())
656   
Note: See TracBrowser for help on using the repository browser.