Changeset 9255:8b1b10d7f43f in orange


Ignore:
Timestamp:
11/23/11 16:08:20 (2 years ago)
Author:
ales_erjavec <ales.erjavec@…>
Branch:
default
Convert:
18c6b3e21252a139159c6048c6857cfd9e2040fc
Message:

Merged Prototypes/OWDataDomainMk2 into OWDataDomain, replacing the old implementation of Select Attributes widgets (fixes #991).

File:
1 edited

Legend:

Unmodified
Added
Removed
  • orange/OrangeWidgets/Data/OWDataDomain.py

    r8042 r9255  
    44<icon>icons/SelectAttributes.png</icon> 
    55<priority>1100</priority> 
    6 <contact>Peter Juvan (peter.juvan@fri.uni-lj.si)</contact> 
     6<contact>Ales Erjavec (ales.erjavec@fri.uni-lj.si)</contact> 
    77""" 
     8 
     9import sys 
     10 
    811from OWWidget import * 
    9 import OWGUI, OWGUIEx 
    10  
     12from OWItemModels import PyListModel, VariableListModel 
     13 
     14import Orange 
     15 
     16def slices(indices): 
     17    """ Group the given integer indices into slices 
     18    """ 
     19    indices = list(sorted(indices)) 
     20    if indices: 
     21        first = last = indices[0] 
     22        for i in indices[1:]: 
     23            if i == last + 1: 
     24                last = i 
     25            else: 
     26                yield first, last + 1 
     27                first = last = i 
     28        yield first, last + 1 
     29         
     30def source_model(view): 
     31    """ Return the source model for the Qt Item View if it uses 
     32    the QSortFilterProxyModel. 
     33     
     34    """ 
     35    if isinstance(view.model(),  QSortFilterProxyModel): 
     36        return view.model().sourceModel() 
     37    else: 
     38        return view.model() 
     39 
     40def source_indexes(indexes, view): 
     41    """ Map model indexes through a views QSortFilterProxyModel 
     42    """ 
     43    model = view.model() 
     44    if isinstance(model, QSortFilterProxyModel): 
     45        return map(model.mapToSource, indexes) 
     46    else: 
     47        return indexes  
     48             
     49def delslice(model, start, end): 
     50    """ Delete the start, end slice (rows) from the model.  
     51    """ 
     52    if isinstance(model, PyListModel): 
     53        model.__delslice__(start, end) 
     54    elif isinstance(model, QAbstractItemModel): 
     55        model.removeRows(start, end-start) 
     56    else: 
     57        raise TypeError(type(model)) 
     58         
     59class VariablesListItemModel(VariableListModel): 
     60    """ An Qt item model for for list of orange.Variable objects. 
     61    Supports drag operations 
     62     
     63    """ 
     64         
     65    def flags(self, index): 
     66        flags = VariableListModel.flags(self, index) 
     67        if index.isValid(): 
     68            flags |= Qt.ItemIsDragEnabled 
     69        else: 
     70            flags |= Qt.ItemIsDropEnabled 
     71        return flags 
     72     
     73    ########### 
     74    # Drag/Drop 
     75    ########### 
     76     
     77    MIME_TYPE = "application/x-Orange-VariableListModelData" 
     78     
     79    def supportedDropActions(self): 
     80        return Qt.MoveAction 
     81     
     82    def supportedDragActions(self): 
     83        return Qt.MoveAction 
     84     
     85    def mimeTypes(self): 
     86        return [self.MIME_TYPE] 
     87     
     88    def mimeData(self, indexlist): 
     89        descriptors = [] 
     90        vars = [] 
     91        item_data = [] 
     92        for index in indexlist: 
     93            var = self[index.row()] 
     94            descriptors.append((var.name, var.varType)) 
     95            vars.append(var) 
     96            item_data.append(self.itemData(index)) 
     97         
     98        mime = QMimeData() 
     99        mime.setData(self.MIME_TYPE, QByteArray(str(descriptors))) 
     100        mime._vars = vars 
     101        mime._item_data = item_data 
     102        return mime 
     103     
     104    def dropMimeData(self, mime, action, row, column, parent): 
     105        if action == Qt.IgnoreAction: 
     106            return True 
     107     
     108        vars, item_data = self.items_from_mime_data(mime) 
     109        if vars is None: 
     110            return False 
     111         
     112        if row == -1: 
     113            row = len(self) 
     114             
     115        self.__setslice__(row, row, vars) 
     116         
     117        for i, data in enumerate(item_data): 
     118            self.setItemData(self.index(row + i), data) 
     119             
     120        return True 
     121     
     122    def items_from_mime_data(self, mime): 
     123        if not mime.hasFormat(self.MIME_TYPE): 
     124            return None, None 
     125         
     126        if hasattr(mime, "_vars"): 
     127            vars = mime._vars 
     128            item_data = mime._item_data 
     129            return vars, item_data 
     130        else: 
     131            #TODO: get vars from orange.Variable.getExisting 
     132            return None, None 
     133         
     134class ClassVarListItemModel(VariablesListItemModel): 
     135    def dropMimeData(self, mime, action, row, column, parent): 
     136        """ Ensure only one variable can be dropped onto the view. 
     137        """ 
     138        vars, _ = self.items_from_mime_data(mime) 
     139         
     140        if vars is None or len(self) + len(vars) > 1: 
     141            return False 
     142         
     143        if action == Qt.IgnoreAction: 
     144            return True 
     145         
     146        return VariablesListItemModel.dropMimeData(self, mime, action, row, column, parent) 
     147         
     148class VariablesListItemView(QListView): 
     149    """ A Simple QListView subclass initialized for displaying 
     150    variables. 
     151      
     152    """ 
     153    def __init__(self, parent=None): 
     154        QListView.__init__(self, parent) 
     155        self.setSelectionMode(QListView.ExtendedSelection) 
     156        self.setAcceptDrops(True) 
     157        self.setDragEnabled(True) 
     158        self.setDropIndicatorShown(True) 
     159        self.setDragDropMode(QListView.DragDrop) 
     160        if hasattr(self, "setDefaultDropAction"): 
     161            # For compatibility with Qt version < 4.6 
     162            self.setDefaultDropAction(Qt.MoveAction) 
     163        self.setDragDropOverwriteMode(False) 
     164        self.viewport().setAcceptDrops(True) 
     165     
     166    def startDrag(self, supported_actions): 
     167        indices = self.selectionModel().selectedIndexes() 
     168        indices = [i for i in indices if i.flags() & Qt.ItemIsDragEnabled] 
     169        if indices: 
     170            data = self.model().mimeData(indices) 
     171            if not data: 
     172                return 
     173            rect = QRect() 
     174#            pixmap = self.render_to_pixmap(indices) 
     175             
     176            drag = QDrag(self) 
     177            drag.setMimeData(data) 
     178#            drag.setPixmap(pixmap) 
     179             
     180            default_action = Qt.IgnoreAction 
     181            if hasattr(self, "defaultDropAction") and \ 
     182                    self.defaultDropAction() != Qt.IgnoreAction and \ 
     183                    supported_actions & self.defaultDropAction(): 
     184                default_action = self.defaultDropAction() 
     185            elif supported_actions & Qt.CopyAction and dragDropMode() != QListView.InternalMove: 
     186                default_action = Qt.CopyAction 
     187             
     188            res = drag.exec_(supported_actions, default_action) 
     189                 
     190            if res == Qt.MoveAction: 
     191                selected = self.selectionModel().selectedIndexes() 
     192                rows = map(QModelIndex.row, selected) 
     193                 
     194                for s1, s2 in reversed(list(slices(rows))): 
     195                    delslice(self.model(), s1, s2) 
     196#                    del source_model(self)[s1:s2] 
     197     
     198    def render_to_pixmap(self, indices): 
     199        pass 
     200 
     201class ClassVariableItemView(VariablesListItemView): 
     202    def __init__(self, parent=None): 
     203        VariablesListItemView.__init__(self, parent) 
     204        self.setDropIndicatorShown(False) 
     205     
     206    def dragEnterEvent(self, event): 
     207        """ Don't accept drops if the class is already present in the model. 
     208        """ 
     209        if self.accepts_drop(event): 
     210            event.accept() 
     211        else: 
     212            event.ignore() 
     213             
     214    def accepts_drop(self, event): 
     215        mime = event.mimeData() 
     216        vars, _ = self.model().items_from_mime_data(mime) 
     217        if vars is None: 
     218            return event.ignore() 
     219         
     220        if len(self.model()) + len(vars) > 1: 
     221            return event.ignore() 
     222        return True 
     223     
     224class VariableFilterProxyModel(QSortFilterProxyModel): 
     225    """ A proxy model for filtering a list of variables based on 
     226    their names and labels. 
     227      
     228    """ 
     229    def __init__(self, parent=None): 
     230        QSortFilterProxyModel.__init__(self, parent) 
     231        self._filter_string = "" 
     232         
     233    def set_filter_string(self, filter): 
     234        self._filter_string = str(filter).lower() 
     235        self.invalidateFilter() 
     236         
     237    def filter_accepts_variable(self, var): 
     238        row_str = var.name + " ".join(("%s=%s" % item) \ 
     239                    for item in var.attributes.items()) 
     240        row_str = row_str.lower() 
     241        filters = self._filter_string.split() 
     242         
     243        return all(f in row_str for f in filters) 
     244                    
     245    def filterAcceptsRow(self, source_row, source_parent): 
     246        model = self.sourceModel() 
     247        if isinstance(model, VariableListModel): 
     248            var = model[source_row] 
     249            return self.filter_accepts_variable(var) 
     250        else: 
     251            return True 
     252         
     253USE_COMPLETER = True 
     254 
     255class CompleterNavigator(QObject): 
     256    """ An event filter to be installed on a QLineEdit, to enable  
     257    Key up/ down to navigate between posible completions. 
     258     
     259    """ 
     260    def eventFilter(self, obj, event): 
     261        if event.type() == QEvent.KeyPress and isinstance(obj, QLineEdit): 
     262            if event.key() == Qt.Key_Down: 
     263                diff = 1 
     264            elif event.key() == Qt.Key_Up: 
     265                diff = -1 
     266            else: 
     267                return False 
     268                 
     269            completer = obj.completer() 
     270            if completer is not None and completer.completionCount() > 0: 
     271                current = completer.currentRow() 
     272                current += diff 
     273                r = completer.setCurrentRow(current % completer.completionCount()) 
     274                completer.complete() 
     275            return True 
     276        else: 
     277            return False 
     278     
     279     
     280from functools import partial 
    11281class OWDataDomain(OWWidget): 
    12     contextHandlers = {"": DomainContextHandler("", [ContextField("chosenAttributes",  
    13                                                                   DomainContextHandler.RequiredList + DomainContextHandler.IncludeMetaAttributes,  
    14                                                                   selected="selectedChosen", reservoir="inputAttributes"),  
    15                                                      ContextField("classAttribute",  
    16                                                                   DomainContextHandler.RequiredList + DomainContextHandler.IncludeMetaAttributes,  
    17                                                                   selected="selectedClass", reservoir="inputAttributes"),  
    18                                                      ContextField("metaAttributes",  
    19                                                                   DomainContextHandler.RequiredList + DomainContextHandler.IncludeMetaAttributes,  
    20                                                                   selected="selectedMeta", reservoir="inputAttributes") 
    21                                                      ], syncWithGlobal = False)} 
    22  
    23  
    24     def __init__(self, parent = None, signalManager = None): 
    25         OWWidget.__init__(self, parent, signalManager, "Data Domain", wantMainArea = 0)  
    26  
    27         self.inputs = [("Examples", ExampleTable, self.setData), ("Attribute Subset", AttributeList, self.setAttributeList)] 
    28         self.outputs = [("Examples", ExampleTable)] 
    29  
    30         buttonWidth = 50 
    31         applyButtonWidth = 101 
    32  
    33         self.data = None 
    34         self.receivedAttrList = None 
    35  
    36         self.selectedInput = [] 
    37         self.inputAttributes = [] 
    38         self.selectedChosen = [] 
    39         self.chosenAttributes = [] 
    40         self.selectedClass = [] 
    41         self.classAttribute = [] 
    42         self.metaAttributes = [] 
    43         self.selectedMeta = [] 
    44  
     282    contextHandlers = {"": DomainContextHandler("", [ContextField("domain_role_hints")])} 
     283     
     284    def __init__(self, parent=None, signalManager=None, name="Select Attributes"): 
     285        OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) 
     286         
     287        self.inputs = [("Examples", ExampleTable, self.set_data)] 
     288        self.outputs = [("Examples", ExampleTable), ("Attribute List", AttributeList)] 
     289         
     290        self.domain_role_hints = {} 
     291         
    45292        self.loadSettings() 
    46  
     293         
     294        # #### 
     295        # GUI 
     296        # #### 
     297         
    47298        import sip 
    48299        sip.delete(self.controlArea.layout()) 
    49         grid = QGridLayout() 
    50         self.controlArea.setLayout(grid) 
    51         grid.setMargin(0) 
     300         
     301        layout = QGridLayout() 
     302        layout.setMargin(0) 
     303        box = OWGUI.widgetBox(self.controlArea, "Available attributes", addToLayout=False) 
     304         
     305        self.filter_edit = QLineEdit() 
     306        self.filter_edit.setToolTip("Filter the list of available variables.") 
     307         
     308        box.layout().addWidget(self.filter_edit) 
     309         
     310        if hasattr(self.filter_edit, "setPlaceholderText"): # For Compatibility with Qt version > 4.7 
     311            self.filter_edit.setPlaceholderText("Filter") 
     312         
     313        # Completer 
     314        if USE_COMPLETER: 
     315            self.completer = QCompleter() 
     316            self.completer.setCompletionMode(QCompleter.InlineCompletion) 
     317            self.completer_model = QStringListModel() 
     318            self.completer.setModel(self.completer_model) 
     319            self.completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) 
     320     
     321            self.filter_edit.setCompleter(self.completer) 
     322            self.completer_navigator = CompleterNavigator(self) 
     323            self.filter_edit.installEventFilter(self.completer_navigator) 
     324             
     325        self.available_attrs = VariablesListItemModel() 
     326        self.available_attrs_proxy = VariableFilterProxyModel() 
     327        self.available_attrs_proxy.setSourceModel(self.available_attrs) 
     328        self.available_attrs_view = VariablesListItemView() 
     329        self.available_attrs_view.setModel(self.available_attrs_proxy) 
     330         
     331         
     332        self.connect(self.filter_edit, 
     333                     SIGNAL("textChanged(QString)"), 
     334                     self.available_attrs_proxy.set_filter_string) 
     335         
     336        self.connect(self.available_attrs_view.selectionModel(), 
     337                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), 
     338                     partial(self.update_interface_state, 
     339                             self.available_attrs_view)) 
     340         
     341        if USE_COMPLETER: 
     342            self.connect(self.filter_edit, 
     343                         SIGNAL("textChanged(QString)"), 
     344                         self.update_completer_prefix) 
     345         
     346            self.connect(self.available_attrs, 
     347                         SIGNAL("dataChanged(QModelIndex, QModelIndex)"), 
     348                         self.update_completer_model) 
     349             
     350            self.connect(self.available_attrs, 
     351                         SIGNAL("rowsInserted(QModelIndex, int, int)"), 
     352                         self.update_completer_model) 
     353             
     354            self.connect(self.available_attrs, 
     355                         SIGNAL("rowsRemoved(QModelIndex, int, int)"), 
     356                         self.update_completer_model) 
     357         
     358        box.layout().addWidget(self.available_attrs_view) 
     359        layout.addWidget(box, 0, 0, 3, 1) 
     360         
     361        box = OWGUI.widgetBox(self.controlArea, "Attributes", addToLayout=False) 
     362        self.used_attrs = VariablesListItemModel() 
     363        self.used_attrs_view = VariablesListItemView() 
     364        self.used_attrs_view.setModel(self.used_attrs) 
     365        self.connect(self.used_attrs_view.selectionModel(), 
     366                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), 
     367                     partial(self.update_interface_state, 
     368                             self.used_attrs_view)) 
     369         
     370        box.layout().addWidget(self.used_attrs_view) 
     371        layout.addWidget(box, 0, 2, 1, 1) 
     372         
     373        box = OWGUI.widgetBox(self.controlArea, "Class", addToLayout=False) 
     374        self.class_attrs = ClassVarListItemModel() 
     375        self.class_attrs_view = ClassVariableItemView() 
     376        self.class_attrs_view.setModel(self.class_attrs) 
     377        self.connect(self.class_attrs_view.selectionModel(), 
     378                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), 
     379                     partial(self.update_interface_state, 
     380                             self.class_attrs_view)) 
     381         
     382        self.class_attrs_view.setMaximumHeight(24) 
     383        box.layout().addWidget(self.class_attrs_view) 
     384        layout.addWidget(box, 1, 2, 1, 1) 
     385         
     386        box = OWGUI.widgetBox(self.controlArea, "Meta Attributes", addToLayout=False) 
     387        self.meta_attrs = VariablesListItemModel() 
     388        self.meta_attrs_view = VariablesListItemView() 
     389        self.meta_attrs_view.setModel(self.meta_attrs) 
     390        self.connect(self.meta_attrs_view.selectionModel(), 
     391                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), 
     392                     partial(self.update_interface_state, 
     393                             self.meta_attrs_view)) 
     394         
     395        box.layout().addWidget(self.meta_attrs_view) 
     396        layout.addWidget(box, 2, 2, 1, 1) 
     397         
     398        bbox = OWGUI.widgetBox(self.controlArea, addToLayout=False, margin=0) 
     399        layout.addWidget(bbox, 0, 1, 1, 1) 
     400         
     401        self.up_attr_button = OWGUI.button(bbox, self, "Up",  
     402                    callback = partial(self.move_up, self.used_attrs_view)) 
     403        self.move_attr_button = OWGUI.button(bbox, self, ">", 
     404                    callback = partial(self.move_selected, self.used_attrs_view)) 
     405        self.down_attr_button = OWGUI.button(bbox, self, "Down", 
     406                    callback = partial(self.move_down, self.used_attrs_view)) 
     407         
     408        bbox = OWGUI.widgetBox(self.controlArea, addToLayout=False, margin=0) 
     409        layout.addWidget(bbox, 1, 1, 1, 1) 
     410        self.move_class_button = OWGUI.button(bbox, self, ">", 
     411                    callback = partial(self.move_selected, 
     412                            self.class_attrs_view, exclusive=True)) 
     413         
     414        bbox = OWGUI.widgetBox(self.controlArea, addToLayout=False, margin=0) 
     415        layout.addWidget(bbox, 2, 1, 1, 1) 
     416        self.up_meta_button = OWGUI.button(bbox, self, "Up", 
     417                    callback = partial(self.move_up, self.meta_attrs_view)) 
     418        self.move_meta_button = OWGUI.button(bbox, self, ">", 
     419                    callback = partial(self.move_selected, self.meta_attrs_view)) 
     420        self.down_meta_button = OWGUI.button(bbox, self, "Down", 
     421                    callback = partial(self.move_down, self.meta_attrs_view)) 
     422         
     423        bbox = OWGUI.widgetBox(self.controlArea, orientation="horizontal", addToLayout=False, margin=0) 
     424        applyButton = OWGUI.button(bbox, self, "Apply", callback=self.commit) 
     425        resetButton = OWGUI.button(bbox, self, "Reset", callback=self.reset) 
     426         
     427        layout.addWidget(bbox, 3, 0, 1, 3) 
     428         
     429        layout.setRowStretch(0, 4) 
     430        layout.setRowStretch(1, 0) 
     431        layout.setRowStretch(2, 2) 
     432        layout.setHorizontalSpacing(0) 
     433        self.controlArea.setLayout(layout) 
     434         
     435        self.data = None 
     436        self.original_completer_items = [] 
     437 
     438        self.resize(500, 600) 
     439         
     440        # For automatic widget testing using 
     441        self._guiElements.extend( \ 
     442                  [(QListView, self.available_attrs_view), 
     443                   (QListView, self.used_attrs_view), 
     444                   (QListView, self.class_attrs_view), 
     445                   (QListView, self.meta_attrs_view), 
     446                  ]) 
     447         
     448    def set_data(self, data=None): 
     449        self.update_domain_role_hints() 
     450        self.closeContext("") 
     451        self.data = data 
     452        if data is not None: 
     453            self.openContext("", data) 
     454            all_vars = data.domain.variables + data.domain.getmetas().values() 
     455             
     456            var_sig = lambda attr: (attr.name, attr.varType) 
     457             
     458            domain_hints = dict([(var_sig(attr), ("attribute", i)) \ 
     459                            for i, attr in enumerate(data.domain.attributes)]) 
     460             
     461            domain_hints.update(dict([(var_sig(attr), ("meta", i)) \ 
     462                for i, attr in enumerate(data.domain.getmetas().values())])) 
     463             
     464            if data.domain.class_var: 
     465                domain_hints[var_sig(data.domain.class_var)] = ("class", 0) 
     466                     
     467            domain_hints.update(self.domain_role_hints) # update the hints from context settings 
     468             
     469            attrs_for_role = lambda role: [(domain_hints[var_sig(attr)][1], attr) \ 
     470                    for attr in all_vars if domain_hints[var_sig(attr)][0] == role] 
     471             
     472            attributes = [attr for place, attr in sorted(attrs_for_role("attribute"))] 
     473            classes = [attr for place, attr in sorted(attrs_for_role("class"))] 
     474            metas = [attr for place, attr in sorted(attrs_for_role("meta"))] 
     475            available = [attr for place, attr in sorted(attrs_for_role("available"))] 
     476             
     477            self.used_attrs[:] = attributes 
     478            self.class_attrs[:] = classes 
     479            self.meta_attrs[:] = metas 
     480            self.available_attrs[:] = available 
     481        else: 
     482            self.used_attrs[:] = [] 
     483            self.class_attrs[:] = [] 
     484            self.meta_attrs[:] = [] 
     485            self.available_attrs[:] = [] 
     486         
     487        self.commit() 
     488         
     489    def update_domain_role_hints(self): 
     490        """ Update the domain hints to be stored in the widgets settings. 
     491        """ 
     492        hints_from_model = lambda role, model: \ 
     493                [((attr.name, attr.varType), (role, i)) \ 
     494                 for i, attr in enumerate(model)] 
     495         
     496        hints = dict(hints_from_model("available", self.available_attrs)) 
     497        hints.update(hints_from_model("attribute", self.used_attrs)) 
     498        hints.update(hints_from_model("class", self.class_attrs)) 
     499        hints.update(hints_from_model("meta", self.meta_attrs)) 
     500        self.domain_role_hints = hints 
     501         
     502    def selected_rows(self, view): 
     503        """ Return the selected rows in the view.  
     504        """ 
     505        rows = view.selectionModel().selectedRows() 
     506        model = view.model() 
     507        if isinstance(model, QSortFilterProxyModel): 
     508            rows = [model.mapToSource(r) for r in rows] 
     509        return [r.row() for r in rows] 
     510     
     511    def move_rows(self, view, rows, offset): 
     512        model = view.model() 
     513        newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] 
     514         
     515        for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): 
     516            model[row], model[newrow] = model[newrow], model[row] 
     517             
     518        selection = QItemSelection() 
     519        for nrow in newrows: 
     520            index = model.index(nrow, 0) 
     521            selection.select(index, index) 
     522        view.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect) 
     523         
     524    def move_up(self, view): 
     525        selected = self.selected_rows(view) 
     526        self.move_rows(view, selected, -1) 
     527     
     528    def move_down(self, view): 
     529        selected = self.selected_rows(view) 
     530        self.move_rows(view, selected, 1) 
     531         
     532    def move_selected(self, view, exclusive=False): 
     533        if self.selected_rows(view): 
     534            self.move_selected_from_to(view, self.available_attrs_view) 
     535        elif self.selected_rows(self.available_attrs_view): 
     536            self.move_selected_from_to(self.available_attrs_view, view, exclusive) 
     537     
     538    def move_selected_from_to(self, src, dst, exclusive=False): 
     539        self.move_from_to(src, dst, self.selected_rows(src), exclusive)     
     540         
     541    def move_from_to(self, src, dst, rows, exclusive=False): 
     542        src_model = source_model(src) 
     543        attrs = [src_model[r] for r in rows] 
     544         
     545        if exclusive and len(attrs) != 1: 
     546            return 
     547         
     548        for s1, s2 in reversed(list(slices(rows))): 
     549            del src_model[s1:s2] 
     550             
     551        dst_model = source_model(dst) 
     552        if exclusive and len(dst_model) > 0: 
     553            src_model.append(dst_model[0]) 
     554            del dst_model[0] 
     555             
     556        dst_model.extend(attrs) 
     557         
     558    def update_interface_state(self, focus=None, selected=None, deselected=None): 
     559        for view in [self.available_attrs_view, self.used_attrs_view, 
     560                     self.class_attrs_view, self.meta_attrs_view]: 
     561            if view is not focus and not view.hasFocus() and self.selected_rows(view): 
     562                view.selectionModel().clear() 
    52563                 
    53         boxAvail = OWGUI.widgetBox(self, 'Available Attributes', addToLayout = 0) 
    54         grid.addWidget(boxAvail, 0, 0, 3, 1) 
    55  
    56         self.filterInputAttrs = OWGUIEx.lineEditFilter(boxAvail, self, None, useRE = 1, emptyText = "filter attributes...", callback = self.setInputAttributes, caseSensitive = 0) 
    57         self.inputAttributesList = OWGUI.listBox(boxAvail, self, "selectedInput", "inputAttributes", callback = self.onSelectionChange, selectionMode = QListWidget.ExtendedSelection, enableDragDrop = 1, dragDropCallback = self.updateInterfaceAndApplyButton) 
    58         self.filterInputAttrs.listbox = self.inputAttributesList  
    59  
    60         vbAttr = OWGUI.widgetBox(self, addToLayout = 0) 
    61         grid.addWidget(vbAttr, 0, 1) 
    62         self.attributesButtonUp = OWGUI.button(vbAttr, self, "Up", self.onAttributesButtonUpClick) 
    63         self.attributesButtonUp.setMaximumWidth(buttonWidth) 
    64         self.attributesButton = OWGUI.button(vbAttr, self, ">", self.onAttributesButtonClicked) 
    65         self.attributesButton.setMaximumWidth(buttonWidth) 
    66         self.attributesButtonDown = OWGUI.button(vbAttr, self, "Down", self.onAttributesButtonDownClick) 
    67         self.attributesButtonDown.setMaximumWidth(buttonWidth) 
    68  
    69         boxAttr = OWGUI.widgetBox(self, 'Attributes', addToLayout = 0) 
    70         grid.addWidget(boxAttr, 0, 2) 
    71         self.attributesList = OWGUI.listBox(boxAttr, self, "selectedChosen", "chosenAttributes", callback = self.onSelectionChange, selectionMode = QListWidget.ExtendedSelection, enableDragDrop = 1, dragDropCallback = self.updateInterfaceAndApplyButton) 
    72  
    73         self.classButton = OWGUI.button(self, self, ">", self.onClassButtonClicked, addToLayout = 0) 
    74         self.classButton.setMaximumWidth(buttonWidth) 
    75         grid.addWidget(self.classButton, 1, 1) 
    76         boxClass = OWGUI.widgetBox(self, 'Class', addToLayout = 0) 
    77         boxClass.setFixedHeight(55) 
    78         grid.addWidget(boxClass, 1, 2) 
    79         self.classList = OWGUI.listBox(boxClass, self, "selectedClass", "classAttribute", callback = self.onSelectionChange, selectionMode = QListWidget.ExtendedSelection, enableDragDrop = 1, dragDropCallback = self.updateInterfaceAndApplyButton, dataValidityCallback = self.dataValidityCallback) 
    80  
    81         vbMeta = OWGUI.widgetBox(self, addToLayout = 0) 
    82         grid.addWidget(vbMeta, 2, 1) 
    83         self.metaButtonUp = OWGUI.button(vbMeta, self, "Up", self.onMetaButtonUpClick) 
    84         self.metaButtonUp.setMaximumWidth(buttonWidth) 
    85         self.metaButton = OWGUI.button(vbMeta, self, ">", self.onMetaButtonClicked) 
    86         self.metaButton.setMaximumWidth(buttonWidth) 
    87         self.metaButtonDown = OWGUI.button(vbMeta, self, "Down", self.onMetaButtonDownClick) 
    88         self.metaButtonDown.setMaximumWidth(buttonWidth) 
    89         boxMeta = OWGUI.widgetBox(self, 'Meta Attributes', addToLayout = 0) 
    90         grid.addWidget(boxMeta, 2, 2) 
    91         self.metaList = OWGUI.listBox(boxMeta, self, "selectedMeta", "metaAttributes", callback = self.onSelectionChange, selectionMode = QListWidget.ExtendedSelection, enableDragDrop = 1, dragDropCallback = self.updateInterfaceAndApplyButton) 
    92  
    93         boxApply = OWGUI.widgetBox(self, addToLayout = 0, orientation = "horizontal", addSpace = 1) 
    94         grid.addWidget(boxApply, 3, 0, 3, 3) 
    95         self.applyButton = OWGUI.button(boxApply, self, "Apply", callback = self.setOutput, default=True) 
    96         self.applyButton.setEnabled(False) 
    97         self.applyButton.setMaximumWidth(applyButtonWidth) 
    98         self.resetButton = OWGUI.button(boxApply, self, "Reset", callback = self.reset) 
    99         self.resetButton.setMaximumWidth(applyButtonWidth) 
    100          
    101         grid.setRowStretch(0, 4) 
    102         grid.setRowStretch(1, 0) 
    103         grid.setRowStretch(2, 2) 
    104  
    105         self.icons = self.createAttributeIconDict() 
    106  
    107         self.inChange = False 
    108         self.resize(400, 480) 
    109  
    110  
    111     def setAttributeList(self, attrList): 
    112         self.receivedAttrList = attrList 
    113         self.setData(self.data) 
    114  
    115     def onSelectionChange(self): 
    116         if not self.inChange: 
    117             self.inChange = True 
    118             for lb, co in [(self.inputAttributesList, "selectedInput"),  
    119                        (self.attributesList, "selectedChosen"),  
    120                        (self.classList, "selectedClass"),  
    121                        (self.metaList, "selectedMeta")]: 
    122                 if not lb.hasFocus(): 
    123                     setattr(self, co, []) 
    124             self.inChange = False 
    125  
    126         self.updateInterfaceState() 
    127  
    128  
    129     def setData(self, data): 
    130         if self.data and data and self.data.checksum() == data.checksum(): 
    131             return   # we received the same dataset again 
    132  
    133         self.closeContext() 
    134  
    135         self.data = data 
    136         self.attributes = {} 
    137         self.filterInputAttrs.setText("") 
    138  
    139         if data: 
    140             domain = data.domain 
    141  
    142             if domain.classVar: 
    143                 self.classAttribute = [(domain.classVar.name, domain.classVar.varType)] 
    144             else: 
    145                 self.classAttribute = [] 
    146             self.metaAttributes = [(a.name, a.varType) for a in domain.getmetas().values()] 
    147  
    148             if self.receivedAttrList: 
    149                 self.chosenAttributes = [(a.name, a.varType) for a in self.receivedAttrList] 
    150                 cas = set(chosenAttributes) 
    151                 self.inputAttributes = [(a.name, a.varType) for a in domain.attributes if (a.name, a.varType) not in cas] 
    152             else: 
    153                 self.chosenAttributes = [(a.name, a.varType) for a in domain.attributes] 
    154                 self.inputAttributes = [] 
    155  
    156             metaIds = domain.getmetas().keys() 
    157             metaIds.sort() 
    158             self.allAttributes = [(attr.name, attr.varType) for attr in domain] + [(domain[i].name, domain[i].varType) for i in metaIds] 
    159         else: 
    160             self.inputAttributes = [] 
    161             self.chosenAttributes = [] 
    162             self.classAttribute = [] 
    163             self.metaAttributes = [] 
    164             self.allAttributes = [] 
    165  
    166         self.openContext("", data) 
    167  
    168         self.usedAttributes = set(self.chosenAttributes + self.classAttribute + self.metaAttributes) 
    169         self.setInputAttributes() 
    170  
    171         self.setOutput() 
    172         self.updateInterfaceState() 
    173  
    174  
    175     def setOutput(self): 
    176         if self.data: 
    177             self.applyButton.setEnabled(False) 
    178              
    179             attributes = [self.data.domain[x[0]] for x in self.chosenAttributes] 
    180             classVar = self.classAttribute and self.data.domain[self.classAttribute[0][0]] or None 
    181             domain = orange.Domain(attributes, classVar) 
    182             for meta in self.metaAttributes: 
    183                 domain.addmeta(orange.newmetaid(), self.data.domain[meta[0]]) 
    184  
    185             newdata = orange.ExampleTable(domain, self.data, filterMetas=1) 
    186             newdata.name = self.data.name 
    187             self.outdataReport = self.prepareDataReport(newdata) 
     564        available_selected = bool(self.selected_rows(self.available_attrs_view)) 
     565                                  
     566        move_attr_enabled = bool(self.selected_rows(self.available_attrs_view) or \ 
     567                                self.selected_rows(self.used_attrs_view)) 
     568        self.move_attr_button.setEnabled(move_attr_enabled) 
     569        if move_attr_enabled: 
     570            self.move_attr_button.setText(">" if available_selected else "<") 
     571             
     572        move_class_enabled = bool(len(self.selected_rows(self.available_attrs_view)) == 1 or \ 
     573                                  self.selected_rows(self.class_attrs_view)) 
     574         
     575        self.move_class_button.setEnabled(move_class_enabled) 
     576        if move_class_enabled: 
     577            self.move_class_button.setText(">" if available_selected else "<") 
     578             
     579        move_meta_enabled = bool(self.selected_rows(self.available_attrs_view) or \ 
     580                                 self.selected_rows(self.meta_attrs_view)) 
     581        self.move_meta_button.setEnabled(move_meta_enabled) 
     582        if move_meta_enabled: 
     583            self.move_meta_button.setText(">" if available_selected else "<") 
     584             
     585    def update_completer_model(self, *args): 
     586        """ This gets called when the model for available attributes changes 
     587        through either drag/drop or the left/right button actions. 
     588           
     589        """ 
     590        vars = list(self.available_attrs) 
     591        items = [var.name for var in vars] 
     592        labels = reduce(list.__add__, [v.attributes.items() for v in vars], []) 
     593        items.extend(["%s=%s" % item for item in labels]) 
     594        items.extend(reduce(list.__add__, map(list, labels), [])) 
     595         
     596        new = sorted(set(items)) 
     597        if new != self.original_completer_items: 
     598            self.original_completer_items = new 
     599            self.completer_model.setStringList(self.original_completer_items) 
     600         
     601    def update_completer_prefix(self, filter): 
     602        """ Prefixes all items in the completer model with the current 
     603        already done completion to enable the completion of multiple keywords.    
     604        """ 
     605        prefix = str(self.completer.completionPrefix()) 
     606        if not prefix.endswith(" ") and " " in prefix: 
     607            prefix, _ = prefix.rsplit(" ", 1) 
     608            items = [prefix + " " + item for item in self.original_completer_items] 
     609        else: 
     610            items = self.original_completer_items 
     611        old = map(str, self.completer_model.stringList()) 
     612         
     613        if set(old) != set(items): 
     614            self.completer_model.setStringList(items) 
     615         
     616    def commit(self): 
     617        self.update_domain_role_hints() 
     618        if self.data is not None: 
     619            attributes = list(self.used_attrs) 
     620            class_var = list(self.class_attrs) 
     621            metas = list(self.meta_attrs) 
     622             
     623            domain = Orange.data.Domain(attributes + class_var, bool(class_var)) 
     624            original_metas = dict(map(reversed, self.data.domain.getmetas().items())) 
     625             
     626            for meta in metas: 
     627                if meta in original_metas: 
     628                    mid = original_metas[meta] 
     629                else: 
     630                    mid = Orange.data.new_meta_id() 
     631                domain.addmeta(mid, meta) 
     632            newdata = Orange.data.Table(domain, self.data) 
     633            self.output_report = self.prepareDataReport(newdata) 
     634            self.output_domain = domain 
    188635            self.send("Examples", newdata) 
    189         else: 
     636            self.send("Attribute List", orange.VarList(attributes)) 
     637        else: 
     638            self.output_report = [] 
    190639            self.send("Examples", None) 
    191  
     640            self.send("Attribute List", None) 
     641     
     642    def reset(self): 
     643        if self.data is not None: 
     644            self.available_attrs[:] = [] 
     645            self.used_attrs[:] = self.data.domain.attributes 
     646            self.class_attrs[:] = [self.data.domain.class_var] if self.data.domain.class_var else [] 
     647            self.meta_attrs[:] = self.data.domain.getmetas().values() 
     648            self.update_domain_role_hints() 
     649             
    192650    def sendReport(self): 
    193651        self.reportData(self.data, "Input data") 
    194         self.reportData(self.outdataReport, "Output data") 
    195         if len(self.allAttributes) != len(self.usedAttributes): 
    196             removed = set.difference(set(self.allAttributes), self.usedAttributes) 
    197             self.reportSettings("", [("Removed", "%i (%s)" % (len(removed), ", ".join(x[0] for x in removed)))]) 
    198  
    199     def reset(self): 
    200         data = self.data 
    201         self.data = None 
    202         self.setData(data) 
    203  
    204  
    205     def disableButtons(self, *arg): 
    206         for b in arg: 
    207             b.setEnabled(False) 
    208  
    209     def setButton(self, button, dir): 
    210         button.setText(dir) 
    211         button.setEnabled(True) 
    212  
    213     # this callback is called when the user is dragging some listbox item(s) over class listbox 
    214     # and we need to check if the data contains a single item or more. if more, reject the data 
    215     def dataValidityCallback(self, ev): 
    216         ev.ignore()     # by default we will not accept items 
    217         if ev.mimeData().hasText() and self.classAttribute == []: 
    218             try: 
    219                 selectedItemIndices = eval(str(ev.mimeData().text())) 
    220                 if type(selectedItemIndices) == list and len(selectedItemIndices) == 1: 
    221                     ev.accept() 
    222                 else: 
    223                     ev.ignore() 
    224             except: 
    225                 pass 
    226              
    227     # this is called when we have dropped some items into a listbox using drag and drop and we have to update interface 
    228     def updateInterfaceAndApplyButton(self): 
    229         self.usedAttributes = set(self.chosenAttributes + self.classAttribute + self.metaAttributes)    # we used drag and drop so we have to compute which attributes are now used 
    230         self.setInputAttributes() 
    231         self.updateInterfaceState() 
    232         self.applyButton.setEnabled(True) 
    233          
    234  
    235     def updateInterfaceState(self): 
    236         if self.selectedInput: 
    237             self.setButton(self.attributesButton, ">") 
    238             self.setButton(self.metaButton, ">") 
    239             self.disableButtons(self.attributesButtonUp, self.attributesButtonDown, self.metaButtonUp, self.metaButtonDown) 
    240  
    241             if len(self.selectedInput) == 1 and self.inputAttributes[self.selectedInput[0]][1] in [orange.VarTypes.Discrete, orange.VarTypes.Continuous]: 
    242                 self.setButton(self.classButton, ">") 
    243             else: 
    244                 self.classButton.setEnabled(False) 
    245  
    246         elif self.selectedChosen: 
    247             self.setButton(self.attributesButton, "<") 
    248             self.disableButtons(self.classButton, self.metaButton, self.metaButtonUp, self.metaButtonDown) 
    249  
    250             mini, maxi = min(self.selectedChosen), max(self.selectedChosen) 
    251             cons = maxi - mini == len(self.selectedChosen) - 1 
    252             self.attributesButtonUp.setEnabled(cons and mini) 
    253             self.attributesButtonDown.setEnabled(cons and maxi < len(self.chosenAttributes)-1) 
    254  
    255         elif self.selectedClass: 
    256             self.setButton(self.classButton, "<") 
    257             self.disableButtons(self.attributesButtonUp, self.attributesButtonDown, self.metaButtonUp, self.metaButtonDown,  
    258                                 self.attributesButton, self.metaButton) 
    259  
    260         elif self.selectedMeta: 
    261             self.setButton(self.metaButton, "<") 
    262             self.disableButtons(self.attributesButton, self.classButton, self.attributesButtonDown, self.attributesButtonUp) 
    263  
    264             mini, maxi, leni = min(self.selectedMeta), max(self.selectedMeta), len(self.selectedMeta) 
    265             cons = maxi - mini == leni - 1 
    266             self.metaButtonUp.setEnabled(cons and mini) 
    267             self.metaButtonDown.setEnabled(cons and maxi < len(self.metaAttributes)-1) 
    268  
    269         else: 
    270             self.disableButtons(self.attributesButtonUp, self.attributesButtonDown, self.metaButtonUp, self.metaButtonDown,  
    271                                 self.attributesButton, self.metaButton, self.classButton) 
    272  
    273  
    274     def splitSelection(self, alist, selected): 
    275         selected.sort() 
    276  
    277         i, sele = 0, selected[0] 
    278         selList, restList = [], [] 
    279         for j, attr in enumerate(alist): 
    280             if j == sele: 
    281                 selList.append(attr) 
    282                 i += 1 
    283                 sele = i<len(selected) and selected[i] or None 
    284             else: 
    285                 restList.append(attr) 
    286         return selList, restList 
    287  
    288     def setInputAttributes(self): 
    289         self.selectedInput = [] 
     652        self.reportData(self.output_report, "Output data") 
    290653        if self.data: 
    291             self.inputAttributes = filter(lambda x:x not in self.usedAttributes, self.allAttributes) 
    292         else: 
    293             self.inputAttributes = [] 
    294         self.filterInputAttrs.setAllListItems() 
    295         self.filterInputAttrs.updateListBoxItems(callCallback = 0) 
    296          
    297         if self.data and self.inputAttributesList.count() != len(self.inputAttributes):       # the user has entered a filter - we have to remove some inputAttributes 
    298             itemsText = [str(self.inputAttributesList.item(i).text()) for i in range(self.inputAttributesList.count())] 
    299             self.inputAttributes = [ (item, self.data.domain[item].varType) for item in itemsText if item in self.data.domain] 
    300         self.updateInterfaceState() 
    301  
    302     def removeFromUsed(self, attributes): 
    303         for attr in attributes: 
    304             self.usedAttributes.remove(attr) 
    305         self.setInputAttributes() 
    306  
    307     def addToUsed(self, attributes): 
    308         self.usedAttributes.update(attributes) 
    309         self.setInputAttributes() 
    310  
    311  
    312     def onAttributesButtonClicked(self): 
    313         if self.selectedInput: 
    314             selList, restList = self.splitSelection(self.inputAttributes, self.selectedInput) 
    315             self.chosenAttributes = self.chosenAttributes + selList 
    316             self.addToUsed(selList) 
    317         else: 
    318             selList, restList = self.splitSelection(self.chosenAttributes, self.selectedChosen) 
    319             self.chosenAttributes = restList 
    320             self.removeFromUsed(selList) 
    321  
    322         self.updateInterfaceState() 
    323         self.applyButton.setEnabled(True) 
    324  
    325  
    326     def onClassButtonClicked(self): 
    327         if self.selectedInput: 
    328             selected = self.inputAttributes[self.selectedInput[0]] 
    329             if self.classAttribute: 
    330                 self.removeFromUsed(self.classAttribute) 
    331             self.addToUsed([selected]) 
    332             self.classAttribute = [selected] 
    333         else: 
    334             self.removeFromUsed(self.classAttribute) 
    335             self.selectedClass = [] 
    336             self.classAttribute = [] 
    337  
    338         self.updateInterfaceState() 
    339         self.applyButton.setEnabled(True) 
    340  
    341  
    342     def onMetaButtonClicked(self): 
    343         if self.selectedInput: 
    344             selList, restList = self.splitSelection(self.inputAttributes, self.selectedInput) 
    345             self.metaAttributes = self.metaAttributes + selList 
    346             self.addToUsed(selList) 
    347         else: 
    348             selList, restList = self.splitSelection(self.metaAttributes, self.selectedMeta) 
    349             self.metaAttributes = restList 
    350             self.removeFromUsed(selList) 
    351  
    352         self.updateInterfaceState() 
    353         self.applyButton.setEnabled(True) 
    354  
    355  
    356     def moveSelection(self, labels, selection, dir): 
    357         labs = getattr(self, labels) 
    358         sel = getattr(self, selection) 
    359         mini, maxi = min(sel), max(sel)+1 
    360         if dir == -1: 
    361             setattr(self, labels, labs[:mini-1] + labs[mini:maxi] + [labs[mini-1]] + labs[maxi:]) 
    362         else: 
    363             setattr(self, labels, labs[:mini] + [labs[maxi]] + labs[mini:maxi] + labs[maxi+1:]) 
    364         setattr(self, selection, map(lambda x:x+dir, sel)) 
    365         self.updateInterfaceState() 
    366         self.applyButton.setEnabled(True) 
    367  
    368     def onMetaButtonUpClick(self): 
    369         self.moveSelection("metaAttributes", "selectedMeta", -1) 
    370  
    371     def onMetaButtonDownClick(self): 
    372         self.moveSelection("metaAttributes", "selectedMeta", 1) 
    373  
    374     def onAttributesButtonUpClick(self): 
    375         self.moveSelection("chosenAttributes", "selectedChosen", -1) 
    376  
    377     def onAttributesButtonDownClick(self): 
    378         self.moveSelection("chosenAttributes", "selectedChosen", 1) 
    379  
    380  
    381 if __name__=="__main__": 
    382     import sys 
    383     data = orange.ExampleTable(r'../../doc/datasets/iris.tab') 
    384 #    data = orange.ExampleTable(r"E:\Development\Orange Datasets\UCI\iris.tab") 
    385     # add meta attribute 
    386     data.domain.addmeta(orange.newmetaid(), orange.StringVariable("name")) 
    387     for ex in data: 
    388         ex["name"] = str(ex.getclass()) 
    389  
    390     a=QApplication(sys.argv) 
    391     ow=OWDataDomain() 
    392     ow.show() 
    393     ow.setData(data) 
    394     a.exec_() 
    395     ow.saveSettings() 
     654            all_vars = self.data.domain.variables + self.data.domain.getmetas().values() 
     655            used_vars = self.output_domain.variables + self.output_domain.getmetas().values() 
     656            if len(all_vars) != len(used_vars): 
     657                removed = set(all_vars).difference(set(used_vars)) 
     658                self.reportSettings("", [("Removed", "%i (%s)" % (len(removed), ", ".join(x.name for x in removed)))]) 
     659     
     660if __name__ == "__main__":     
     661    app = QApplication(sys.argv) 
     662    w = OWDataDomain() 
     663#    data = Orange.data.Table("rep:dicty-express.tab") 
     664    data = Orange.data.Table("brown-selected.tab") 
     665    w.set_data(data) 
     666    w.show() 
     667    app.exec_() 
     668    w.set_data(None) 
     669    w.saveSettings() 
     670     
     671     
Note: See TracChangeset for help on using the changeset viewer.