source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11827:746f5c391a27

Revision 11827:746f5c391a27, 37.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 3 months ago (diff)

'Paint Data' widget code cleanup.

Line 
1import itertools
2import unicodedata
3import copy
4import random
5
6
7from PyQt4 import QtCore
8from PyQt4.QtCore import pyqtSignal as Signal
9
10import Orange
11
12import OWToolbars
13import OWColorPalette
14from OWWidget import *
15from OWGraph import *
16from OWItemModels import VariableListModel, PyListModel, ModelActionsWidget
17
18
19NAME = "Paint Data"
20DESCRIPTION = """
21Paints the data on a 2D plane. Place individual data
22points or use brush to paint a larger data sets.
23"""
24LONG_DESCRIPTION = ""
25ICON = "icons/PaintData.svg"
26PRIORITY = 40
27OUTPUTS = [("Data", Orange.data.Table, )]
28
29dir = OWToolbars.dir
30icon_magnet = os.path.join(dir, "magnet_64px.png")
31icon_jitter = os.path.join(dir, "jitter_64px.png")
32icon_brush = os.path.join(dir, "brush_64px.png")
33icon_put = os.path.join(dir, "put_64px.png")
34icon_select = os.path.join(dir, "select-transparent_42px.png")
35icon_lasso = os.path.join(dir, "lasso-transparent_42px.png")
36#icon_remove = os.path.join(dir, "remove.svg")
37
38
39class PaintDataGraph(OWGraph):
40    def __init__(self, *args, **kwargs):
41        OWGraph.__init__(self, *args, **kwargs)
42        self.toolPixmap = QPixmap()
43
44    def setToolPixmap(self, pixmap):
45        self.toolPixmap = pixmap
46        self.update()
47
48    def setData(self, data, attr1, attr2):
49        """
50        Set the data to display.
51
52        :param data: data
53        :param attr1: attr for X axis
54        :param attr2: attr for Y axis
55        """
56        OWGraph.setData(self, data)
57        self.data = data
58        self.attr1 = attr1
59        self.attr2 = attr2
60        self.updateGraph()
61
62    def updateGraph(self, dataInterval=None):
63        if dataInterval:
64            start, end = dataInterval
65            data = self.data[start:end]
66        else:
67            self.removeDrawingCurves()
68            data = self.data
69
70        domain = self.data.domain
71        clsValues = domain.class_var.values if domain.class_var else [0]
72
73        palette = ColorPaletteGenerator(len(clsValues))
74        for i, cls in enumerate(clsValues):
75            x = [float(ex[self.attr1]) for ex in data if ex.getclass() == cls]
76            y = [float(ex[self.attr2]) for ex in data if ex.getclass() == cls]
77            self.addCurve("data points", xData=x, yData=y,
78                          brushColor=palette[i], penColor=palette[i])
79        self.replot()
80
81    def drawCanvas(self, painter):
82        OWGraph.drawCanvas(self, painter)
83        pixmap = self.toolPixmap
84        if not pixmap.isNull():
85            painter.drawPixmap(0, 0, pixmap)
86
87
88class DataTool(QObject):
89    """
90    A base class for data tools that operate on PaintDataGraph
91    widget by installing itself as its event filter.
92
93    """
94    editing = Signal()
95    editingFinished = Signal()
96
97    cursor = Qt.ArrowCursor
98
99    class optionsWidget(QFrame):
100        """
101        An options (parameters) widget for the tool (this will
102        be put in the "Options" box in the main OWPaintData widget
103        when this tool is selected.
104
105        """
106        def __init__(self, tool, parent=None):
107            QFrame.__init__(self, parent)
108            self.tool = tool
109
110    def __init__(self, graph, parent=None):
111        QObject.__init__(self, parent)
112        self.setGraph(graph)
113
114    def setGraph(self, graph):
115        """
116        Install this tool to operate on ``graph``.
117
118        If another tool is already operating on the graph it will first
119        be removed.
120
121        """
122        self.graph = graph
123        if graph:
124            installed = getattr(graph, "_data_tool_event_filter", None)
125            if installed:
126                self.graph.canvas().removeEventFilter(installed)
127                installed.removed()
128            self.graph.canvas().setMouseTracking(True)
129            self.graph.canvas().installEventFilter(self)
130            self.graph._data_tool_event_filter = self
131            self.graph.setToolPixmap(QPixmap())
132            self.graph.setCursor(self.cursor)
133            self.graph.replot()
134            self.installed()
135
136    def removed(self):
137        """Called when the tool is removed from a graph.
138        """
139        pass
140
141    def installed(self):
142        """Called when the tool is installed on a graph.
143        """
144        pass
145
146    def eventFilter(self, obj, event):
147        if event.type() == QEvent.MouseButtonPress:
148            return self.mousePressEvent(event)
149        elif event.type() == QEvent.MouseButtonRelease:
150            return self.mouseReleaseEvent(event)
151        elif event.type() == QEvent.MouseButtonDblClick:
152            return self.mouseDoubleClickEvent(event)
153        elif event.type() == QEvent.MouseMove:
154            return self.mouseMoveEvent(event)
155        elif event.type() == QEvent.Paint:
156            return self.paintEvent(event)
157        elif event.type() == QEvent.Leave:
158            return self.leaveEvent(event)
159        elif event.type() == QEvent.Enter:
160            return self.enterEvent(event)
161        return False
162
163    # These are actually event filters (note the return values)
164    def paintEvent(self, event):
165        return False
166
167    def mousePressEvent(self, event):
168        return False
169
170    def mouseMoveEvent(self, event):
171        return False
172
173    def mouseReleaseEvent(self, event):
174        return False
175
176    def mouseDoubleClickEvent(self, event):
177        return False
178
179    def enterEvent(self, event):
180        return False
181
182    def leaveEvent(self, event):
183        return False
184
185    def keyPressEvent(self, event):
186        return False
187
188    def transform(self, point):
189        x = self.graph.transform(QwtPlot.xBottom, point.x())
190        y = self.graph.transform(QwtPlot.yLeft, point.y())
191        return QPoint(x, y)
192
193    def invTransform(self, point):
194        x = self.graph.invTransform(QwtPlot.xBottom, point.x())
195        y = self.graph.invTransform(QwtPlot.yLeft, point.y())
196        return QPointF(x, y)
197
198    def attributes(self):
199        return self.graph.attr1, self.graph.attr2
200
201    def dataTransform(self, *args):
202        pass
203
204
205class GraphSelections(QObject):
206    selectionRegionAdded = Signal(int, QPainterPath)
207    selectionRegionUpdated = Signal(int, QPainterPath)
208    selectionRegionRemoved = Signal(int, QPainterPath)
209
210    selectionRegionMoveStarted = Signal(int, QPointF, QPainterPath)
211    selectionRegionMoved = Signal(int, QPointF, QPainterPath)
212    selectionRegionMoveFinished = Signal(int, QPointF, QPainterPath)
213    selectionGeometryChanged = Signal()
214
215    def __init__(self, parent, movable=True, multipleSelection=False):
216        QObject.__init__(self, parent)
217        self.selection = []
218        self.movable = movable
219        self.multipleSelection = multipleSelection
220
221        self._moving_index, self._moving_pos, self._selection_region = \
222            (-1, QPointF(), (QPointF(), QPointF()))
223
224    def getPos(self, event):
225        graph = self.parent()
226        pos = event.pos()
227        x = graph.invTransform(QwtPlot.xBottom, pos.x())
228        y = graph.invTransform(QwtPlot.yLeft, pos.y())
229        return QPointF(x, y)
230
231    def toPath(self, region):
232        path = QPainterPath()
233        if isinstance(region, QRectF) or isinstance(region, QRect):
234            path.addRect(region.normalized())
235        elif isinstance(region, tuple):
236            path.addRect(QRectF(*region).normalized())
237        elif isinstance(region, list):
238            path.addPolygon(QPolygonF(region + [region[0]]))
239        return path
240
241    def addSelectionRegion(self, region):
242        self.selection.append(region)
243        self.selectionRegionAdded.emit(
244            len(self.selection) - 1, self.toPath(region))
245
246    def setSelectionRegion(self, index, region):
247        self.selection[index] = region
248        self.selectionRegionUpdated.emit(index, self.toPath(region))
249
250    def clearSelection(self):
251        for i, region in enumerate(self.selection):
252            self.selectionRegionRemoved.emit(i, self.toPath(region))
253        self.selection = []
254
255    def start(self, event):
256        pos = self.getPos(event)
257        index = self.regionAt(event)
258        if index == -1 or not self.movable:
259            if event.modifiers() & Qt.ControlModifier and \
260                    self.multipleSelection:
261                self.addSelectionRegion((pos, pos))
262            else:
263                self.clearSelection()
264                self.addSelectionRegion((pos, pos))
265            self._moving_index = -1
266        else:
267            self._moving_index, self._moving_pos, self._selection_region = \
268                (index, pos, self.selection[index])
269
270            self.selectionRegionMoveStarted.emit(
271                index, pos, self.toPath(self.selection[index])
272            )
273        self.selectionGeometryChanged.emit()
274
275    def update(self, event):
276        pos = self.getPos(event)
277        index = self._moving_index
278        if index == -1:
279            self.selection[-1] = self.selection[-1][:-1] + (pos,)
280            self.selectionRegionUpdated.emit(
281                len(self.selection) - 1, self.toPath(self.selection[-1]))
282
283        else:
284            diff = self._moving_pos - pos
285            self.selection[index] = \
286                tuple([p - diff for p in self._selection_region])
287            self.selectionRegionMoved.emit(
288                index, pos, self.toPath(self.selection[index])
289            )
290        self.selectionGeometryChanged.emit()
291
292    def end(self, event):
293        self.update(event)
294        if self._moving_index != -1:
295            self.selectionRegionMoveFinished.emit(
296                self._moving_index, self.getPos(event),
297                self.toPath(self.selection[self._moving_index])
298            )
299        self._moving_index = -1
300
301    def regionAt(self, event):
302        pos = self.getPos(event)
303        for i, region in enumerate(self.selection):
304            if self.toPath(region).contains(pos):
305                return i
306        return -1
307
308    def testSelection(self, data):
309        data = numpy.asarray(data)
310        path = QPainterPath()
311        for region in self.selection:
312            path = path.united(self.toPath(region))
313        def test(point):
314            return path.contains(QPointF(point[0], point[1]))
315        test = numpy.apply_along_axis(test, 1, data)
316        return test
317
318    def __nonzero__(self):
319        return bool(self.selection)
320
321    def __bool__(self):
322        return bool(self.selection)
323
324    def path(self):
325        path = QPainterPath()
326        for region in self.selection:
327            path = path.united(self.toPath(region))
328        return path
329
330    def qTransform(self):
331        graph = self.parent()
332        invTransform = graph.invTransform
333        sx = invTransform(QwtPlot.xBottom, 1) - invTransform(QwtPlot.xBottom, 0)
334        sy = invTransform(QwtPlot.yLeft, 1) - invTransform(QwtPlot.yLeft, 0)
335        dx = invTransform(QwtPlot.xBottom, 0)
336        dy = invTransform(QwtPlot.yLeft, 0)
337        return QTransform(sx, 0.0, 0.0, sy, dx, dy)
338
339
340class SelectTool(DataTool):
341    class optionsWidget(QFrame):
342        def __init__(self, tool, parent=None):
343            QFrame.__init__(self, parent)
344            self.tool = tool
345            layout = QHBoxLayout()
346            delete = QToolButton(self, text="Delete",
347                                 toolTip="Delete selected instances")
348            delete.clicked.connect(self.tool.deleteSelected)
349
350            layout.addWidget(delete)
351            layout.addStretch(10)
352            self.setLayout(layout)
353
354    def __init__(self, graph, parent=None, graphSelection=None):
355        DataTool.__init__(self, graph, parent)
356        if graphSelection is None:
357            self.selection = GraphSelections(graph)
358        else:
359            self.selection = graphSelection
360
361        self.pen = QPen(Qt.black, 1, Qt.DashDotLine)
362        self.pen.setCosmetic(True)
363        self.pen.setJoinStyle(Qt.RoundJoin)
364        self.pen.setCapStyle(Qt.RoundCap)
365
366        self.selection.selectionRegionMoveStarted.connect(self.onMoveStarted)
367        self.selection.selectionRegionMoved.connect(self.onMove)
368        self.selection.selectionRegionMoveFinished.connect(self.onMoveFinished)
369        self.selection.selectionRegionUpdated.connect(
370                self.invalidateMoveSelection)
371
372        self._validMoveSelection = False
373        self._moving = None
374
375    def setGraph(self, graph):
376        DataTool.setGraph(self, graph)
377        if graph and hasattr(self, "selection"):
378            self.selection.setParent(graph)
379
380    def installed(self):
381        DataTool.installed(self)
382        self.invalidateMoveSelection()
383
384    def paintEvent(self, event):
385        if self.selection:
386            pixmap = QPixmap(self.graph.canvas().size())
387            pixmap.fill(QColor(255, 255, 255, 0))
388            painter = QPainter(pixmap)
389            painter.setRenderHints(QPainter.Antialiasing)
390            inverted, singular = self.selection.qTransform().inverted()
391            painter.setPen(self.pen)
392
393            painter.setTransform(inverted)
394            for region in self.selection.selection:
395                painter.drawPath(self.selection.toPath(region))
396            painter.end()
397            self.graph.setToolPixmap(pixmap)
398        return False
399
400    def mousePressEvent(self, event):
401        if event.button() == Qt.LeftButton:
402            self.selection.start(event)
403            self.graph.replot()
404        return True
405
406    def mouseMoveEvent(self, event):
407        index = self.selection.regionAt(event)
408        if index != -1:
409            self.graph.canvas().setCursor(Qt.OpenHandCursor)
410        else:
411            self.graph.canvas().setCursor(self.graph._cursor)
412
413        if event.buttons() & Qt.LeftButton:
414            self.selection.update(event)
415            self.graph.replot()
416        return True
417
418    def mouseReleaseEvent(self, event):
419        if event.button() == Qt.LeftButton:
420            self.selection.end(event)
421            self.graph.replot()
422        return True
423
424    def invalidateMoveSelection(self, *args):
425        self._validMoveSelection = False
426        self._moving = None
427
428    def onMoveStarted(self, index, pos, path):
429        data = self.graph.data
430        attr1, attr2 = self.graph.attr1, self.graph.attr2
431        if not self._validMoveSelection:
432            self._moving = [(i, float(ex[attr1]), float(ex[attr2]))
433                            for i, ex in enumerate(data)]
434            self._moving = [(i, x, y) for i, x, y in self._moving
435                            if path.contains(QPointF(x, y))]
436            self._validMoveSelection = True
437        self._move_anchor = pos
438
439    def onMove(self, index, pos, path):
440        data = self.graph.data
441        attr1, attr2 = self.graph.attr1, self.graph.attr2
442
443        diff = pos - self._move_anchor
444        for i, x, y in self._moving:
445            ex = data[i]
446            ex[attr1] = x + diff.x()
447            ex[attr2] = y + diff.y()
448        self.graph.updateGraph()
449        self.editing.emit()
450
451    def onMoveFinished(self, index, pos, path):
452        self.onMove(index, pos, path)
453        diff = pos - self._move_anchor
454        self._moving = [(i, x + diff.x(), y + diff.y()) \
455                        for i, x, y in self._moving]
456
457        self.editingFinished.emit()
458
459    def deleteSelected(self, *args):
460        data = self.graph.data
461        attr1, attr2 = self.graph.attr1, self.graph.attr2
462        path = self.selection.path()
463        selected = [i for i, ex in enumerate(data)
464                    if path.contains(QPointF(float(ex[attr1]),
465                                             float(ex[attr2])))]
466        if selected:
467            self.editing.emit()
468
469        for i in reversed(selected):
470            del data[i]
471        self.graph.updateGraph()
472
473        if selected:
474            self.editingFinished.emit()
475
476
477class GraphLassoSelections(GraphSelections):
478    def start(self, event):
479        pos = self.getPos(event)
480        index = self.regionAt(event)
481        if index == -1:
482            self.clearSelection()
483            self.addSelectionRegion([pos])
484        else:
485            self._moving_index, self._moving_pos, self._selection_region = \
486                index, pos, self.selection[index]
487
488            self.selectionRegionMoveStarted.emit(
489                index, pos, self.toPath(self.selection[index])
490            )
491
492        self.selectionGeometryChanged.emit()
493
494    def update(self, event):
495        pos = self.getPos(event)
496        index = self._moving_index
497        if index == -1:
498            self.selection[-1].append(pos)
499            self.selectionRegionUpdated.emit(
500                len(self.selection) - 1, self.toPath(self.selection[-1])
501            )
502        else:
503            diff = self._moving_pos - pos
504            self.selection[index] = [p - diff for p in self._selection_region]
505            self.selectionRegionMoved.emit(
506                index, pos, self.toPath(self.selection[index])
507            )
508
509        self.selectionGeometryChanged.emit()
510
511    def end(self, event):
512        self.update(event)
513        if self._moving_index != -1:
514            self.selectionRegionMoveFinished.emit(
515                self._moving_index, self.getPos(event),
516                self.toPath(self.selection[self._moving_index])
517            )
518        self._moving_index = -1
519
520
521class LassoTool(SelectTool):
522    def __init__(self, graph, parent=None):
523        SelectTool.__init__(self, graph, parent,
524                            graphSelection=GraphLassoSelections(graph))
525
526
527class ZoomTool(DataTool):
528    def __init__(self, graph, parent=None):
529        DataTool.__init__(self, graph, parent)
530
531    def paintEvent(self, event):
532        return False
533
534    def mousePressEvent(self, event):
535        return False
536
537    def mouseMoveEvent(self, event):
538        return False
539
540    def mouseReleaseEvent(self, event):
541        return False
542
543    def mouseDoubleClickEvent(self, event):
544        return False
545
546    def keyPressEvent(self, event):
547        return False
548
549
550class PutInstanceTool(DataTool):
551    cursor = Qt.CrossCursor
552
553    def mousePressEvent(self, event):
554        if event.buttons() & Qt.LeftButton:
555            coord = self.invTransform(event.pos())
556            val1, val2 = coord.x(), coord.y()
557            attr1, attr2 = self.attributes()
558            self.editing.emit()
559            self.dataTransform(attr1, val1, attr2, val2)
560            self.editingFinished.emit()
561        return True
562
563    def dataTransform(self, attr1, val1, attr2, val2):
564        domain = self.graph.data.domain
565        example = Orange.data.Instance(
566            domain, [val1, val2, domain.class_var.base_value]
567        )
568
569        self.graph.data.append(example)
570        self.graph.updateGraph(dataInterval=(-1, sys.maxint))
571
572
573class BrushTool(DataTool):
574    brushRadius = 20
575    density = 5
576    cursor = Qt.CrossCursor
577
578    class optionsWidget(QFrame):
579        def __init__(self, tool, parent=None):
580            QFrame.__init__(self, parent)
581            self.tool = tool
582            layout = QFormLayout()
583            self.radiusSlider = QSlider(Qt.Horizontal, minimum=10, maximum=30,
584                                        value=self.tool.brushRadius)
585            self.densitySlider = QSlider(Qt.Horizontal, minimum=3, maximum=10,
586                                         value=self.tool.density)
587
588            layout.addRow("Radius", self.radiusSlider)
589            layout.addRow("Density", self.densitySlider)
590            self.setLayout(layout)
591
592            self.radiusSlider.valueChanged[int].connect(
593                lambda value: setattr(self.tool, "brushRadius", value)
594            )
595
596            self.densitySlider.valueChanged[int].connect(
597                lambda value: setattr(self.tool, "density", value)
598            )
599
600    def __init__(self, graph, parent=None):
601        DataTool.__init__(self, graph, parent)
602        self.brushState = -20, -20, 0, 0
603
604    def mousePressEvent(self, event):
605        self.brushState = (event.pos().x(), event.pos().y(),
606                           self.brushRadius, self.brushRadius)
607        x, y, rx, ry = self.brushGeometry(event.pos())
608        if event.buttons() & Qt.LeftButton:
609            attr1, attr2 = self.attributes()
610            self.editing.emit()
611            self.dataTransform(attr1, x, rx, attr2, y, ry)
612
613        self.graph.replot()
614        return True
615
616    def mouseMoveEvent(self, event):
617        self.brushState = (event.pos().x(), event.pos().y(),
618                           self.brushRadius, self.brushRadius)
619        x, y, rx, ry = self.brushGeometry(event.pos())
620        if event.buttons() & Qt.LeftButton:
621            attr1, attr2 = self.attributes()
622            self.dataTransform(attr1, x, rx, attr2, y, ry)
623        self.graph.replot()
624        return True
625
626    def mouseReleaseEvent(self, event):
627        self.graph.replot()
628        if event.button() & Qt.LeftButton:
629            self.editingFinished.emit()
630        return True
631
632    def leaveEvent(self, event):
633        self.graph.setToolPixmap(QPixmap())
634        self.graph.replot()
635        return False
636
637    def paintEvent(self, event):
638        if not self.graph.canvas().underMouse():
639            self.graph.setToolPixmap(QPixmap())
640            return False
641
642        pixmap = QPixmap(self.graph.canvas().size())
643        pixmap.fill(QColor(255, 255, 255, 0))
644        painter = QPainter(pixmap)
645        painter.setRenderHint(QPainter.Antialiasing)
646        painter.setPen(QPen(Qt.black, 1))
647        x, y, w, h = self.brushState
648        painter.drawEllipse(QPoint(x, y), w, h)
649
650        painter.end()
651
652        self.graph.setToolPixmap(pixmap)
653        return False
654
655    def brushGeometry(self, point):
656        coord = self.invTransform(point)
657        dcoord = self.invTransform(QPoint(point.x() + self.brushRadius,
658                                          point.y() + self.brushRadius))
659        x, y = coord.x(), coord.y()
660        rx, ry = dcoord.x() - x, -(dcoord.y() - y)
661        return x, y, rx, ry
662
663    def dataTransform(self, attr1, x, rx, attr2, y, ry):
664        new = []
665        domain = self.graph.data.domain
666
667        for i in range(self.density):
668            ex = Orange.data.Instance(
669                domain, [random.normalvariate(x, rx),
670                         random.normalvariate(y, ry),
671                         domain.class_var.base_value]
672            )
673
674            new.append(ex)
675        self.graph.data.extend(new)
676        self.graph.updateGraph(dataInterval=(-len(new), sys.maxint))
677
678
679class MagnetTool(BrushTool):
680    cursor = Qt.ArrowCursor
681
682    def dataTransform(self, attr1, x, rx, attr2, y, ry):
683        for ex in self.graph.data:
684            x1, y1 = float(ex[attr1]), float(ex[attr2])
685            distsq = (x1 - x) ** 2 + (y1 - y) ** 2
686            dist = math.sqrt(distsq)
687            attraction = self.density / 100.0
688            advance = 0.005
689            dx = -(x1 - x) / dist * attraction / max(distsq, rx) * advance
690            dy = -(y1 - y) / dist * attraction / max(distsq, ry) * advance
691            ex[attr1] = x1 + dx
692            ex[attr2] = y1 + dy
693        self.graph.updateGraph()
694
695
696class JitterTool(BrushTool):
697    cursor = Qt.ArrowCursor
698
699    def dataTransform(self, attr1, x, rx, attr2, y, ry):
700        for ex in self.graph.data:
701            x1, y1 = float(ex[attr1]), float(ex[attr2])
702            distsq = (x1 - x) ** 2 + (y1 - y) ** 2
703            dist = math.sqrt(distsq)
704            attraction = self.density / 100.0
705            advance = 0.005
706            dx = -(x1 - x) / dist * attraction / max(distsq, rx) * advance
707            dy = -(y1 - y) / dist * attraction / max(distsq, ry) * advance
708            ex[attr1] = x1 - random.normalvariate(0, dx)    # *self.density)
709            ex[attr2] = y1 - random.normalvariate(0, dy)    # *self.density)
710        self.graph.updateGraph()
711
712
713class EnumVariableModel(PyListModel):
714    def __init__(self, var, parent=None, **kwargs):
715        PyListModel.__init__(self, [], parent, **kwargs)
716        self.wrap(var.values)
717        self.colorPalette = OWColorPalette.ColorPaletteHSV(len(self))
718
719    def __delitem__(self, index):
720        raise TypeError("Cannot delete EnumVariable value")
721
722    def __delslice__(self, i, j):
723        raise TypeError("Cannot delete EnumVariable values")
724
725    def __setitem__(self, index, item):
726        self._list[index] = str(item)
727
728    def data(self, index, role=Qt.DisplayRole):
729        if role == Qt.DecorationRole:
730            i = index.row()
731            return QVariant(self.itemQIcon(i))
732        else:
733            return PyListModel.data(self, index, role)
734
735    def itemQIcon(self, i):
736        pixmap = QPixmap(64, 64)
737        pixmap.fill(QColor(255, 255, 255, 0))
738        painter = QPainter(pixmap)
739        painter.setRenderHint(QPainter.Antialiasing, True)
740        painter.setBrush(self.colorPalette[i])
741        painter.drawEllipse(QRectF(15, 15, 39, 39))
742        painter.end()
743        return QIcon(pixmap)
744
745
746class OWPaintData(OWWidget):
747    TOOLS = [("Brush", "Create multiple instances", BrushTool, icon_brush),
748             ("Put", "Put individual instances", PutInstanceTool, icon_put),
749             ("Select", "Select and move instances", SelectTool, icon_select),
750             ("Lasso", "Select and move instances", LassoTool, icon_lasso),
751             ("Jitter", "Jitter instances", JitterTool, icon_jitter),
752             ("Magnet", "Move (drag) multiple instances", MagnetTool, icon_magnet),
753             ("Zoom", "Zoom", ZoomTool, OWToolbars.dlg_zoom)
754             ]
755    settingsList = ["commitOnChange"]
756
757    def __init__(self, parent=None, signalManager=None, name="Data Generator"):
758        OWWidget.__init__(self, parent, signalManager, name)
759
760        self.outputs = [("Data", ExampleTable)]
761
762        self.addClassAsMeta = False
763        self.attributes = []
764        self.cov = []
765        self.commitOnChange = False
766
767        self.loadSettings()
768
769        self.variablesModel = VariableListModel(
770            [Orange.feature.Continuous(name) for name in ["X", "Y"]], self,
771            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
772        )
773
774        self.classVariable = Orange.feature.Discrete(
775            "Class label", values=["Class 1", "Class 2"], baseValue=0
776        )
777
778        w = OWGUI.widgetBox(self.controlArea, "Class Label")
779        w.layout().setSpacing(1)
780
781        self.classValuesView = listView = QListView(
782            selectionMode=QListView.SingleSelection,
783            sizePolicy=QSizePolicy(QSizePolicy.Ignored,
784                                   QSizePolicy.Maximum)
785        )
786
787        self.classValuesModel = EnumVariableModel(
788            self.classVariable, self,
789            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
790        )
791        self.classValuesModel.wrap(self.classVariable.values)
792
793        listView.setModel(self.classValuesModel)
794        listView.selectionModel().select(self.classValuesModel.index(0),
795                                         QItemSelectionModel.ClearAndSelect)
796        listView.selectionModel().selectionChanged.connect(
797            self.onClassLabelSelection
798        )
799        w.layout().addWidget(listView)
800
801        self.addClassLabel = QAction(
802            "+", self, toolTip="Add class label"
803        )
804        self.addClassLabel.triggered.connect(self.addNewClassLabel)
805
806        self.removeClassLabel = QAction(
807            unicodedata.lookup("MINUS SIGN"), self,
808            toolTip="Remove selected class label"
809        )
810        self.removeClassLabel.triggered.connect(self.removeSelectedClassLabel)
811
812        actionsWidget = ModelActionsWidget(
813            [self.addClassLabel, self.removeClassLabel], self
814        )
815        actionsWidget.layout().addStretch(10)
816        actionsWidget.layout().setSpacing(1)
817
818        w.layout().addWidget(actionsWidget)
819
820        toolbox = OWGUI.widgetBox(self.controlArea, "Tools",
821                                  orientation=QGridLayout())
822        self.toolActions = QActionGroup(self, exclusive=True)
823
824        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
825            action = QAction(name, self, toolTip=tooltip, checkable=True)
826            if os.path.exists(icon):
827                action.setIcon(QIcon(icon))
828
829            action.triggered[()].connect(
830                lambda tool=tool: self.onToolAction(tool)
831            )
832
833            button = QToolButton(
834                iconSize=QSize(24, 24),
835                toolButtonStyle=Qt.ToolButtonTextUnderIcon,
836                sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
837                                       QSizePolicy.Fixed)
838            )
839            button.setDefaultAction(action)
840            toolbox.layout().addWidget(button, i / 3, i % 3)
841            self.toolActions.addAction(action)
842
843        # TODO: Columns should have uniform widths
844        for column in range(3):
845            toolbox.layout().setColumnMinimumWidth(column, 10)
846            toolbox.layout().setColumnStretch(column, 1)
847
848        self.optionsLayout = QStackedLayout()
849        self.toolsStackCache = {}
850        OWGUI.widgetBox(self.controlArea, "Options",
851                        orientation=self.optionsLayout)
852
853        ur = OWGUI.widgetBox(self.controlArea, "")
854        undo = QAction("Undo", self, toolTip="Undo action",
855                       shortcut=QKeySequence.Undo)
856        undo.triggered.connect(self.undo)
857
858        redo = QAction("Redo", self, toolTip="Redo action",
859                       shortcut=QKeySequence.Redo)
860        redo.triggered.connect(self.redo)
861
862        actionsWidget = ModelActionsWidget([undo, redo], self)
863        actionsWidget.layout().addStretch(10)
864        actionsWidget.layout().setSpacing(1)
865
866        ur.layout().addWidget(actionsWidget)
867
868        OWGUI.rubber(self.controlArea)
869        box = OWGUI.widgetBox(self.controlArea, "Commit")
870
871        cb = OWGUI.checkBox(box, self, "commitOnChange", "Commit on change",
872                            tooltip="Send the data on any change.",
873                            callback=self.commitIf,)
874        b = OWGUI.button(box, self, "Commit",
875                         callback=self.commit, default=True)
876        OWGUI.setStopper(self, b, cb, "dataChangedFlag", callback=self.commit)
877
878        self.graph = PaintDataGraph(self)
879        self.graph.setAxisScale(QwtPlot.xBottom, 0.0, 1.0)
880        self.graph.setAxisScale(QwtPlot.yLeft, 0.0, 1.0)
881        self.graph.setAttribute(Qt.WA_Hover, True)
882        self.mainArea.layout().addWidget(self.graph)
883
884        self.currentOptionsWidget = None
885        self.data = []
886        self.dataChangedFlag = False
887        self.domain = None
888
889        self.onDomainChanged()
890        self.toolActions.actions()[0].trigger()
891
892        self.dataHistory = [(Orange.data.Table(self.domain),
893                             ["Class 1", "Class 2"])]
894        self.historyCounter = 0
895        self.updateHistoryBool = True
896
897        self.resize(800, 600)
898
899    def addNewClassLabel(self):
900        """
901        Create and add a new class label
902        """
903        labels = ("Class %i" % i for i in itertools.count(1))
904        newlabel = next(label for label in labels
905                        if not label in self.classValuesModel)
906
907        values = list(self.classValuesModel) + [newlabel]
908        newclass = Orange.feature.Discrete("Class label", values=values)
909        newdomain = Orange.data.Domain(self.graph.data.domain.attributes,
910                                       newclass)
911        newdata = Orange.data.Table(newdomain)
912
913        newdata.extend(
914            [Orange.data.Instance(
915                newdomain, [ex[a] for a in ex.domain.attributes] +
916                           [str(ex.getclass())])
917             for ex in self.graph.data]
918        )
919        self.classVariable = newclass
920        self.classValuesModel.wrap(self.classVariable.values)
921
922        self.graph.data = newdata
923        self.graph.updateGraph()
924
925        newindex = self.classValuesModel.index(len(self.classValuesModel) - 1)
926        self.classValuesView.selectionModel().select(
927            newindex, QItemSelectionModel.ClearAndSelect
928        )
929
930        self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
931
932        self.updateHistory()
933
934    def removeSelectedClassLabel(self, label=None):
935        index = self.selectedClassLabelIndex()
936        if index is not None and len(self.classValuesModel) > 1:
937            if not label:
938                label = self.classValuesModel[index]
939            examples = [ex for ex in self.graph.data
940                        if str(ex.getclass()) != label]
941
942            values = [val for val in self.classValuesModel if val != label]
943
944            newclass = Orange.feature.Discrete("Class label", values=values)
945            newdomain = Orange.data.Domain(self.graph.data.domain.attributes,
946                                           newclass)
947            newdata = Orange.data.Table(newdomain)
948            for ex in examples:
949                if ex[self.classVariable] != label and \
950                        ex[self.classVariable] in values:
951                    values = ([ex[a] for a in ex.domain.attributes] +
952                              [str(ex.getclass())])
953                    newdata.append(Orange.data.Instance(newdomain, values))
954
955            self.classVariable = newclass
956            self.classValuesModel.wrap(self.classVariable.values)
957
958            self.graph.data = newdata
959            self.graph.updateGraph()
960
961            newindex = self.classValuesModel.index(max(0, index - 1))
962            self.classValuesView.selectionModel().select(
963                newindex, QItemSelectionModel.ClearAndSelect
964            )
965
966            self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
967
968            self.updateHistory()
969
970    def selectedClassLabelIndex(self):
971        rows = self.classValuesView.selectionModel().selectedRows()
972        if rows:
973            return rows[0].row()
974        else:
975            return None
976
977    def onClassLabelSelection(self, selected, unselected):
978        index = self.selectedClassLabelIndex()
979        if index is not None:
980            self.classVariable.baseValue = index
981
982    def onToolAction(self, tool):
983        self.setCurrentTool(tool)
984
985    def setCurrentTool(self, tool):
986        if tool not in self.toolsStackCache:
987            newtool = tool(None, self)
988            option = newtool.optionsWidget(newtool, self)
989            self.optionsLayout.addWidget(option)
990            newtool.editing.connect(self.onDataChanged)
991            newtool.editingFinished.connect(self.commitIf)
992            newtool.editingFinished.connect(self.updateHistory)
993
994            self.toolsStackCache[tool] = (newtool, option)
995
996        self.currentTool, self.currentOptionsWidget = self.toolsStackCache[tool]
997        self.optionsLayout.setCurrentWidget(self.currentOptionsWidget)
998        self.currentTool.setGraph(self.graph)
999
1000    def updateHistory(self):
1001        if not self.updateHistoryBool:
1002            return
1003        # if we start updating from previously undone actions, we cut off redos in our history
1004        if not self.historyCounter == len(self.dataHistory) - 1:
1005            self.dataHistory = self.dataHistory[:self.historyCounter + 1]
1006        # append an update of labels and data
1007        labels = list(self.classValuesModel)
1008        self.dataHistory.append((copy.deepcopy(self.graph.data), labels))
1009        # set the size of the data history stack and remove the oldest update if we go over the size
1010        if len(self.dataHistory) > 100:
1011            self.dataHistory.pop(0)
1012            self.historyCounter -= 1
1013        self.historyCounter += 1
1014
1015    def undo(self):
1016        # check to see if we are at the end of the stack
1017        if self.historyCounter > 0:
1018            self.historyCounter -= 1
1019            data, labels = self.dataHistory[self.historyCounter]
1020            # we dont update history when undoing
1021            self.updateHistoryBool = False
1022            # check if we only need to update labels
1023            if len(self.classValuesModel) > len(labels):
1024                diff = set(self.classValuesModel) - set(labels)
1025                self.removeSelectedClassLabel(label=diff.pop())
1026            elif len(self.classValuesModel) < len(labels):
1027                self.addNewClassLabel()
1028            # if not, update data
1029            else:
1030                self.graph.data = copy.deepcopy(data)
1031                self.graph.updateGraph()
1032            self.updateHistoryBool = True
1033
1034    def redo(self):
1035        if self.historyCounter < len(self.dataHistory) - 1:
1036            self.historyCounter += 1
1037            data, labels = self.dataHistory[self.historyCounter]
1038            self.updateHistoryBool = False
1039            if len(self.classValuesModel) > len(labels):
1040                diff = set(self.classValuesModel) - set(labels)
1041                self.removeSelectedClassLabel(label=diff.pop())
1042            elif len(self.classValuesModel) < len(labels):
1043                self.addNewClassLabel()
1044            else:
1045                self.graph.data = copy.deepcopy(data)
1046                self.graph.updateGraph()
1047            self.updateHistoryBool = True
1048
1049    def onDomainChanged(self, *args):
1050        if self.variablesModel:
1051            self.domain = Orange.data.Domain(list(self.variablesModel),
1052                                             self.classVariable)
1053            if self.data:
1054                self.data = Orange.data.Table(self.domain, self.data)
1055            else:
1056                self.data = Orange.data.Table(self.domain)
1057            self.graph.setData(self.data, 0, 1)
1058
1059    def onDataChanged(self):
1060        self.dataChangedFlag = True
1061
1062    def keyPressEvent(self, event):
1063        if event.key() == QtCore.Qt.Key_Delete and \
1064                isinstance(self.currentTool, SelectTool):
1065            self.currentTool.deleteSelected()
1066
1067    def commitIf(self):
1068        if self.commitOnChange and self.dataChangedFlag:
1069            self.commit()
1070        else:
1071            self.dataChangedFlag = True
1072
1073    def commit(self):
1074        data = self.graph.data
1075        values = set([str(ex.getclass()) for ex in data])
1076        if len(values) == 1:
1077            # Remove the useless class variable.
1078            domain = Orange.data.Domain(data.domain.attributes, None)
1079            data = Orange.data.Table(domain, data)
1080        self.send("Data", data)
1081
1082
1083if __name__ == "__main__":
1084    app = QApplication(sys.argv)
1085    w = OWPaintData()
1086    w.show()
1087    app.exec_()
Note: See TracBrowser for help on using the repository browser.