source: orange/Orange/OrangeWidgets/Data/OWDataTable.py @ 11743:8310e8c8072f

Revision 11743:8310e8c8072f, 36.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Added an option to select individual columns in Data Table widget.

It is now possible to select columns/variables to include in output and/or
clipboard selection.

Line 
1"""
2<name>Data Table</name>
3<description>Shows data in a spreadsheet.</description>
4<icon>icons/DataTable.svg</icon>
5<priority>100</priority>
6<contact>Peter Juvan (peter.juvan@fri.uni-lj.si)</contact>
7"""
8
9import sys
10import math
11import csv
12import itertools
13
14from StringIO import StringIO
15from xml.sax.saxutils import escape
16from functools import wraps
17
18import sip
19
20import Orange
21
22from OWWidget import *
23import OWGUI
24from orngDataCaching import *
25import OWColorPalette
26
27NAME = "Data Table"
28
29DESCRIPTION = "Displays data in a spreadsheet."
30
31LONG_DESCRIPTION = """Data Table widget takes one or more data sets
32on its input and presents them in a spreadsheet format.
33"""
34
35ICON = "icons/DataTable.svg"
36
37PRIORITY = 100
38
39AUTHOR = "Peter Juvan"
40
41AUTHOR_EMAIL = "peter.juvan(@at@)fri.uni-lj.si"
42
43INPUTS = [("Data", ExampleTable, "dataset", Multiple + Default)]
44
45OUTPUTS = [("Selected Data", ExampleTable, Default),
46           ("Other Data", ExampleTable)]
47
48
49def header_text(feature, labels=None):
50    """
51    Return an header text for an `Orange.feature.Descriptor` instance
52    `feature`. If labels is not none it should be a sequence of keys into
53    `feature.attributes` to include in the header (one per line). If the
54    `feature.attribures` does not contain a value for the key the returned
55    text will include an empty line for it.
56
57    """
58    lines = [feature.name]
59    if labels is not None:
60        lines += [str(feature.attributes.get(label, ""))
61                  for label in labels]
62    return "\n".join(lines)
63
64
65def header_tooltip(feature, labels=None):
66    """
67    Return an header tooltip text for an `Orange.feature.Decriptor` instance.
68    """
69
70    if labels is None:
71        labels = feature.attributes.keys()
72
73    pairs = [(escape(key), escape(str(feature.attributes[key])))
74             for key in labels if key in feature.attributes]
75    tip = "<b>%s</b>" % escape(feature.name)
76    tip = "<br/>".join([tip] + ["%s = %s" % pair for pair in pairs])
77    return tip
78
79
80def api_qvariant(func):
81    @wraps(func)
82    def data_get(*args, **kwargs):
83        return QVariant(func(*args, **kwargs))
84    return data_get
85
86if hasattr(sip, "getapi") and sip.getapi("QVariant") > 1:
87    def api_qvariant(func):
88        return func
89
90
91class ExampleTableModel(QAbstractItemModel):
92    Attribute, ClassVar, ClassVars, Meta = range(4)
93
94    def __init__(self, examples, dist, *args):
95        QAbstractItemModel.__init__(self, *args)
96        self.examples = examples
97        self.domain = examples.domain
98        self.dist = dist
99        self.attributes = list(examples.domain.attributes)
100        self.class_var = self.examples.domain.class_var
101        self.class_vars = list(self.examples.domain.class_vars)
102        # (meta_id, attribute) list
103        self.metas = self.examples.domain.getmetas().items()
104        # Attributes/features for all table columns
105        self.all_attrs = (self.attributes +
106                          ([self.class_var] if self.class_var else []) +
107                          self.class_vars +
108                          [attr for _, attr in self.metas])
109        # Table roles for all table columns
110        self.table_roles = \
111            (([ExampleTableModel.Attribute] * len(self.attributes)) +
112             ([ExampleTableModel.ClassVar] if self.class_var else []) +
113             ([ExampleTableModel.ClassVars] * len(self.class_vars)) +
114             ([ExampleTableModel.Meta] * len(self.metas)))
115
116        # True if an feature at column i is continuous
117        self._continuous_mask = [isinstance(attr, Orange.feature.Continuous)
118                                 for attr in self.all_attrs]
119
120        self._meta_mask = [role == ExampleTableModel.Meta
121                           for role in self.table_roles]
122
123        self.cls_color = QColor(160, 160, 160)
124        self.meta_color = QColor(220, 220, 200)
125
126        role_to_color = {ExampleTableModel.Attribute: None,
127                         ExampleTableModel.ClassVar: self.cls_color,
128                         ExampleTableModel.ClassVars: self.cls_color,
129                         ExampleTableModel.Meta: self.meta_color}
130
131        self.background_colors = map(role_to_color.get, self.table_roles)
132
133        # all attribute labels (annotation) keys
134        self.attr_labels = sorted(
135            reduce(set.union,
136                   [attr.attributes for attr in self.all_attrs],
137                   set())
138        )
139
140        # text for all header items (no attr labels by default)
141        self.header_labels = [header_text(feature)
142                              for feature in self.all_attrs]
143
144        self.sorted_map = range(len(self.examples))
145
146        self._show_attr_labels = False
147        self._other_data = {}
148
149    def get_show_attr_labels(self):
150        return self._show_attr_labels
151
152    def set_show_attr_labels(self, val):
153        if self._show_attr_labels != val:
154            self.emit(SIGNAL("layoutAboutToBeChanged()"))
155            self._show_attr_labels = val
156            if val:
157                labels = self.attr_labels
158            else:
159                labels = None
160            self.header_labels = [header_text(feature, labels)
161                                  for feature in self.all_attrs]
162
163            self.emit(SIGNAL("headerDataChanged(Qt::Orientation, int, int)"),
164                      Qt.Horizontal,
165                      0,
166                      len(self.all_attrs) - 1)
167
168            self.emit(SIGNAL("layoutChanged()"))
169
170            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
171                      self.index(0, 0),
172                      self.index(len(self.examples) - 1,
173                                 len(self.all_attrs) - 1)
174                      )
175
176    show_attr_labels = pyqtProperty("bool",
177                                    fget=get_show_attr_labels,
178                                    fset=set_show_attr_labels)
179
180    @api_qvariant
181    def data(self, index, role,
182             # For optimizing out LOAD_GLOBAL byte code instructions in
183             # the item role tests (goes from 14 us to 9 us average).
184             _str=str,
185             _Qt_DisplayRole=Qt.DisplayRole,
186             _Qt_BackgroundRole=Qt.BackgroundRole,
187             _OWGUI_TableBarItem_BarRole=OWGUI.TableBarItem.BarRole,
188             _OWGUI_TableValueRole=OWGUI.TableValueRole,
189             _OWGUI_TableClassValueRole=OWGUI.TableClassValueRole,
190             _OWGUI_TableVariable=OWGUI.TableVariable,
191             # Some cached local precomputed values.
192             # All of the above roles we respond to
193             _recognizedRoles=set([Qt.DisplayRole,
194                                   Qt.BackgroundRole,
195                                   OWGUI.TableBarItem.BarRole,
196                                   OWGUI.TableValueRole,
197                                   OWGUI.TableClassValueRole,
198                                   OWGUI.TableVariable]),
199             ):
200        """
201        Return the data for `role` for an value at `index`.
202        """
203        if role not in _recognizedRoles:
204            return self._other_data.get((index.row(), index.column(), role),
205                                        None)
206
207        row, col = self.sorted_map[index.row()], index.column()
208        example, attr = self.examples[row], self.all_attrs[col]
209
210        val = example[attr]
211
212        if role == _Qt_DisplayRole:
213            return _str(val)
214        elif role == _Qt_BackgroundRole:
215            return self.background_colors[col]
216        elif role == _OWGUI_TableBarItem_BarRole and \
217                self._continuous_mask[col] and not val.isSpecial() and \
218                col < len(self.dist):
219            dist = self.dist[col]
220            return (float(val) - dist.min) / (dist.max - dist.min or 1)
221        elif role == _OWGUI_TableValueRole:
222            # The actual value instance (should it be EditRole?)
223            return val
224        elif role == _OWGUI_TableClassValueRole and self.class_var is not None:
225            # The class value for the row's example
226            return example.get_class()
227        elif role == _OWGUI_TableVariable:
228            # The variable descriptor for column
229            return attr
230
231        return None
232
233    def setData(self, index, variant, role):
234        self._other_data[index.row(), index.column(), role] = variant
235        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
236       
237    def index(self, row, col, parent=QModelIndex()):
238        return self.createIndex(row, col, 0)
239   
240    def parent(self, index):
241        return QModelIndex()
242   
243    def rowCount(self, parent=QModelIndex()):
244        if parent.isValid():
245            return 0
246        else:
247            return len(self.examples)
248
249    def columnCount(self, parent=QModelIndex()):
250        if parent.isValid():
251            return 0
252        else:
253            return len(self.all_attrs)
254
255    @api_qvariant
256    def headerData(self, section, orientation, role):
257        if orientation == Qt.Horizontal:
258            attr = self.all_attrs[section]
259            if role == Qt.DisplayRole:
260                return self.header_labels[section]
261            if role == Qt.ToolTipRole:
262                return header_tooltip(attr, self.attr_labels)
263        else:
264            if role == Qt.DisplayRole:
265                return QVariant(section + 1)
266        return QVariant()
267
268    def sort(self, column, order=Qt.AscendingOrder):
269        self.emit(SIGNAL("layoutAboutToBeChanged()"))
270        attr = self.all_attrs[column] 
271        values = [(ex[attr], i) for i, ex in enumerate(self.examples)]
272        values = sorted(values, reverse=order != Qt.AscendingOrder)
273        self.sorted_map = [v[1] for v in values]
274        self.emit(SIGNAL("layoutChanged()"))
275        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
276                  self.index(0,0),
277                  self.index(len(self.examples) - 1, len(self.all_attrs) - 1)
278                  )
279
280
281class TableViewWithCopy(QTableView):
282    def keyPressEvent(self, event):
283        if event == QKeySequence.Copy:
284            sel_model = self.selectionModel()
285            try:
286                self.copy_selection_to_clipboard(sel_model)
287            except Exception:
288                import traceback
289                traceback.print_exc(file=sys.stderr)
290        else:
291            return QTableView.keyPressEvent(self, event)
292
293    def copy_selection_to_clipboard(self, selection_model):
294        """
295        Copy table selection to the clipboard.
296        """
297        # TODO: html/rtf table
298        mime = table_selection_to_mime_data(self)
299        QApplication.clipboard().setMimeData(mime, QClipboard.Clipboard)
300
301
302def table_selection_to_mime_data(table):
303    lines = table_selection_to_list(table)
304
305    csv = lines_to_csv_string(lines, dialect="excel")
306    tsv = lines_to_csv_string(lines, dialect="excel-tab")
307
308    mime = QMimeData()
309    mime.setData("text/csv", QByteArray(csv))
310    mime.setData("text/tab-separated-values", QByteArray(tsv))
311    mime.setData("text/plain", QByteArray(tsv))
312    return mime
313
314
315def lines_to_csv_string(lines, dialect="excel"):
316    stream = StringIO()
317    writer = csv.writer(stream, dialect=dialect)
318    writer.writerows(lines)
319    return stream.getvalue()
320
321
322def table_selection_to_list(table):
323    model = table.model()
324    indexes = table.selectedIndexes()
325
326    rows = sorted(set(index.row() for index in indexes))
327    columns = sorted(set(index.column() for index in indexes))
328
329    lines = []
330    for row in rows:
331        line = []
332        for col in columns:
333            val = model.index(row, col).data(Qt.DisplayRole)
334            # TODO: use style item delegate displayText?
335            line.append(unicode(val.toString()))
336        lines.append(line)
337
338    return lines
339
340
341class OWDataTable(OWWidget):
342    settingsList = [
343        "showDistributions", "showMeta", "distColorRgb",
344        "showAttributeLabels", "autoCommit", "selectedSchemaIndex",
345        "colorByClass", "selectRows"]
346
347    def __init__(self, parent=None, signalManager = None):
348        OWWidget.__init__(self, parent, signalManager, "Data Table")
349
350        self.inputs = [("Data", ExampleTable, self.dataset, Multiple + Default)]
351        self.outputs = [("Selected Data", ExampleTable, Default), ("Other Data", ExampleTable)]
352
353        self.data = {}          # key: id, value: ExampleTable
354        self.showMetas = {}     # key: id, value: (True/False, columnList)
355        self.showMeta = 1
356        self.showAttributeLabels = 1
357        self.showDistributions = 1
358        self.distColorRgb = (220,220,220, 255)
359        self.distColor = QColor(*self.distColorRgb)
360        self.locale = QLocale()
361        self.autoCommit = False
362        self.colorSettings = None
363        self.selectedSchemaIndex = 0
364        self.colorByClass = True
365        self.selectRows = True
366
367        self.loadSettings()
368
369        # info box
370        infoBox = OWGUI.widgetBox(self.controlArea, "Info")
371        self.infoEx = OWGUI.widgetLabel(infoBox, 'No data on input.')
372        self.infoMiss = OWGUI.widgetLabel(infoBox, ' ')
373        OWGUI.widgetLabel(infoBox, ' ')
374        self.infoAttr = OWGUI.widgetLabel(infoBox, ' ')
375        self.infoMeta = OWGUI.widgetLabel(infoBox, ' ')
376        OWGUI.widgetLabel(infoBox, ' ')
377        self.infoClass = OWGUI.widgetLabel(infoBox, ' ')
378        infoBox.setMinimumWidth(200)
379        OWGUI.separator(self.controlArea)
380
381        # settings box
382        boxSettings = OWGUI.widgetBox(self.controlArea, "Settings", addSpace=True)
383        self.cbShowMeta = OWGUI.checkBox(boxSettings, self, "showMeta", 'Show meta attributes', callback = self.cbShowMetaClicked)
384        self.cbShowMeta.setEnabled(False)
385        self.cbShowAttLbls = OWGUI.checkBox(boxSettings, self, "showAttributeLabels", 'Show attribute labels (if any)', callback = self.cbShowAttLabelsClicked)
386        self.cbShowAttLbls.setEnabled(True)
387
388        box = OWGUI.widgetBox(self.controlArea, "Colors")
389        OWGUI.checkBox(box, self, "showDistributions", 'Visualize continuous values', callback = self.cbShowDistributions)
390        OWGUI.checkBox(box, self, "colorByClass", 'Color by class value', callback = self.cbShowDistributions)
391        OWGUI.button(box, self, "Set colors", self.setColors, tooltip = "Set the canvas background color and color palette for coloring continuous variables", debuggingEnabled = 0)
392
393        resizeColsBox = OWGUI.widgetBox(boxSettings, 0, "horizontal", 0)
394        OWGUI.label(resizeColsBox, self, "Resize columns: ")
395        OWGUI.toolButton(resizeColsBox, self, "+", self.increaseColWidth, tooltip = "Increase the width of the columns", width=20, height=20)
396        OWGUI.toolButton(resizeColsBox, self, "-", self.decreaseColWidth, tooltip = "Decrease the width of the columns", width=20, height=20)
397        OWGUI.rubber(resizeColsBox)
398
399        self.btnResetSort = OWGUI.button(boxSettings, self, "Restore Order of Examples", callback = self.btnResetSortClicked, tooltip = "Show examples in the same order as they appear in the file")
400
401        OWGUI.separator(self.controlArea)
402
403        box = OWGUI.widgetBox(self.controlArea, "Selection")
404        OWGUI.checkBox(box, self, "selectRows", "Select rows",
405                       callback=self.onSelectRowsChanged
406        )
407
408        cb = OWGUI.checkBox(box, self, "autoCommit", "Commit on any change", callback=self.commitIf)
409        self.sendButton = OWGUI.button(box, self, "Send selections", self.commit, default=True)
410
411        OWGUI.setStopper(self, self.sendButton, cb, "selectionChangedFlag", self.commit)
412
413        OWGUI.rubber(self.controlArea)
414
415        dlg = self.createColorDialog()
416        self.discPalette = dlg.getDiscretePalette("discPalette")
417
418        # GUI with tabs
419        self.tabs = OWGUI.tabWidget(self.mainArea)
420        self.id2table = {}  # key: widget id, value: table
421        self.table2id = {}  # key: table, value: widget id
422        self.connect(self.tabs, SIGNAL("currentChanged(QWidget*)"), self.tabClicked)
423       
424        self.selectionChangedFlag = False
425       
426
427    def createColorDialog(self):
428        c = OWColorPalette.ColorPaletteDlg(self, "Color Palette")
429        c.createDiscretePalette("discPalette", "Discrete Palette")
430        box = c.createBox("otherColors", "Other Colors")
431        c.createColorButton(box, "Default", "Default color", QColor(Qt.white))
432        c.setColorSchemas(self.colorSettings, self.selectedSchemaIndex)
433        return c
434
435    def setColors(self):
436        dlg = self.createColorDialog()
437        if dlg.exec_():
438            self.colorSettings = dlg.getColorSchemas()
439            self.selectedSchemaIndex = dlg.selectedSchemaIndex
440            self.discPalette = dlg.getDiscretePalette("discPalette")
441            self.distColor = color = dlg.getColor("Default")
442            self.distColorRgb = (color.red(), color.green(), color.red())
443
444            if self.showDistributions:
445                # Update the views
446                self.cbShowDistributions()
447
448    def increaseColWidth(self):
449        table = self.tabs.currentWidget()
450        if table:
451            for col in range(table.model().columnCount(QModelIndex())):
452                w = table.columnWidth(col)
453                table.setColumnWidth(col, w + 10)
454
455    def decreaseColWidth(self):
456        table = self.tabs.currentWidget()
457        if table:
458            for col in range(table.model().columnCount(QModelIndex())):
459                w = table.columnWidth(col)
460                minW = table.sizeHintForColumn(col)
461                table.setColumnWidth(col, max(w - 10, minW))
462
463
464    def dataset(self, data, id=None):
465        """Generates a new table and adds it to a new tab when new data arrives;
466        or hides the table and removes a tab when data==None;
467        or replaces the table when new data arrives together with already existing id."""
468        if data != None:  # can be an empty table!
469            if self.data.has_key(id):
470                # remove existing table
471                self.data.pop(id)
472                self.showMetas.pop(id)
473                self.id2table[id].hide()
474                self.tabs.removeTab(self.tabs.indexOf(self.id2table[id]))
475                self.table2id.pop(self.id2table.pop(id))
476            self.data[id] = data
477            self.showMetas[id] = (True, [])
478
479            table = TableViewWithCopy() #QTableView()
480
481            if self.selectRows:
482                table.setSelectionBehavior(QTableView.SelectRows)
483            else:
484                table.setSelectionBehavior(QTableView.SelectItems)
485
486            table.setSortingEnabled(True)
487            table.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
488            table.horizontalHeader().setMovable(True)
489            table.horizontalHeader().setClickable(True)
490            table.horizontalHeader().setSortIndicatorShown(False)
491           
492            option = table.viewOptions()
493            size = table.style().sizeFromContents(QStyle.CT_ItemViewItem, option, QSize(20, 20), table) #QSize(20, QFontMetrics(option.font).lineSpacing()), table)
494           
495            table.verticalHeader().setDefaultSectionSize(size.height() + 2) #int(size.height() * 1.25) + 2)
496
497            self.id2table[id] = table
498            self.table2id[table] = id
499            if data.name:
500                tabName = "%s " % data.name
501            else:
502                tabName = ""
503            tabName += "(" + str(id[1]) + ")"
504            if id[2] != None:
505                tabName += " [" + str(id[2]) + "]"
506            self.tabs.addTab(table, tabName)
507
508            self.progressBarInit()
509            self.setTable(table, data)
510            self.progressBarFinished()
511            self.tabs.setCurrentIndex(self.tabs.indexOf(table))
512            self.setInfo(data)
513            self.sendButton.setEnabled(not self.autoCommit)
514
515        elif self.data.has_key(id):
516            table = self.id2table[id]
517            self.data.pop(id)
518            self.showMetas.pop(id)
519            table.hide()
520            self.tabs.removeTab(self.tabs.indexOf(table))
521            self.table2id.pop(self.id2table.pop(id))
522            self.setInfo(self.data.get(self.table2id.get(self.tabs.currentWidget(),None),None))
523
524        if len(self.data) == 0:
525            self.sendButton.setEnabled(False)
526
527        self.setCbShowMeta()
528
529    def setCbShowMeta(self):
530        for ti in range(self.tabs.count()):
531            if len(self.tabs.widget(ti).model().metas)>0:
532                self.cbShowMeta.setEnabled(True)
533                break
534        else:
535            self.cbShowMeta.setEnabled(False)
536           
537    def sendReport(self):
538        qTableInstance = self.tabs.currentWidget()
539        id = self.table2id.get(qTableInstance, None)
540        data = self.data.get(id, None)
541        self.reportData(data)
542        table = self.id2table[id]
543        import OWReport
544        self.reportRaw(OWReport.reportTable(table))
545       
546       
547    # Writes data into table, adjusts the column width.
548    def setTable(self, table, data):
549        if data==None:
550            return
551        qApp.setOverrideCursor(Qt.WaitCursor)
552        vars = data.domain.variables
553        m = data.domain.getmetas(False)
554        ml = [(k, m[k]) for k in m]
555        ml.sort(lambda x,y: cmp(y[0], x[0]))
556        metas = [x[1] for x in ml]
557        metaKeys = [x[0] for x in ml]
558
559        mo = data.domain.getmetas(True).items()
560        if mo:
561            mo.sort(lambda x,y: cmp(x[1].name.lower(),y[1].name.lower()))
562            metas.append(None)
563            metaKeys.append(None)
564
565        varsMetas = vars + metas
566
567        numVars = len(data.domain.variables)
568        numMetas = len(metas)
569        numVarsMetas = numVars + numMetas
570        numEx = len(data)
571        numSpaces = int(math.log(max(numEx,1), 10))+1
572
573#        table.clear()
574        table.oldSortingIndex = -1
575        table.oldSortingOrder = 1
576#        table.setColumnCount(numVarsMetas)
577#        table.setRowCount(numEx)
578
579        dist = getCached(data, orange.DomainBasicAttrStat, (data,))
580       
581        datamodel = ExampleTableModel(data, dist, self)
582       
583#        proxy = QSortFilterProxyModel(self)
584#        proxy.setSourceModel(datamodel)
585       
586        color_schema = self.discPalette if self.colorByClass else None
587        table.setItemDelegate(OWGUI.TableBarItem(self, color=self.distColor, color_schema=color_schema) \
588                              if self.showDistributions else QStyledItemDelegate(self)) #TableItemDelegate(self, table))
589       
590        table.setModel(datamodel)
591        def p():
592            try:
593                table.updateGeometries()
594                table.viewport().update()
595            except RuntimeError:
596                pass
597       
598        size = table.verticalHeader().sectionSizeHint(0)
599        table.verticalHeader().setDefaultSectionSize(size)
600       
601        self.connect(datamodel, SIGNAL("layoutChanged()"), lambda *args: QTimer.singleShot(50, p))
602       
603        id = self.table2id.get(table, None)
604
605        # set the header (attribute names)
606
607        self.drawAttributeLabels(table)
608
609        sel_model = BlockSelectionModel(datamodel)
610        sel_model.setSelectBlocks(not self.selectRows)
611        table.setSelectionModel(sel_model)
612
613        # meta column indices
614        metacol = range(len(datamodel.all_attrs) - len(datamodel.metas),
615                        len(datamodel.all_attrs))
616        self.showMetas[id][1].extend(metacol)
617        self.connect(table.horizontalHeader(), SIGNAL("sectionClicked(int)"), self.sortByColumn)
618        self.connect(table.selectionModel(), SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.updateSelection)
619        #table.verticalHeader().setMovable(False)
620
621        qApp.restoreOverrideCursor() 
622
623    def setCornerText(self, table, text):
624        """
625        Set table corner text. As this is an ugly hack, do everything in
626        try - except blocks, as it may stop working in newer Qt.
627        """
628
629        if not hasattr(table, "btn") and not hasattr(table, "btnfailed"):
630            try:
631                btn = table.findChild(QAbstractButton)
632
633                class efc(QObject):
634                    def eventFilter(self, o, e):
635                        if (e.type() == QEvent.Paint):
636                            if isinstance(o, QAbstractButton):
637                                btn = o
638                                #paint by hand (borrowed from QTableCornerButton)
639                                opt = QStyleOptionHeader()
640                                opt.init(btn)
641                                state = QStyle.State_None;
642                                if (btn.isEnabled()):
643                                    state |= QStyle.State_Enabled;
644                                if (btn.isActiveWindow()):
645                                    state |= QStyle.State_Active;
646                                if (btn.isDown()):
647                                    state |= QStyle.State_Sunken;
648                                opt.state = state;
649                                opt.rect = btn.rect();
650                                opt.text = btn.text();
651                                opt.position = QStyleOptionHeader.OnlyOneSection;
652                                painter = QStylePainter(btn);
653                                painter.drawControl(QStyle.CE_Header, opt);
654                                return True # eat evebt
655                        return False
656               
657                table.efc = efc()
658                btn.installEventFilter(table.efc)
659
660                if sys.platform == "darwin":
661                    btn.setAttribute(Qt.WA_MacSmallSize)
662
663                table.btn = btn
664            except:
665                table.btnfailed = True
666
667        if hasattr(table, "btn"):
668            try:
669                btn = table.btn
670                btn.setText(text)
671                opt = QStyleOptionHeader()
672                opt.text = btn.text()
673                s = btn.style().sizeFromContents(QStyle.CT_HeaderSection, opt, QSize(), btn).expandedTo(QApplication.globalStrut())
674                if s.isValid():
675                    table.verticalHeader().setMinimumWidth(s.width())
676                   
677            except:
678                pass
679
680    def sortByColumn(self, index):
681        table = self.tabs.currentWidget()
682        table.horizontalHeader().setSortIndicatorShown(1)
683        header = table.horizontalHeader()
684        if index == table.oldSortingIndex:
685            order = table.oldSortingOrder == Qt.AscendingOrder and Qt.DescendingOrder or Qt.AscendingOrder
686        else:
687            order = Qt.AscendingOrder
688        table.sortByColumn(index, order)
689        table.oldSortingIndex = index
690        table.oldSortingOrder = order
691        #header.setSortIndicator(index, order)
692
693    def tabClicked(self, qTableInstance):
694        """Updates the info box and showMetas checkbox when a tab is clicked.
695        """
696        id = self.table2id.get(qTableInstance,None)
697        self.setInfo(self.data.get(id,None))
698        show_col = self.showMetas.get(id,None)
699        if show_col:
700            self.cbShowMeta.setChecked(show_col[0])
701            self.cbShowMeta.setEnabled(len(show_col[1])>0)
702        self.updateSelection()
703
704    def cbShowMetaClicked(self):
705        table = self.tabs.currentWidget()
706        id = self.table2id.get(table, None)
707        if self.showMetas.has_key(id):
708            show,col = self.showMetas[id]
709            self.showMetas[id] = (not show,col)
710        if show:
711            for c in col:
712                table.hideColumn(c)
713        else:
714            for c in col:
715                table.showColumn(c)
716                table.resizeColumnToContents(c)
717
718    def drawAttributeLabels(self, table):
719#        table.setHorizontalHeaderLabels(table.variableNames)
720        table.model().show_attr_labels = bool(self.showAttributeLabels)
721        if self.showAttributeLabels:
722            labelnames = set()
723            for a in table.model().examples.domain:
724                labelnames.update(a.attributes.keys())
725            labelnames = sorted(list(labelnames))
726#            if len(labelnames):
727#                table.setHorizontalHeaderLabels([table.variableNames[i] + "\n" + "\n".join(["%s" % a.attributes.get(lab, "") for lab in labelnames]) for (i, a) in enumerate(table.data.domain.attributes)])
728            self.setCornerText(table, "\n".join([""] + labelnames))
729        else:
730            self.setCornerText(table, "")
731        table.repaint()
732
733    def cbShowAttLabelsClicked(self):
734        for table in self.table2id.keys():
735            self.drawAttributeLabels(table)
736
737    def cbShowDistributions(self):
738        for ti in range(self.tabs.count()):
739            color_schema = self.discPalette if self.colorByClass else None
740            delegate = OWGUI.TableBarItem(self, color=self.distColor,
741                                          color_schema=color_schema) \
742                       if self.showDistributions else QStyledItemDelegate(self)
743            self.tabs.widget(ti).setItemDelegate(delegate)
744        tab = self.tabs.currentWidget()
745        if tab:
746            tab.reset()
747
748    # show data in the default order
749    def btnResetSortClicked(self):
750        table = self.tabs.currentWidget()
751        if table:
752            id = self.table2id[table]
753            data = self.data[id]
754            table.horizontalHeader().setSortIndicatorShown(False)
755            self.progressBarInit()
756            self.setTable(table, data)
757            self.progressBarFinished()
758
759    def setInfo(self, data):
760        """Updates data info.
761        """
762        def sp(l, capitalize=False):
763            n = len(l)
764            if n == 0:
765                if capitalize:
766                    return "No", "s"
767                else:
768                    return "no", "s"
769            elif n == 1:
770                return str(n), ''
771            else:
772                return str(n), 's'
773
774        if data == None:
775            self.infoEx.setText('No data on input.')
776            self.infoMiss.setText('')
777            self.infoAttr.setText('')
778            self.infoMeta.setText('')
779            self.infoClass.setText('')
780        else:
781            self.infoEx.setText("%s example%s," % sp(data))
782            missData = orange.Preprocessor_takeMissing(data)
783            self.infoMiss.setText('%s (%.1f%s) with missing values.' % (len(missData), len(data) and 100.*len(missData)/len(data), "%"))
784            self.infoAttr.setText("%s attribute%s," % sp(data.domain.attributes,True))
785            self.infoMeta.setText("%s meta attribute%s." % sp(data.domain.getmetas()))
786            if data.domain.classVar:
787                if data.domain.classVar.varType == orange.VarTypes.Discrete:
788                    self.infoClass.setText('Discrete class with %s value%s.' % sp(data.domain.classVar.values))
789                elif data.domain.classVar.varType == orange.VarTypes.Continuous:
790                    self.infoClass.setText('Continuous class.')
791                else:
792                    self.infoClass.setText("Class is neither discrete nor continuous.")
793            else:
794                self.infoClass.setText('Classless domain.')
795
796    def onSelectRowsChanged(self):
797        for table in self.table2id.keys():
798            selection_model = table.selectionModel()
799            selection_model.setSelectBlocks(not self.selectRows)
800            if self.selectRows:
801                table.setSelectionBehavior(QTableView.SelectRows)
802                # Expand the current selection to row selection.
803                selection_model.select(
804                    selection_model.selection(),
805                    QItemSelectionModel.Select | QItemSelectionModel.Rows
806                )
807            else:
808                table.setSelectionBehavior(QTableView.SelectItems)
809
810    def updateSelection(self, *args):
811        self.sendButton.setEnabled(bool(self.getCurrentSelection()) and
812                                   not self.autoCommit)
813        self.commitIf()
814
815    def getCurrentSelection(self):
816        table = self.tabs.currentWidget()
817        if table and table.model():
818            model = table.model()
819            new = table.selectionModel().selectedIndexes()
820            return sorted(set([model.sorted_map[ind.row()] for ind in new]))
821
822    def getCurrentColumnSelection(self):
823        view = self.tabs.currentWidget()
824        if view and view.model():
825            model = view.model()
826            indexes = view.selectionModel().selectedIndexes()
827            cols = sorted(set(ind.column() for ind in indexes))
828            roles = list(enumerate(model.table_roles))
829            roles = [(col, role) for col, role in roles if col in cols]
830
831            def select(role):
832                return [i for i, r in roles if r == role]
833
834            attrs = select(ExampleTableModel.Attribute)
835            class_var = select(ExampleTableModel.ClassVar)
836            class_vars = select(ExampleTableModel.ClassVars)
837            metas = select(ExampleTableModel.Meta)
838
839            return attrs, class_var, class_vars, metas
840        else:
841            return [], [], [], []
842
843    def getOutputDomain(self):
844        view = self.tabs.currentWidget()
845        if view and view.model():
846            model = view.model()
847            attrs, class_var, class_vars, metas = \
848                self.getCurrentColumnSelection()
849
850            domain = model.examples.domain
851            attrs = [domain[i] for i in attrs]
852            class_var = [domain[i] for i in class_var]
853            meta_offset = len(model.all_attrs) - len(model.metas)
854            class_vars_offset = meta_offset - len(model.class_vars)
855
856            class_vars = [domain.class_vars[i - class_vars_offset]
857                          for i in class_vars]
858            # map column indices into (meta_id, attr) tuples
859            metas = [model.metas[i - meta_offset] for i in metas]
860            if class_var:
861                class_var = class_var[0]
862            else:
863                class_var = None
864
865            domain = Orange.data.Domain(
866                attrs, class_var, class_vars=class_vars
867            )
868            domain.addmetas(dict(metas))
869
870            return domain
871
872    def commitIf(self):
873        if self.autoCommit:
874            self.commit()
875        else:
876            self.selectionChangedFlag = True
877
878    def commit(self):
879        selected = other = None
880        table = self.tabs.currentWidget()
881        if table and table.model():
882            model = table.model()
883            selected = set(self.getCurrentSelection())
884            selection = [1 if i in selected else 0 for
885                         i in range(len(model.examples))]
886
887            selected = model.examples.select(selection)
888            other = model.examples.select(selection, 0)
889
890            if not self.selectRows:
891                domain = self.getOutputDomain()
892                selected = Orange.data.Table(domain, selected)
893                other = Orange.data.Table(domain, other)
894
895            selected = selected if len(selected) > 0 else None
896            other = other if len(other) > 0 else None
897
898        self.send("Selected Data", selected)
899        self.send("Other Data", other)
900
901        self.selectionChangedFlag = False
902
903
904class BlockSelectionModel(QItemSelectionModel):
905    def __init__(self, *args, **kwargs):
906        super(QItemSelectionModel, self).__init__(*args, **kwargs)
907        self._selectBlocks = True
908
909    def select(self, selection, flags):
910        if isinstance(selection, QModelIndex):
911            selection = QItemSelection(selection, selection)
912
913        model = self.model()
914        indexes = self.selectedIndexes()
915
916        rows = set(ind.row() for ind in indexes)
917        cols = set(ind.column() for ind in indexes)
918
919        if flags & QItemSelectionModel.Select and \
920                not flags & QItemSelectionModel.Clear and self._selectBlocks:
921            indexes = selection.indexes()
922            sel_rows = set(ind.row() for ind in indexes).union(rows)
923            sel_cols = set(ind.column() for ind in indexes).union(cols)
924
925            selection = QItemSelection()
926
927            for r_start, r_end in ranges(sorted(sel_rows)):
928                for c_start, c_end in ranges(sorted(sel_cols)):
929                    top_left = model.index(r_start, c_start)
930                    bottom_right = model.index(r_end - 1, c_end - 1)
931                    selection.select(top_left, bottom_right)
932
933        QItemSelectionModel.select(self, selection, flags)
934
935    def selectBlock(self):
936        return self._selectBlocks
937
938    def setSelectBlocks(self, state):
939        self._selectBlocks = state
940
941
942def ranges(indices):
943    g = itertools.groupby(enumerate(indices),
944                          key=lambda t: t[1] - t[0])
945    for _, range_ind in g:
946        range_ind = list(range_ind)
947        _, start = range_ind[0]
948        _, end = range_ind[-1]
949        yield start, end + 1
950
951
952def test():
953    a = QApplication(sys.argv)
954    ow = OWDataTable()
955
956    d1 = orange.ExampleTable("auto-mpg")
957    d2 = orange.ExampleTable("sponge.tab")
958    d3 = orange.ExampleTable("wpbc.csv")
959    d4 = orange.ExampleTable("adult_sample.tab")
960    d5 = orange.ExampleTable("wine.tab")
961
962    ow.show()
963    ow.dataset(d1, "auto-mpg")
964    ow.dataset(d2, "sponge")
965    ow.dataset(d3, "wpbc")
966    ow.dataset(d4, "adult_sample")
967    ow.dataset(d5, "wine")
968    a.exec_()
969    ow.saveSettings()
970
971
972if __name__ == "__main__":
973    test()
Note: See TracBrowser for help on using the repository browser.