source: orange/Orange/OrangeWidgets/Data/OWDataTable.py @ 11744:d1fa1576ed42

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

Fixed 'Show meta attributes' save/restore when switching tabs.

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(int)"),
423                     self.onCurrentTabChanged)
424
425        self.selectionChangedFlag = False
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 onCurrentTabChanged(self, index):
694        """
695        Updates the info box and showMetas checkbox when a tab is clicked.
696        """
697        if index == -1:
698            self.setInfo(None)
699            return
700
701        tableview = self.tabs.widget(index)
702        id = self.table2id.get(tableview, None)
703        self.setInfo(self.data.get(id, None))
704        show_col = self.showMetas.get(id, None)
705        if show_col is not None:
706            self.cbShowMeta.setChecked(show_col[0])
707            self.cbShowMeta.setEnabled(len(show_col[1]) > 0)
708        self.updateSelection()
709
710    def cbShowMetaClicked(self):
711        table = self.tabs.currentWidget()
712        id = self.table2id.get(table, None)
713        if id in self.showMetas:
714            _, meta_cols = self.showMetas[id]
715            self.showMetas[id] = (self.showMeta, meta_cols)
716            for c in meta_cols:
717                table.setColumnHidden(c, not self.showMeta)
718
719    def drawAttributeLabels(self, table):
720#        table.setHorizontalHeaderLabels(table.variableNames)
721        table.model().show_attr_labels = bool(self.showAttributeLabels)
722        if self.showAttributeLabels:
723            labelnames = set()
724            for a in table.model().examples.domain:
725                labelnames.update(a.attributes.keys())
726            labelnames = sorted(list(labelnames))
727#            if len(labelnames):
728#                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)])
729            self.setCornerText(table, "\n".join([""] + labelnames))
730        else:
731            self.setCornerText(table, "")
732        table.repaint()
733
734    def cbShowAttLabelsClicked(self):
735        for table in self.table2id.keys():
736            self.drawAttributeLabels(table)
737
738    def cbShowDistributions(self):
739        for ti in range(self.tabs.count()):
740            color_schema = self.discPalette if self.colorByClass else None
741            delegate = OWGUI.TableBarItem(self, color=self.distColor,
742                                          color_schema=color_schema) \
743                       if self.showDistributions else QStyledItemDelegate(self)
744            self.tabs.widget(ti).setItemDelegate(delegate)
745        tab = self.tabs.currentWidget()
746        if tab:
747            tab.reset()
748
749    # show data in the default order
750    def btnResetSortClicked(self):
751        table = self.tabs.currentWidget()
752        if table:
753            id = self.table2id[table]
754            data = self.data[id]
755            table.horizontalHeader().setSortIndicatorShown(False)
756            self.progressBarInit()
757            self.setTable(table, data)
758            self.progressBarFinished()
759
760    def setInfo(self, data):
761        """Updates data info.
762        """
763        def sp(l, capitalize=False):
764            n = len(l)
765            if n == 0:
766                if capitalize:
767                    return "No", "s"
768                else:
769                    return "no", "s"
770            elif n == 1:
771                return str(n), ''
772            else:
773                return str(n), 's'
774
775        if data == None:
776            self.infoEx.setText('No data on input.')
777            self.infoMiss.setText('')
778            self.infoAttr.setText('')
779            self.infoMeta.setText('')
780            self.infoClass.setText('')
781        else:
782            self.infoEx.setText("%s example%s," % sp(data))
783            missData = orange.Preprocessor_takeMissing(data)
784            self.infoMiss.setText('%s (%.1f%s) with missing values.' % (len(missData), len(data) and 100.*len(missData)/len(data), "%"))
785            self.infoAttr.setText("%s attribute%s," % sp(data.domain.attributes,True))
786            self.infoMeta.setText("%s meta attribute%s." % sp(data.domain.getmetas()))
787            if data.domain.classVar:
788                if data.domain.classVar.varType == orange.VarTypes.Discrete:
789                    self.infoClass.setText('Discrete class with %s value%s.' % sp(data.domain.classVar.values))
790                elif data.domain.classVar.varType == orange.VarTypes.Continuous:
791                    self.infoClass.setText('Continuous class.')
792                else:
793                    self.infoClass.setText("Class is neither discrete nor continuous.")
794            else:
795                self.infoClass.setText('Classless domain.')
796
797    def onSelectRowsChanged(self):
798        for table in self.table2id.keys():
799            selection_model = table.selectionModel()
800            selection_model.setSelectBlocks(not self.selectRows)
801            if self.selectRows:
802                table.setSelectionBehavior(QTableView.SelectRows)
803                # Expand the current selection to row selection.
804                selection_model.select(
805                    selection_model.selection(),
806                    QItemSelectionModel.Select | QItemSelectionModel.Rows
807                )
808            else:
809                table.setSelectionBehavior(QTableView.SelectItems)
810
811    def updateSelection(self, *args):
812        self.sendButton.setEnabled(bool(self.getCurrentSelection()) and
813                                   not self.autoCommit)
814        self.commitIf()
815
816    def getCurrentSelection(self):
817        table = self.tabs.currentWidget()
818        if table and table.model():
819            model = table.model()
820            new = table.selectionModel().selectedIndexes()
821            return sorted(set([model.sorted_map[ind.row()] for ind in new]))
822
823    def getCurrentColumnSelection(self):
824        view = self.tabs.currentWidget()
825        if view and view.model():
826            model = view.model()
827            indexes = view.selectionModel().selectedIndexes()
828            cols = sorted(set(ind.column() for ind in indexes))
829            roles = list(enumerate(model.table_roles))
830            roles = [(col, role) for col, role in roles if col in cols]
831
832            def select(role):
833                return [i for i, r in roles if r == role]
834
835            attrs = select(ExampleTableModel.Attribute)
836            class_var = select(ExampleTableModel.ClassVar)
837            class_vars = select(ExampleTableModel.ClassVars)
838            metas = select(ExampleTableModel.Meta)
839
840            return attrs, class_var, class_vars, metas
841        else:
842            return [], [], [], []
843
844    def getOutputDomain(self):
845        view = self.tabs.currentWidget()
846        if view and view.model():
847            model = view.model()
848            attrs, class_var, class_vars, metas = \
849                self.getCurrentColumnSelection()
850
851            domain = model.examples.domain
852            attrs = [domain[i] for i in attrs]
853            class_var = [domain[i] for i in class_var]
854            meta_offset = len(model.all_attrs) - len(model.metas)
855            class_vars_offset = meta_offset - len(model.class_vars)
856
857            class_vars = [domain.class_vars[i - class_vars_offset]
858                          for i in class_vars]
859            # map column indices into (meta_id, attr) tuples
860            metas = [model.metas[i - meta_offset] for i in metas]
861            if class_var:
862                class_var = class_var[0]
863            else:
864                class_var = None
865
866            domain = Orange.data.Domain(
867                attrs, class_var, class_vars=class_vars
868            )
869            domain.addmetas(dict(metas))
870
871            return domain
872
873    def commitIf(self):
874        if self.autoCommit:
875            self.commit()
876        else:
877            self.selectionChangedFlag = True
878
879    def commit(self):
880        selected = other = None
881        table = self.tabs.currentWidget()
882        if table and table.model():
883            model = table.model()
884            selected = set(self.getCurrentSelection())
885            selection = [1 if i in selected else 0 for
886                         i in range(len(model.examples))]
887
888            selected = model.examples.select(selection)
889            other = model.examples.select(selection, 0)
890
891            if not self.selectRows:
892                domain = self.getOutputDomain()
893                selected = Orange.data.Table(domain, selected)
894                other = Orange.data.Table(domain, other)
895
896            selected = selected if len(selected) > 0 else None
897            other = other if len(other) > 0 else None
898
899        self.send("Selected Data", selected)
900        self.send("Other Data", other)
901
902        self.selectionChangedFlag = False
903
904
905class BlockSelectionModel(QItemSelectionModel):
906    def __init__(self, *args, **kwargs):
907        super(QItemSelectionModel, self).__init__(*args, **kwargs)
908        self._selectBlocks = True
909
910    def select(self, selection, flags):
911        if isinstance(selection, QModelIndex):
912            selection = QItemSelection(selection, selection)
913
914        model = self.model()
915        indexes = self.selectedIndexes()
916
917        rows = set(ind.row() for ind in indexes)
918        cols = set(ind.column() for ind in indexes)
919
920        if flags & QItemSelectionModel.Select and \
921                not flags & QItemSelectionModel.Clear and self._selectBlocks:
922            indexes = selection.indexes()
923            sel_rows = set(ind.row() for ind in indexes).union(rows)
924            sel_cols = set(ind.column() for ind in indexes).union(cols)
925
926            selection = QItemSelection()
927
928            for r_start, r_end in ranges(sorted(sel_rows)):
929                for c_start, c_end in ranges(sorted(sel_cols)):
930                    top_left = model.index(r_start, c_start)
931                    bottom_right = model.index(r_end - 1, c_end - 1)
932                    selection.select(top_left, bottom_right)
933
934        QItemSelectionModel.select(self, selection, flags)
935
936    def selectBlock(self):
937        return self._selectBlocks
938
939    def setSelectBlocks(self, state):
940        self._selectBlocks = state
941
942
943def ranges(indices):
944    g = itertools.groupby(enumerate(indices),
945                          key=lambda t: t[1] - t[0])
946    for _, range_ind in g:
947        range_ind = list(range_ind)
948        _, start = range_ind[0]
949        _, end = range_ind[-1]
950        yield start, end + 1
951
952
953def test():
954    a = QApplication(sys.argv)
955    ow = OWDataTable()
956
957    d1 = orange.ExampleTable("auto-mpg")
958    d2 = orange.ExampleTable("sponge.tab")
959    d3 = orange.ExampleTable("wpbc.csv")
960    d4 = orange.ExampleTable("adult_sample.tab")
961    d5 = orange.ExampleTable("wine.tab")
962
963    ow.show()
964    ow.dataset(d1, "auto-mpg")
965    ow.dataset(d2, "sponge")
966    ow.dataset(d3, "wpbc")
967    ow.dataset(d4, "adult_sample")
968    ow.dataset(d5, "wine")
969    a.exec_()
970    ow.saveSettings()
971
972
973if __name__ == "__main__":
974    test()
Note: See TracBrowser for help on using the repository browser.