source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11828:6266629a6575

Revision 11828:6266629a6575, 37.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 3 months ago (diff)

Added 'Command + Backspace' shortcut for deleting selections on OSX.

(fixes #1352)

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            action = QAction(
347                 "Delete", self,
348                 toolTip="Delete selected instances.",
349                 shortcut=QKeySequence.Delete,
350                 shortcutContext=Qt.WindowShortcut,
351            )
352            if sys.platform == "darwin":
353                action.setShortcuts(
354                    [QKeySequence(Qt.ControlModifier + Qt.Key_Backspace),
355                     QKeySequence(QKeySequence.Delete)]
356                )
357
358            delete = QToolButton(self)
359            delete.setDefaultAction(action)
360
361            action.triggered.connect(self.tool.deleteSelected)
362
363            layout.addWidget(delete)
364            layout.addStretch(10)
365            self.setLayout(layout)
366
367    def __init__(self, graph, parent=None, graphSelection=None):
368        DataTool.__init__(self, graph, parent)
369        if graphSelection is None:
370            self.selection = GraphSelections(graph)
371        else:
372            self.selection = graphSelection
373
374        self.pen = QPen(Qt.black, 1, Qt.DashDotLine)
375        self.pen.setCosmetic(True)
376        self.pen.setJoinStyle(Qt.RoundJoin)
377        self.pen.setCapStyle(Qt.RoundCap)
378
379        self.selection.selectionRegionMoveStarted.connect(self.onMoveStarted)
380        self.selection.selectionRegionMoved.connect(self.onMove)
381        self.selection.selectionRegionMoveFinished.connect(self.onMoveFinished)
382        self.selection.selectionRegionUpdated.connect(
383                self.invalidateMoveSelection)
384
385        self._validMoveSelection = False
386        self._moving = None
387
388    def setGraph(self, graph):
389        DataTool.setGraph(self, graph)
390        if graph and hasattr(self, "selection"):
391            self.selection.setParent(graph)
392
393    def installed(self):
394        DataTool.installed(self)
395        self.invalidateMoveSelection()
396
397    def paintEvent(self, event):
398        if self.selection:
399            pixmap = QPixmap(self.graph.canvas().size())
400            pixmap.fill(QColor(255, 255, 255, 0))
401            painter = QPainter(pixmap)
402            painter.setRenderHints(QPainter.Antialiasing)
403            inverted, singular = self.selection.qTransform().inverted()
404            painter.setPen(self.pen)
405
406            painter.setTransform(inverted)
407            for region in self.selection.selection:
408                painter.drawPath(self.selection.toPath(region))
409            painter.end()
410            self.graph.setToolPixmap(pixmap)
411        return False
412
413    def mousePressEvent(self, event):
414        if event.button() == Qt.LeftButton:
415            self.selection.start(event)
416            self.graph.replot()
417        return True
418
419    def mouseMoveEvent(self, event):
420        index = self.selection.regionAt(event)
421        if index != -1:
422            self.graph.canvas().setCursor(Qt.OpenHandCursor)
423        else:
424            self.graph.canvas().setCursor(self.graph._cursor)
425
426        if event.buttons() & Qt.LeftButton:
427            self.selection.update(event)
428            self.graph.replot()
429        return True
430
431    def mouseReleaseEvent(self, event):
432        if event.button() == Qt.LeftButton:
433            self.selection.end(event)
434            self.graph.replot()
435        return True
436
437    def invalidateMoveSelection(self, *args):
438        self._validMoveSelection = False
439        self._moving = None
440
441    def onMoveStarted(self, index, pos, path):
442        data = self.graph.data
443        attr1, attr2 = self.graph.attr1, self.graph.attr2
444        if not self._validMoveSelection:
445            self._moving = [(i, float(ex[attr1]), float(ex[attr2]))
446                            for i, ex in enumerate(data)]
447            self._moving = [(i, x, y) for i, x, y in self._moving
448                            if path.contains(QPointF(x, y))]
449            self._validMoveSelection = True
450        self._move_anchor = pos
451
452    def onMove(self, index, pos, path):
453        data = self.graph.data
454        attr1, attr2 = self.graph.attr1, self.graph.attr2
455
456        diff = pos - self._move_anchor
457        for i, x, y in self._moving:
458            ex = data[i]
459            ex[attr1] = x + diff.x()
460            ex[attr2] = y + diff.y()
461        self.graph.updateGraph()
462        self.editing.emit()
463
464    def onMoveFinished(self, index, pos, path):
465        self.onMove(index, pos, path)
466        diff = pos - self._move_anchor
467        self._moving = [(i, x + diff.x(), y + diff.y()) \
468                        for i, x, y in self._moving]
469
470        self.editingFinished.emit()
471
472    def deleteSelected(self, *args):
473        data = self.graph.data
474        attr1, attr2 = self.graph.attr1, self.graph.attr2
475        path = self.selection.path()
476        selected = [i for i, ex in enumerate(data)
477                    if path.contains(QPointF(float(ex[attr1]),
478                                             float(ex[attr2])))]
479        if selected:
480            self.editing.emit()
481
482        for i in reversed(selected):
483            del data[i]
484        self.graph.updateGraph()
485
486        if selected:
487            self.editingFinished.emit()
488
489
490class GraphLassoSelections(GraphSelections):
491    def start(self, event):
492        pos = self.getPos(event)
493        index = self.regionAt(event)
494        if index == -1:
495            self.clearSelection()
496            self.addSelectionRegion([pos])
497        else:
498            self._moving_index, self._moving_pos, self._selection_region = \
499                index, pos, self.selection[index]
500
501            self.selectionRegionMoveStarted.emit(
502                index, pos, self.toPath(self.selection[index])
503            )
504
505        self.selectionGeometryChanged.emit()
506
507    def update(self, event):
508        pos = self.getPos(event)
509        index = self._moving_index
510        if index == -1:
511            self.selection[-1].append(pos)
512            self.selectionRegionUpdated.emit(
513                len(self.selection) - 1, self.toPath(self.selection[-1])
514            )
515        else:
516            diff = self._moving_pos - pos
517            self.selection[index] = [p - diff for p in self._selection_region]
518            self.selectionRegionMoved.emit(
519                index, pos, self.toPath(self.selection[index])
520            )
521
522        self.selectionGeometryChanged.emit()
523
524    def end(self, event):
525        self.update(event)
526        if self._moving_index != -1:
527            self.selectionRegionMoveFinished.emit(
528                self._moving_index, self.getPos(event),
529                self.toPath(self.selection[self._moving_index])
530            )
531        self._moving_index = -1
532
533
534class LassoTool(SelectTool):
535    def __init__(self, graph, parent=None):
536        SelectTool.__init__(self, graph, parent,
537                            graphSelection=GraphLassoSelections(graph))
538
539
540class ZoomTool(DataTool):
541    def __init__(self, graph, parent=None):
542        DataTool.__init__(self, graph, parent)
543
544    def paintEvent(self, event):
545        return False
546
547    def mousePressEvent(self, event):
548        return False
549
550    def mouseMoveEvent(self, event):
551        return False
552
553    def mouseReleaseEvent(self, event):
554        return False
555
556    def mouseDoubleClickEvent(self, event):
557        return False
558
559    def keyPressEvent(self, event):
560        return False
561
562
563class PutInstanceTool(DataTool):
564    cursor = Qt.CrossCursor
565
566    def mousePressEvent(self, event):
567        if event.buttons() & Qt.LeftButton:
568            coord = self.invTransform(event.pos())
569            val1, val2 = coord.x(), coord.y()
570            attr1, attr2 = self.attributes()
571            self.editing.emit()
572            self.dataTransform(attr1, val1, attr2, val2)
573            self.editingFinished.emit()
574        return True
575
576    def dataTransform(self, attr1, val1, attr2, val2):
577        domain = self.graph.data.domain
578        example = Orange.data.Instance(
579            domain, [val1, val2, domain.class_var.base_value]
580        )
581
582        self.graph.data.append(example)
583        self.graph.updateGraph(dataInterval=(-1, sys.maxint))
584
585
586class BrushTool(DataTool):
587    brushRadius = 20
588    density = 5
589    cursor = Qt.CrossCursor
590
591    class optionsWidget(QFrame):
592        def __init__(self, tool, parent=None):
593            QFrame.__init__(self, parent)
594            self.tool = tool
595            layout = QFormLayout()
596            self.radiusSlider = QSlider(Qt.Horizontal, minimum=10, maximum=30,
597                                        value=self.tool.brushRadius)
598            self.densitySlider = QSlider(Qt.Horizontal, minimum=3, maximum=10,
599                                         value=self.tool.density)
600
601            layout.addRow("Radius", self.radiusSlider)
602            layout.addRow("Density", self.densitySlider)
603            self.setLayout(layout)
604
605            self.radiusSlider.valueChanged[int].connect(
606                lambda value: setattr(self.tool, "brushRadius", value)
607            )
608
609            self.densitySlider.valueChanged[int].connect(
610                lambda value: setattr(self.tool, "density", value)
611            )
612
613    def __init__(self, graph, parent=None):
614        DataTool.__init__(self, graph, parent)
615        self.brushState = -20, -20, 0, 0
616
617    def mousePressEvent(self, event):
618        self.brushState = (event.pos().x(), event.pos().y(),
619                           self.brushRadius, self.brushRadius)
620        x, y, rx, ry = self.brushGeometry(event.pos())
621        if event.buttons() & Qt.LeftButton:
622            attr1, attr2 = self.attributes()
623            self.editing.emit()
624            self.dataTransform(attr1, x, rx, attr2, y, ry)
625
626        self.graph.replot()
627        return True
628
629    def mouseMoveEvent(self, event):
630        self.brushState = (event.pos().x(), event.pos().y(),
631                           self.brushRadius, self.brushRadius)
632        x, y, rx, ry = self.brushGeometry(event.pos())
633        if event.buttons() & Qt.LeftButton:
634            attr1, attr2 = self.attributes()
635            self.dataTransform(attr1, x, rx, attr2, y, ry)
636        self.graph.replot()
637        return True
638
639    def mouseReleaseEvent(self, event):
640        self.graph.replot()
641        if event.button() & Qt.LeftButton:
642            self.editingFinished.emit()
643        return True
644
645    def leaveEvent(self, event):
646        self.graph.setToolPixmap(QPixmap())
647        self.graph.replot()
648        return False
649
650    def paintEvent(self, event):
651        if not self.graph.canvas().underMouse():
652            self.graph.setToolPixmap(QPixmap())
653            return False
654
655        pixmap = QPixmap(self.graph.canvas().size())
656        pixmap.fill(QColor(255, 255, 255, 0))
657        painter = QPainter(pixmap)
658        painter.setRenderHint(QPainter.Antialiasing)
659        painter.setPen(QPen(Qt.black, 1))
660        x, y, w, h = self.brushState
661        painter.drawEllipse(QPoint(x, y), w, h)
662
663        painter.end()
664
665        self.graph.setToolPixmap(pixmap)
666        return False
667
668    def brushGeometry(self, point):
669        coord = self.invTransform(point)
670        dcoord = self.invTransform(QPoint(point.x() + self.brushRadius,
671                                          point.y() + self.brushRadius))
672        x, y = coord.x(), coord.y()
673        rx, ry = dcoord.x() - x, -(dcoord.y() - y)
674        return x, y, rx, ry
675
676    def dataTransform(self, attr1, x, rx, attr2, y, ry):
677        new = []
678        domain = self.graph.data.domain
679
680        for i in range(self.density):
681            ex = Orange.data.Instance(
682                domain, [random.normalvariate(x, rx),
683                         random.normalvariate(y, ry),
684                         domain.class_var.base_value]
685            )
686
687            new.append(ex)
688        self.graph.data.extend(new)
689        self.graph.updateGraph(dataInterval=(-len(new), sys.maxint))
690
691
692class MagnetTool(BrushTool):
693    cursor = Qt.ArrowCursor
694
695    def dataTransform(self, attr1, x, rx, attr2, y, ry):
696        for ex in self.graph.data:
697            x1, y1 = float(ex[attr1]), float(ex[attr2])
698            distsq = (x1 - x) ** 2 + (y1 - y) ** 2
699            dist = math.sqrt(distsq)
700            attraction = self.density / 100.0
701            advance = 0.005
702            dx = -(x1 - x) / dist * attraction / max(distsq, rx) * advance
703            dy = -(y1 - y) / dist * attraction / max(distsq, ry) * advance
704            ex[attr1] = x1 + dx
705            ex[attr2] = y1 + dy
706        self.graph.updateGraph()
707
708
709class JitterTool(BrushTool):
710    cursor = Qt.ArrowCursor
711
712    def dataTransform(self, attr1, x, rx, attr2, y, ry):
713        for ex in self.graph.data:
714            x1, y1 = float(ex[attr1]), float(ex[attr2])
715            distsq = (x1 - x) ** 2 + (y1 - y) ** 2
716            dist = math.sqrt(distsq)
717            attraction = self.density / 100.0
718            advance = 0.005
719            dx = -(x1 - x) / dist * attraction / max(distsq, rx) * advance
720            dy = -(y1 - y) / dist * attraction / max(distsq, ry) * advance
721            ex[attr1] = x1 - random.normalvariate(0, dx)    # *self.density)
722            ex[attr2] = y1 - random.normalvariate(0, dy)    # *self.density)
723        self.graph.updateGraph()
724
725
726class EnumVariableModel(PyListModel):
727    def __init__(self, var, parent=None, **kwargs):
728        PyListModel.__init__(self, [], parent, **kwargs)
729        self.wrap(var.values)
730        self.colorPalette = OWColorPalette.ColorPaletteHSV(len(self))
731
732    def __delitem__(self, index):
733        raise TypeError("Cannot delete EnumVariable value")
734
735    def __delslice__(self, i, j):
736        raise TypeError("Cannot delete EnumVariable values")
737
738    def __setitem__(self, index, item):
739        self._list[index] = str(item)
740
741    def data(self, index, role=Qt.DisplayRole):
742        if role == Qt.DecorationRole:
743            i = index.row()
744            return QVariant(self.itemQIcon(i))
745        else:
746            return PyListModel.data(self, index, role)
747
748    def itemQIcon(self, i):
749        pixmap = QPixmap(64, 64)
750        pixmap.fill(QColor(255, 255, 255, 0))
751        painter = QPainter(pixmap)
752        painter.setRenderHint(QPainter.Antialiasing, True)
753        painter.setBrush(self.colorPalette[i])
754        painter.drawEllipse(QRectF(15, 15, 39, 39))
755        painter.end()
756        return QIcon(pixmap)
757
758
759class OWPaintData(OWWidget):
760    TOOLS = [("Brush", "Create multiple instances", BrushTool, icon_brush),
761             ("Put", "Put individual instances", PutInstanceTool, icon_put),
762             ("Select", "Select and move instances", SelectTool, icon_select),
763             ("Lasso", "Select and move instances", LassoTool, icon_lasso),
764             ("Jitter", "Jitter instances", JitterTool, icon_jitter),
765             ("Magnet", "Move (drag) multiple instances", MagnetTool, icon_magnet),
766             ("Zoom", "Zoom", ZoomTool, OWToolbars.dlg_zoom)
767             ]
768    settingsList = ["commitOnChange"]
769
770    def __init__(self, parent=None, signalManager=None, name="Data Generator"):
771        OWWidget.__init__(self, parent, signalManager, name)
772
773        self.outputs = [("Data", ExampleTable)]
774
775        self.addClassAsMeta = False
776        self.attributes = []
777        self.cov = []
778        self.commitOnChange = False
779
780        self.loadSettings()
781
782        self.variablesModel = VariableListModel(
783            [Orange.feature.Continuous(name) for name in ["X", "Y"]], self,
784            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
785        )
786
787        self.classVariable = Orange.feature.Discrete(
788            "Class label", values=["Class 1", "Class 2"], baseValue=0
789        )
790
791        w = OWGUI.widgetBox(self.controlArea, "Class Label")
792        w.layout().setSpacing(1)
793
794        self.classValuesView = listView = QListView(
795            selectionMode=QListView.SingleSelection,
796            sizePolicy=QSizePolicy(QSizePolicy.Ignored,
797                                   QSizePolicy.Maximum)
798        )
799
800        self.classValuesModel = EnumVariableModel(
801            self.classVariable, self,
802            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
803        )
804        self.classValuesModel.wrap(self.classVariable.values)
805
806        listView.setModel(self.classValuesModel)
807        listView.selectionModel().select(self.classValuesModel.index(0),
808                                         QItemSelectionModel.ClearAndSelect)
809        listView.selectionModel().selectionChanged.connect(
810            self.onClassLabelSelection
811        )
812        w.layout().addWidget(listView)
813
814        self.addClassLabel = QAction(
815            "+", self, toolTip="Add class label"
816        )
817        self.addClassLabel.triggered.connect(self.addNewClassLabel)
818
819        self.removeClassLabel = QAction(
820            unicodedata.lookup("MINUS SIGN"), self,
821            toolTip="Remove selected class label"
822        )
823        self.removeClassLabel.triggered.connect(self.removeSelectedClassLabel)
824
825        actionsWidget = ModelActionsWidget(
826            [self.addClassLabel, self.removeClassLabel], self
827        )
828        actionsWidget.layout().addStretch(10)
829        actionsWidget.layout().setSpacing(1)
830
831        w.layout().addWidget(actionsWidget)
832
833        toolbox = OWGUI.widgetBox(self.controlArea, "Tools",
834                                  orientation=QGridLayout())
835        self.toolActions = QActionGroup(self, exclusive=True)
836
837        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
838            action = QAction(name, self, toolTip=tooltip, checkable=True)
839            if os.path.exists(icon):
840                action.setIcon(QIcon(icon))
841
842            action.triggered[()].connect(
843                lambda tool=tool: self.onToolAction(tool)
844            )
845
846            button = QToolButton(
847                iconSize=QSize(24, 24),
848                toolButtonStyle=Qt.ToolButtonTextUnderIcon,
849                sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
850                                       QSizePolicy.Fixed)
851            )
852            button.setDefaultAction(action)
853            toolbox.layout().addWidget(button, i / 3, i % 3)
854            self.toolActions.addAction(action)
855
856        # TODO: Columns should have uniform widths
857        for column in range(3):
858            toolbox.layout().setColumnMinimumWidth(column, 10)
859            toolbox.layout().setColumnStretch(column, 1)
860
861        self.optionsLayout = QStackedLayout()
862        self.toolsStackCache = {}
863        OWGUI.widgetBox(self.controlArea, "Options",
864                        orientation=self.optionsLayout)
865
866        ur = OWGUI.widgetBox(self.controlArea, "")
867        undo = QAction("Undo", self, toolTip="Undo action",
868                       shortcut=QKeySequence.Undo)
869        undo.triggered.connect(self.undo)
870
871        redo = QAction("Redo", self, toolTip="Redo action",
872                       shortcut=QKeySequence.Redo)
873        redo.triggered.connect(self.redo)
874
875        actionsWidget = ModelActionsWidget([undo, redo], self)
876        actionsWidget.layout().addStretch(10)
877        actionsWidget.layout().setSpacing(1)
878
879        ur.layout().addWidget(actionsWidget)
880
881        OWGUI.rubber(self.controlArea)
882        box = OWGUI.widgetBox(self.controlArea, "Commit")
883
884        cb = OWGUI.checkBox(box, self, "commitOnChange", "Commit on change",
885                            tooltip="Send the data on any change.",
886                            callback=self.commitIf,)
887        b = OWGUI.button(box, self, "Commit",
888                         callback=self.commit, default=True)
889        OWGUI.setStopper(self, b, cb, "dataChangedFlag", callback=self.commit)
890
891        self.graph = PaintDataGraph(self)
892        self.graph.setAxisScale(QwtPlot.xBottom, 0.0, 1.0)
893        self.graph.setAxisScale(QwtPlot.yLeft, 0.0, 1.0)
894        self.graph.setAttribute(Qt.WA_Hover, True)
895        self.mainArea.layout().addWidget(self.graph)
896
897        self.currentOptionsWidget = None
898        self.data = []
899        self.dataChangedFlag = False
900        self.domain = None
901
902        self.onDomainChanged()
903        self.toolActions.actions()[0].trigger()
904
905        self.dataHistory = [(Orange.data.Table(self.domain),
906                             ["Class 1", "Class 2"])]
907        self.historyCounter = 0
908        self.updateHistoryBool = True
909
910        self.resize(800, 600)
911
912    def addNewClassLabel(self):
913        """
914        Create and add a new class label
915        """
916        labels = ("Class %i" % i for i in itertools.count(1))
917        newlabel = next(label for label in labels
918                        if not label in self.classValuesModel)
919
920        values = list(self.classValuesModel) + [newlabel]
921        newclass = Orange.feature.Discrete("Class label", values=values)
922        newdomain = Orange.data.Domain(self.graph.data.domain.attributes,
923                                       newclass)
924        newdata = Orange.data.Table(newdomain)
925
926        newdata.extend(
927            [Orange.data.Instance(
928                newdomain, [ex[a] for a in ex.domain.attributes] +
929                           [str(ex.getclass())])
930             for ex in self.graph.data]
931        )
932        self.classVariable = newclass
933        self.classValuesModel.wrap(self.classVariable.values)
934
935        self.graph.data = newdata
936        self.graph.updateGraph()
937
938        newindex = self.classValuesModel.index(len(self.classValuesModel) - 1)
939        self.classValuesView.selectionModel().select(
940            newindex, QItemSelectionModel.ClearAndSelect
941        )
942
943        self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
944
945        self.updateHistory()
946
947    def removeSelectedClassLabel(self, label=None):
948        index = self.selectedClassLabelIndex()
949        if index is not None and len(self.classValuesModel) > 1:
950            if not label:
951                label = self.classValuesModel[index]
952            examples = [ex for ex in self.graph.data
953                        if str(ex.getclass()) != label]
954
955            values = [val for val in self.classValuesModel if val != label]
956
957            newclass = Orange.feature.Discrete("Class label", values=values)
958            newdomain = Orange.data.Domain(self.graph.data.domain.attributes,
959                                           newclass)
960            newdata = Orange.data.Table(newdomain)
961            for ex in examples:
962                if ex[self.classVariable] != label and \
963                        ex[self.classVariable] in values:
964                    values = ([ex[a] for a in ex.domain.attributes] +
965                              [str(ex.getclass())])
966                    newdata.append(Orange.data.Instance(newdomain, values))
967
968            self.classVariable = newclass
969            self.classValuesModel.wrap(self.classVariable.values)
970
971            self.graph.data = newdata
972            self.graph.updateGraph()
973
974            newindex = self.classValuesModel.index(max(0, index - 1))
975            self.classValuesView.selectionModel().select(
976                newindex, QItemSelectionModel.ClearAndSelect
977            )
978
979            self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
980
981            self.updateHistory()
982
983    def selectedClassLabelIndex(self):
984        rows = self.classValuesView.selectionModel().selectedRows()
985        if rows:
986            return rows[0].row()
987        else:
988            return None
989
990    def onClassLabelSelection(self, selected, unselected):
991        index = self.selectedClassLabelIndex()
992        if index is not None:
993            self.classVariable.baseValue = index
994
995    def onToolAction(self, tool):
996        self.setCurrentTool(tool)
997
998    def setCurrentTool(self, tool):
999        if tool not in self.toolsStackCache:
1000            newtool = tool(None, self)
1001            option = newtool.optionsWidget(newtool, self)
1002            self.optionsLayout.addWidget(option)
1003            newtool.editing.connect(self.onDataChanged)
1004            newtool.editingFinished.connect(self.commitIf)
1005            newtool.editingFinished.connect(self.updateHistory)
1006
1007            self.toolsStackCache[tool] = (newtool, option)
1008
1009        self.currentTool, self.currentOptionsWidget = self.toolsStackCache[tool]
1010        self.optionsLayout.setCurrentWidget(self.currentOptionsWidget)
1011        self.currentTool.setGraph(self.graph)
1012
1013    def updateHistory(self):
1014        if not self.updateHistoryBool:
1015            return
1016        # if we start updating from previously undone actions, we cut off redos in our history
1017        if not self.historyCounter == len(self.dataHistory) - 1:
1018            self.dataHistory = self.dataHistory[:self.historyCounter + 1]
1019        # append an update of labels and data
1020        labels = list(self.classValuesModel)
1021        self.dataHistory.append((copy.deepcopy(self.graph.data), labels))
1022        # set the size of the data history stack and remove the oldest update if we go over the size
1023        if len(self.dataHistory) > 100:
1024            self.dataHistory.pop(0)
1025            self.historyCounter -= 1
1026        self.historyCounter += 1
1027
1028    def undo(self):
1029        # check to see if we are at the end of the stack
1030        if self.historyCounter > 0:
1031            self.historyCounter -= 1
1032            data, labels = self.dataHistory[self.historyCounter]
1033            # we dont update history when undoing
1034            self.updateHistoryBool = False
1035            # check if we only need to update labels
1036            if len(self.classValuesModel) > len(labels):
1037                diff = set(self.classValuesModel) - set(labels)
1038                self.removeSelectedClassLabel(label=diff.pop())
1039            elif len(self.classValuesModel) < len(labels):
1040                self.addNewClassLabel()
1041            # if not, update data
1042            else:
1043                self.graph.data = copy.deepcopy(data)
1044                self.graph.updateGraph()
1045            self.updateHistoryBool = True
1046
1047    def redo(self):
1048        if self.historyCounter < len(self.dataHistory) - 1:
1049            self.historyCounter += 1
1050            data, labels = self.dataHistory[self.historyCounter]
1051            self.updateHistoryBool = False
1052            if len(self.classValuesModel) > len(labels):
1053                diff = set(self.classValuesModel) - set(labels)
1054                self.removeSelectedClassLabel(label=diff.pop())
1055            elif len(self.classValuesModel) < len(labels):
1056                self.addNewClassLabel()
1057            else:
1058                self.graph.data = copy.deepcopy(data)
1059                self.graph.updateGraph()
1060            self.updateHistoryBool = True
1061
1062    def onDomainChanged(self, *args):
1063        if self.variablesModel:
1064            self.domain = Orange.data.Domain(list(self.variablesModel),
1065                                             self.classVariable)
1066            if self.data:
1067                self.data = Orange.data.Table(self.domain, self.data)
1068            else:
1069                self.data = Orange.data.Table(self.domain)
1070            self.graph.setData(self.data, 0, 1)
1071
1072    def onDataChanged(self):
1073        self.dataChangedFlag = True
1074
1075    def commitIf(self):
1076        if self.commitOnChange and self.dataChangedFlag:
1077            self.commit()
1078        else:
1079            self.dataChangedFlag = True
1080
1081    def commit(self):
1082        data = self.graph.data
1083        values = set([str(ex.getclass()) for ex in data])
1084        if len(values) == 1:
1085            # Remove the useless class variable.
1086            domain = Orange.data.Domain(data.domain.attributes, None)
1087            data = Orange.data.Table(domain, data)
1088        self.send("Data", data)
1089
1090
1091if __name__ == "__main__":
1092    app = QApplication(sys.argv)
1093    w = OWPaintData()
1094    w.show()
1095    app.exec_()
Note: See TracBrowser for help on using the repository browser.