Exemplo n.º 1
0
    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QWidget], Any)-> None
        super().__init__(parent, **kwargs)
        self.__pages = []  # type: List[_ToolBoxPage]
        self.__tabButtonHeight = -1
        self.__tabIconSize = QSize()
        self.__exclusive = False
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = QScrollArea(
            self,
            objectName="toolbox-scroll-area",
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                   QSizePolicy.MinimumExpanding),
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            widgetResizable=True,
        )
        sb = ScrollBar()
        sb.styleChange.connect(self.updateGeometry)
        self.__scrollArea.setVerticalScrollBar(sb)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")
        self.__contentsLayout = _ToolBoxLayout(
            sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize, spacing=0)
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = QActionGroup(
            self,
            objectName="toolbox-tab-action-group",
        )
        self.__tabActionGroup.setExclusive(self.__exclusive)
        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToggled)
Exemplo n.º 2
0
    def contextMenuEvent(self, event):
        if event.modifiers() & Qt.AltModifier:
            menu = QMenu(event.widget())
            menu.setAttribute(Qt.WA_DeleteOnClose)
            formatmenu = menu.addMenu("Render as")
            group = QActionGroup(self, exclusive=True)

            def makeaction(text, parent, data=None, **kwargs):
                action = QAction(text, parent, **kwargs)
                if data is not None:
                    action.setData(data)
                return action

            formatactions = [
                makeaction(
                    "Plain Text",
                    group,
                    checkable=True,
                    toolTip=self.tr("Render contents as plain text"),
                    data="text/plain",
                ),
                makeaction(
                    "HTML",
                    group,
                    checkable=True,
                    toolTip=self.tr("Render contents as HTML"),
                    data="text/html",
                ),
                makeaction(
                    "RST",
                    group,
                    checkable=True,
                    toolTip=self.tr("Render contents as RST " "(reStructuredText)"),
                    data="text/rst",
                ),
                makeaction(
                    "Markdown",
                    group,
                    checkable=True,
                    toolTip=self.tr("Render contents as Markdown"),
                    data="text/markdown",
                ),
            ]
            for action in formatactions:
                action.setChecked(action.data() == self.__contentType.lower())
                formatmenu.addAction(action)

            def ontriggered(action):
                mimetype = action.data()
                content = self.content()
                self.setContent(content, mimetype)
                self.editingFinished.emit()

            menu.triggered.connect(ontriggered)
            menu.popup(event.screenPos())
            event.accept()
        else:
            event.ignore()
Exemplo n.º 3
0
    def __setupUi(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = \
                _ToolBoxScrollArea(self, objectName="toolbox-scroll-area")

        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
        self.__scrollArea.setWidgetResizable(True)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")

        # The layout where all the tab/pages are placed
        self.__contentsLayout = QVBoxLayout()
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contentsLayout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
        self.__contentsLayout.setSpacing(0)

        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = \
                QActionGroup(self, objectName="toolbox-tab-action-group")

        self.__tabActionGroup.setExclusive(self.__exclusive)

        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)
Exemplo n.º 4
0
    def __setupUi(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = \
                _ToolBoxScrollArea(self, objectName="toolbox-scroll-area")

        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
        self.__scrollArea.setWidgetResizable(True)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")

        # The layout where all the tab/pages are placed
        self.__contentsLayout = QVBoxLayout()
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contentsLayout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
        self.__contentsLayout.setSpacing(0)

        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = \
                QActionGroup(self, objectName="toolbox-tab-action-group")

        self.__tabActionGroup.setExclusive(self.__exclusive)

        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)
Exemplo n.º 5
0
class ToolBox(QFrame):
    """
    A tool box widget.
    """
    # Emitted when a tab is toggled.
    tabToogled = Signal(int, bool)

    def setExclusive(self, exclusive):
        """
        Set exclusive tabs (only one tab can be open at a time).
        """
        if self.__exclusive != exclusive:
            self.__exclusive = exclusive
            self.__tabActionGroup.setExclusive(exclusive)
            checked = self.__tabActionGroup.checkedAction()
            if checked is None:
                # The action group can be out of sync with the actions state
                # when switching between exclusive states.
                actions_checked = [page.action for page in self.__pages
                                   if page.action.isChecked()]
                if actions_checked:
                    checked = actions_checked[0]

            # Trigger/toggle remaining open pages
            if exclusive and checked is not None:
                for page in self.__pages:
                    if checked != page.action and page.action.isChecked():
                        page.action.trigger()

    def exclusive(self):
        """
        Are the tabs in the toolbox exclusive.
        """
        return self.__exclusive

    exclusive_ = Property(bool,
                          fget=exclusive,
                          fset=setExclusive,
                          designable=True,
                          doc="Exclusive tabs")

    def __init__(self, parent=None, **kwargs):
        QFrame.__init__(self, parent, **kwargs)

        self.__pages = []
        self.__tabButtonHeight = -1
        self.__tabIconSize = QSize()
        self.__exclusive = False
        self.__setupUi()

    def __setupUi(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = \
                _ToolBoxScrollArea(self, objectName="toolbox-scroll-area")

        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
        self.__scrollArea.setWidgetResizable(True)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")
        self.__contentsLayout = _ToolBoxLayout(
            sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize,
            spacing=0
        )
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = \
                QActionGroup(self, objectName="toolbox-tab-action-group")

        self.__tabActionGroup.setExclusive(self.__exclusive)

        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)

    def setTabButtonHeight(self, height):
        """
        Set the tab button height.
        """
        if self.__tabButtonHeight != height:
            self.__tabButtonHeight = height
            for page in self.__pages:
                page.button.setFixedHeight(height)

    def tabButtonHeight(self):
        """
        Return the tab button height.
        """
        return self.__tabButtonHeight

    def setTabIconSize(self, size):
        """
        Set the tab button icon size.
        """
        if self.__tabIconSize != size:
            self.__tabIconSize = size
            for page in self.__pages:
                page.button.setIconSize(size)

    def tabIconSize(self):
        """
        Return the tab icon size.
        """
        return self.__tabIconSize

    def tabButton(self, index):
        """
        Return the tab button at `index`
        """
        return self.__pages[index].button

    def tabAction(self, index):
        """
        Return open/close action for the tab at `index`.
        """
        return self.__pages[index].action

    def addItem(self, widget, text, icon=None, toolTip=None):
        """
        Append the `widget` in a new tab and return its index.

        Parameters
        ----------
        widget : :class:`QWidget`
            A widget to be inserted. The toolbox takes ownership
            of the widget.

        text : str
            Name/title of the new tab.

        icon : :class:`QIcon`, optional
            An icon for the tab button.

        toolTip : str, optional
            Tool tip for the tab button.

        """
        return self.insertItem(self.count(), widget, text, icon, toolTip)

    def insertItem(self, index, widget, text, icon=None, toolTip=None):
        """
        Insert the `widget` in a new tab at position `index`.

        See also
        --------
        ToolBox.addItem

        """
        button = self.createTabButton(widget, text, icon, toolTip)

        self.__contentsLayout.insertWidget(index * 2, button)
        self.__contentsLayout.insertWidget(index * 2 + 1, widget)

        widget.hide()

        page = _ToolBoxPage(index, widget, button.defaultAction(), button)
        self.__pages.insert(index, page)

        for i in range(index + 1, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        self.__updatePositions()

        # Show (open) the first tab.
        if self.count() == 1 and index == 0:
            page.action.trigger()

        self.__updateSelected()

        self.updateGeometry()
        return index

    def removeItem(self, index):
        """
        Remove the widget at `index`.

        .. note:: The widget hidden but is is not deleted.

        """
        self.__contentsLayout.takeAt(2 * index + 1)
        self.__contentsLayout.takeAt(2 * index)
        page = self.__pages.pop(index)

        # Update the page indexes
        for i in range(index, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        page.button.deleteLater()

        # Hide the widget and reparent to self
        # This follows QToolBox.removeItem
        page.widget.hide()
        page.widget.setParent(self)

        self.__updatePositions()
        self.__updateSelected()

        self.updateGeometry()

    def count(self):
        """
        Return the number of widgets inserted in the toolbox.
        """
        return len(self.__pages)

    def widget(self, index):
        """
        Return the widget at `index`.
        """
        return self.__pages[index].widget

    def createTabButton(self, widget, text, icon=None, toolTip=None):
        """
        Create the tab button for `widget`.
        """
        action = QAction(text, self)
        action.setCheckable(True)

        if icon:
            action.setIcon(icon)

        if toolTip:
            action.setToolTip(toolTip)
        self.__tabActionGroup.addAction(action)
        self.__actionMapper.setMapping(action, action)
        action.toggled.connect(self.__actionMapper.map)

        button = ToolBoxTabButton(self, objectName="toolbox-tab-button")
        button.setDefaultAction(action)
        button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        button.setSizePolicy(QSizePolicy.Ignored,
                             QSizePolicy.Fixed)

        if self.__tabIconSize.isValid():
            button.setIconSize(self.__tabIconSize)

        if self.__tabButtonHeight > 0:
            button.setFixedHeight(self.__tabButtonHeight)

        return button

    def ensureWidgetVisible(self, child, xmargin=50, ymargin=50):
        """
        Scroll the contents so child widget instance is visible inside
        the viewport.

        """
        self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin)

    def sizeHint(self):
        hint = self.__contentsLayout.sizeHint()

        if self.count():
            # Compute max width of hidden widgets also.
            scroll = self.__scrollArea
            scroll_w = scroll.verticalScrollBar().sizeHint().width()
            frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2
            max_w = max([p.widget.sizeHint().width() for p in self.__pages])
            hint = QSize(max(max_w, hint.width()) + scroll_w + frame_w,
                         hint.height())

        return QSize(200, 200).expandedTo(hint)

    def __onTabActionToogled(self, action):
        page = find(self.__pages, action, key=attrgetter("action"))
        on = action.isChecked()
        page.widget.setVisible(on)
        index = page.index

        if index > 0:
            # Update the `previous` tab buttons style hints
            previous = self.__pages[index - 1].button
            flag = QStyleOptionToolBox.NextIsSelected
            if on:
                previous.selected |= flag
            else:
                previous.selected &= ~flag

            previous.update()

        if index < self.count() - 1:
            next = self.__pages[index + 1].button
            flag = QStyleOptionToolBox.PreviousIsSelected
            if on:
                next.selected |= flag
            else:
                next.selected &= ~flag

            next.update()

        self.tabToogled.emit(index, on)

        self.__contentsLayout.invalidate()

    def __updateSelected(self):
        """Update the tab buttons selected style flags.
        """
        if self.count() == 0:
            return

        opt = QStyleOptionToolBox

        def update(button, next_sel, prev_sel):
            if next_sel:
                button.selected |= opt.NextIsSelected
            else:
                button.selected &= ~opt.NextIsSelected

            if prev_sel:
                button.selected |= opt.PreviousIsSelected
            else:
                button.selected &= ~ opt.PreviousIsSelected

            button.update()

        if self.count() == 1:
            update(self.__pages[0].button, False, False)
        elif self.count() >= 2:
            pages = self.__pages
            for i in range(1, self.count() - 1):
                update(pages[i].button,
                       pages[i + 1].action.isChecked(),
                       pages[i - 1].action.isChecked())

    def __updatePositions(self):
        """Update the tab buttons position style flags.
        """
        if self.count() == 0:
            return
        elif self.count() == 1:
            self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab
        else:
            self.__pages[0].button.position = QStyleOptionToolBox.Beginning
            self.__pages[-1].button.position = QStyleOptionToolBox.End
            for p in self.__pages[1:-1]:
                p.button.position = QStyleOptionToolBox.Middle

        for p in self.__pages:
            p.button.update()
Exemplo n.º 6
0
    def _init_ui(self):
        namesBox = gui.vBox(self.controlArea, "Names")

        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        gui.lineEdit(hbox,
                     self,
                     "attr1",
                     "Variable X: ",
                     controlWidth=80,
                     orientation=Qt.Horizontal,
                     callback=self._attr_name_changed)
        gui.separator(hbox, 21)
        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        attr2 = gui.lineEdit(hbox,
                             self,
                             "attr2",
                             "Variable Y: ",
                             controlWidth=80,
                             orientation=Qt.Horizontal,
                             callback=self._attr_name_changed)
        gui.separator(hbox)
        gui.checkBox(hbox,
                     self,
                     "hasAttr2",
                     '',
                     disables=attr2,
                     labelWidth=0,
                     callback=self.set_dimensions)

        gui.widgetLabel(namesBox, "Labels")
        self.classValuesView = listView = gui.ListViewWithSizeHint(
            preferred_size=(-1, 30))
        listView.setModel(self.class_model)
        itemmodels.select_row(listView, 0)
        namesBox.layout().addWidget(listView)

        self.addClassLabel = QAction("+",
                                     self,
                                     toolTip="Add new class label",
                                     triggered=self.add_new_class_label)

        self.removeClassLabel = QAction(
            unicodedata.lookup("MINUS SIGN"),
            self,
            toolTip="Remove selected class label",
            triggered=self.remove_selected_class_label)

        actionsWidget = itemmodels.ModelActionsWidget(
            [self.addClassLabel, self.removeClassLabel], self)
        actionsWidget.layout().addStretch(10)
        actionsWidget.layout().setSpacing(1)
        namesBox.layout().addWidget(actionsWidget)

        tBox = gui.vBox(self.buttonsArea, "Tools")
        toolsBox = gui.widgetBox(tBox, orientation=QGridLayout())

        self.toolActions = QActionGroup(self)
        self.toolActions.setExclusive(True)
        self.toolButtons = []

        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
            action = QAction(
                name,
                self,
                toolTip=tooltip,
                checkable=tool.checkable,
                icon=QIcon(icon),
            )
            action.triggered.connect(partial(self.set_current_tool, tool))

            button = QToolButton(iconSize=QSize(24, 24),
                                 toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                                 sizePolicy=QSizePolicy(
                                     QSizePolicy.MinimumExpanding,
                                     QSizePolicy.Fixed))
            button.setDefaultAction(action)
            self.toolButtons.append((button, tool))

            toolsBox.layout().addWidget(button, i / 3, i % 3)
            self.toolActions.addAction(action)

        for column in range(3):
            toolsBox.layout().setColumnMinimumWidth(column, 10)
            toolsBox.layout().setColumnStretch(column, 1)

        undo = self.undo_stack.createUndoAction(self)
        redo = self.undo_stack.createRedoAction(self)

        undo.setShortcut(QKeySequence.Undo)
        redo.setShortcut(QKeySequence.Redo)

        self.addActions([undo, redo])
        self.undo_stack.indexChanged.connect(self.invalidate)

        indBox = gui.indentedBox(tBox, sep=8)
        form = QFormLayout(formAlignment=Qt.AlignLeft,
                           labelAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
        indBox.layout().addLayout(form)
        slider = gui.hSlider(indBox,
                             self,
                             "brushRadius",
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             addToLayout=False)
        form.addRow("Radius:", slider)

        slider = gui.hSlider(indBox,
                             self,
                             "density",
                             None,
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             addToLayout=False)

        form.addRow("Intensity:", slider)

        slider = gui.hSlider(indBox,
                             self,
                             "symbol_size",
                             None,
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             callback=self.set_symbol_size,
                             addToLayout=False)

        form.addRow("Symbol:", slider)

        self.btResetToInput = gui.button(tBox, self, "Reset to Input Data",
                                         self.reset_to_input)
        self.btResetToInput.setDisabled(True)

        gui.auto_send(self.buttonsArea, self, "autocommit")

        # main area GUI
        viewbox = PaintViewBox(enableMouse=False)
        self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
        self.plotview.sizeHint = lambda: QSize(
            200, 100)  # Minimum size for 1-d painting
        self.plot = self.plotview.getPlotItem()

        axis_color = self.palette().color(QPalette.Text)
        axis_pen = QPen(axis_color)

        tickfont = QFont(self.font())
        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))

        axis = self.plot.getAxis("bottom")
        axis.setLabel(self.attr1)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)

        axis = self.plot.getAxis("left")
        axis.setLabel(self.attr2)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)
        if not self.hasAttr2:
            self.plot.hideAxis('left')

        self.plot.hideButtons()
        self.plot.setXRange(0, 1, padding=0.01)

        self.mainArea.layout().addWidget(self.plotview)

        # enable brush tool
        self.toolActions.actions()[0].setChecked(True)
        self.set_current_tool(self.TOOLS[0][2])

        self.set_dimensions()
Exemplo n.º 7
0
class OWPaintData(OWWidget):
    TOOLS = [("Brush", "Create multiple instances", AirBrushTool,
              _icon("brush.svg")),
             ("Put", "Put individual instances", PutInstanceTool,
              _icon("put.svg")),
             ("Select", "Select and move instances", SelectTool,
              _icon("select-transparent_42px.png")),
             ("Jitter", "Jitter instances", JitterTool, _icon("jitter.svg")),
             ("Magnet", "Attract multiple instances", MagnetTool,
              _icon("magnet.svg")),
             ("Clear", "Clear the plot", ClearTool,
              _icon("../../../icons/Dlg_clear.png"))]

    name = "Paint Data"
    description = "Create data by painting data points on a plane."
    icon = "icons/PaintData.svg"
    priority = 60
    keywords = ["create", "draw"]

    class Inputs:
        data = Input("Data", Table)

    class Outputs:
        data = Output("Data", Table)

    autocommit = Setting(True)
    table_name = Setting("Painted data")
    attr1 = Setting("x")
    attr2 = Setting("y")
    hasAttr2 = Setting(True)

    brushRadius = Setting(75)
    density = Setting(7)
    symbol_size = Setting(10)

    #: current data array (shape=(N, 3)) as presented on the output
    data = Setting(None, schema_only=True)
    labels = Setting(["C1", "C2"], schema_only=True)

    buttons_area_orientation = Qt.Vertical
    graph_name = "plot"

    class Warning(OWWidget.Warning):
        no_input_variables = Msg("Input data has no variables")
        continuous_target = Msg("Numeric target value can not be used.")
        sparse_not_supported = Msg("Sparse data is ignored.")
        renamed_vars = Msg("Some variables have been renamed "
                           "to avoid duplicates.\n{}")

    class Information(OWWidget.Information):
        use_first_two = \
            Msg("Paint Data uses data from the first two attributes.")

    def __init__(self):
        super().__init__()

        self.input_data = None
        self.input_classes = []
        self.input_colors = None
        self.input_has_attr2 = True
        self.current_tool = None
        self._selected_indices = None
        self._scatter_item = None
        #: A private data buffer (can be modified in place). `self.data` is
        #: a copy of this array (as seen when the `invalidate` method is
        #: called
        self.__buffer = None

        self.undo_stack = QUndoStack(self)

        self.class_model = ColoredListModel(
            self.labels,
            self,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)

        self.class_model.dataChanged.connect(self._class_value_changed)
        self.class_model.rowsInserted.connect(self._class_count_changed)
        self.class_model.rowsRemoved.connect(self._class_count_changed)

        # if self.data: raises Deprecation warning in older workflows, where
        # data could be a np.array. This would raise an error in the future.
        if self.data is None or len(self.data) == 0:
            self.data = []
            self.__buffer = np.zeros((0, 3))
        elif isinstance(self.data, np.ndarray):
            self.__buffer = self.data.copy()
            self.data = self.data.tolist()
        else:
            self.__buffer = np.array(self.data)

        self.colors = colorpalettes.DefaultRGBColors
        self.tools_cache = {}

        self._init_ui()
        self.commit()

    def _init_ui(self):
        namesBox = gui.vBox(self.controlArea, "Names")

        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        gui.lineEdit(hbox,
                     self,
                     "attr1",
                     "Variable X: ",
                     controlWidth=80,
                     orientation=Qt.Horizontal,
                     callback=self._attr_name_changed)
        gui.separator(hbox, 21)
        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        attr2 = gui.lineEdit(hbox,
                             self,
                             "attr2",
                             "Variable Y: ",
                             controlWidth=80,
                             orientation=Qt.Horizontal,
                             callback=self._attr_name_changed)
        gui.separator(hbox)
        gui.checkBox(hbox,
                     self,
                     "hasAttr2",
                     '',
                     disables=attr2,
                     labelWidth=0,
                     callback=self.set_dimensions)

        gui.widgetLabel(namesBox, "Labels")
        self.classValuesView = listView = gui.ListViewWithSizeHint(
            preferred_size=(-1, 30))
        listView.setModel(self.class_model)
        itemmodels.select_row(listView, 0)
        namesBox.layout().addWidget(listView)

        self.addClassLabel = QAction("+",
                                     self,
                                     toolTip="Add new class label",
                                     triggered=self.add_new_class_label)

        self.removeClassLabel = QAction(
            unicodedata.lookup("MINUS SIGN"),
            self,
            toolTip="Remove selected class label",
            triggered=self.remove_selected_class_label)

        actionsWidget = itemmodels.ModelActionsWidget(
            [self.addClassLabel, self.removeClassLabel], self)
        actionsWidget.layout().addStretch(10)
        actionsWidget.layout().setSpacing(1)
        namesBox.layout().addWidget(actionsWidget)

        tBox = gui.vBox(self.buttonsArea, "Tools")
        toolsBox = gui.widgetBox(tBox, orientation=QGridLayout())

        self.toolActions = QActionGroup(self)
        self.toolActions.setExclusive(True)
        self.toolButtons = []

        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
            action = QAction(
                name,
                self,
                toolTip=tooltip,
                checkable=tool.checkable,
                icon=QIcon(icon),
            )
            action.triggered.connect(partial(self.set_current_tool, tool))

            button = QToolButton(iconSize=QSize(24, 24),
                                 toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                                 sizePolicy=QSizePolicy(
                                     QSizePolicy.MinimumExpanding,
                                     QSizePolicy.Fixed))
            button.setDefaultAction(action)
            self.toolButtons.append((button, tool))

            toolsBox.layout().addWidget(button, i / 3, i % 3)
            self.toolActions.addAction(action)

        for column in range(3):
            toolsBox.layout().setColumnMinimumWidth(column, 10)
            toolsBox.layout().setColumnStretch(column, 1)

        undo = self.undo_stack.createUndoAction(self)
        redo = self.undo_stack.createRedoAction(self)

        undo.setShortcut(QKeySequence.Undo)
        redo.setShortcut(QKeySequence.Redo)

        self.addActions([undo, redo])
        self.undo_stack.indexChanged.connect(self.invalidate)

        indBox = gui.indentedBox(tBox, sep=8)
        form = QFormLayout(formAlignment=Qt.AlignLeft,
                           labelAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
        indBox.layout().addLayout(form)
        slider = gui.hSlider(indBox,
                             self,
                             "brushRadius",
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             addToLayout=False)
        form.addRow("Radius:", slider)

        slider = gui.hSlider(indBox,
                             self,
                             "density",
                             None,
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             addToLayout=False)

        form.addRow("Intensity:", slider)

        slider = gui.hSlider(indBox,
                             self,
                             "symbol_size",
                             None,
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             callback=self.set_symbol_size,
                             addToLayout=False)

        form.addRow("Symbol:", slider)

        self.btResetToInput = gui.button(tBox, self, "Reset to Input Data",
                                         self.reset_to_input)
        self.btResetToInput.setDisabled(True)

        gui.auto_send(self.buttonsArea, self, "autocommit")

        # main area GUI
        viewbox = PaintViewBox(enableMouse=False)
        self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
        self.plotview.sizeHint = lambda: QSize(
            200, 100)  # Minimum size for 1-d painting
        self.plot = self.plotview.getPlotItem()

        axis_color = self.palette().color(QPalette.Text)
        axis_pen = QPen(axis_color)

        tickfont = QFont(self.font())
        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))

        axis = self.plot.getAxis("bottom")
        axis.setLabel(self.attr1)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)

        axis = self.plot.getAxis("left")
        axis.setLabel(self.attr2)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)
        if not self.hasAttr2:
            self.plot.hideAxis('left')

        self.plot.hideButtons()
        self.plot.setXRange(0, 1, padding=0.01)

        self.mainArea.layout().addWidget(self.plotview)

        # enable brush tool
        self.toolActions.actions()[0].setChecked(True)
        self.set_current_tool(self.TOOLS[0][2])

        self.set_dimensions()

    def set_symbol_size(self):
        if self._scatter_item:
            self._scatter_item.setSize(self.symbol_size)

    def set_dimensions(self):
        if self.hasAttr2:
            self.plot.setYRange(0, 1, padding=0.01)
            self.plot.showAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Minimum)
        else:
            self.plot.setYRange(-.5, .5, padding=0.01)
            self.plot.hideAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Maximum)
        self._replot()
        for button, tool in self.toolButtons:
            if tool.only2d:
                button.setDisabled(not self.hasAttr2)

    @Inputs.data
    def set_data(self, data):
        """Set the input_data and call reset_to_input"""
        def _check_and_set_data(data):
            self.clear_messages()
            if data and data.is_sparse():
                self.Warning.sparse_not_supported()
                return False
            if data:
                if not data.domain.attributes:
                    self.Warning.no_input_variables()
                    data = None
                elif len(data.domain.attributes) > 2:
                    self.Information.use_first_two()
            self.input_data = data
            self.btResetToInput.setDisabled(data is None)
            return bool(data)

        if not _check_and_set_data(data):
            return

        X = np.array([scale(vals) for vals in data.X[:, :2].T]).T
        try:
            y = next(cls for cls in data.domain.class_vars if cls.is_discrete)
        except StopIteration:
            if data.domain.class_vars:
                self.Warning.continuous_target()
            self.input_classes = ["C1"]
            self.input_colors = None
            y = np.zeros(len(data))
        else:
            self.input_classes = y.values
            self.input_colors = y.palette

            y = data[:, y].Y

        self.input_has_attr2 = len(data.domain.attributes) >= 2
        if not self.input_has_attr2:
            self.input_data = np.column_stack((X, np.zeros(len(data)), y))
        else:
            self.input_data = np.column_stack((X, y))
        self.reset_to_input()
        self.unconditional_commit()

    def reset_to_input(self):
        """Reset the painting to input data if present."""
        if self.input_data is None:
            return
        self.undo_stack.clear()

        index = self.selected_class_label()
        if self.input_colors is not None:
            palette = self.input_colors
        else:
            palette = colorpalettes.DefaultRGBColors
        self.colors = palette
        self.class_model.colors = palette
        self.class_model[:] = self.input_classes

        newindex = min(max(index, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

        self.data = self.input_data.tolist()
        self.__buffer = self.input_data.copy()

        prev_attr2 = self.hasAttr2
        self.hasAttr2 = self.input_has_attr2
        if prev_attr2 != self.hasAttr2:
            self.set_dimensions()
        else:  # set_dimensions already calls _replot, no need to call it again
            self._replot()

        self.commit()

    def add_new_class_label(self, undoable=True):

        newlabel = next(label for label in namegen('C', 1)
                        if label not in self.class_model)

        command = SimpleUndoCommand(lambda: self.class_model.append(newlabel),
                                    lambda: self.class_model.__delitem__(-1))
        if undoable:
            self.undo_stack.push(command)
        else:
            command.redo()

    def remove_selected_class_label(self):
        index = self.selected_class_label()

        if index is None:
            return

        label = self.class_model[index]
        mask = self.__buffer[:, 2] == index
        move_mask = self.__buffer[~mask][:, 2] > index

        self.undo_stack.beginMacro("Delete class label")
        self.undo_stack.push(UndoCommand(DeleteIndices(mask), self))
        self.undo_stack.push(UndoCommand(Move((move_mask, 2), -1), self))
        self.undo_stack.push(
            SimpleUndoCommand(lambda: self.class_model.__delitem__(index),
                              lambda: self.class_model.insert(index, label)))
        self.undo_stack.endMacro()

        newindex = min(max(index - 1, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

    def _class_count_changed(self):
        self.labels = list(self.class_model)
        self.removeClassLabel.setEnabled(len(self.class_model) > 1)
        self.addClassLabel.setEnabled(len(self.class_model) < len(self.colors))
        if self.selected_class_label() is None:
            itemmodels.select_row(self.classValuesView, 0)

    def _class_value_changed(self, index, _):
        index = index.row()
        newvalue = self.class_model[index]
        oldvalue = self.labels[index]
        if newvalue != oldvalue:
            self.labels[index] = newvalue


#             command = Command(
#                 lambda: self.class_model.__setitem__(index, newvalue),
#                 lambda: self.class_model.__setitem__(index, oldvalue),
#             )
#             self.undo_stack.push(command)

    def selected_class_label(self):
        rows = self.classValuesView.selectedIndexes()
        if rows:
            return rows[0].row()
        return None

    def set_current_tool(self, tool):
        prev_tool = self.current_tool.__class__

        if self.current_tool is not None:
            self.current_tool.deactivate()
            self.current_tool.editingStarted.disconnect(
                self._on_editing_started)
            self.current_tool.editingFinished.disconnect(
                self._on_editing_finished)
            self.current_tool = None
            self.plot.getViewBox().tool = None

        if tool not in self.tools_cache:
            newtool = tool(self, self.plot)
            self.tools_cache[tool] = newtool
            newtool.issueCommand.connect(self._add_command)

        self.current_tool = tool = self.tools_cache[tool]
        self.plot.getViewBox().tool = tool
        tool.editingStarted.connect(self._on_editing_started)
        tool.editingFinished.connect(self._on_editing_finished)
        tool.activate()

        if not tool.checkable:
            self.set_current_tool(prev_tool)

    def _on_editing_started(self):
        self.undo_stack.beginMacro("macro")

    def _on_editing_finished(self):
        self.undo_stack.endMacro()

    def execute(self, command):
        assert isinstance(command, (Append, DeleteIndices, Insert, Move)), \
            "Non normalized command"
        if isinstance(command, (DeleteIndices, Insert)):
            self._selected_indices = None

            if isinstance(self.current_tool, SelectTool):
                self.current_tool.reset()

        self.__buffer, undo = transform(command, self.__buffer)
        self._replot()
        return undo

    def _add_command(self, cmd):
        # pylint: disable=too-many-branches
        name = "Name"

        if (not self.hasAttr2
                and isinstance(cmd, (Move, MoveSelection, Jitter, Magnet))):
            # tool only supported if both x and y are enabled
            return

        if isinstance(cmd, Append):
            cls = self.selected_class_label()
            points = np.array([(p.x(), p.y() if self.hasAttr2 else 0, cls)
                               for p in cmd.points])
            self.undo_stack.push(UndoCommand(Append(points), self, text=name))
        elif isinstance(cmd, Move):
            self.undo_stack.push(UndoCommand(cmd, self, text=name))
        elif isinstance(cmd, SelectRegion):
            indices = [
                i for i, (x, y) in enumerate(self.__buffer[:, :2])
                if cmd.region.contains(QPointF(x, y))
            ]
            indices = np.array(indices, dtype=int)
            self._selected_indices = indices
        elif isinstance(cmd, DeleteSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(DeleteIndices(indices), self, text="Delete"))
        elif isinstance(cmd, MoveSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(Move((self._selected_indices, slice(0, 2)),
                                     np.array([cmd.delta.x(),
                                               cmd.delta.y()])),
                                self,
                                text="Move"))
        elif isinstance(cmd, DeleteIndices):
            self.undo_stack.push(UndoCommand(cmd, self, text="Delete"))
        elif isinstance(cmd, Insert):
            self.undo_stack.push(UndoCommand(cmd, self))
        elif isinstance(cmd, AirBrush):
            data = create_data(cmd.pos.x(), cmd.pos.y(),
                               self.brushRadius / 1000,
                               int(1 + self.density / 20), cmd.rstate)
            self._add_command(Append([QPointF(*p) for p in zip(*data.T)]))
        elif isinstance(cmd, Jitter):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = -apply_jitter(self.__buffer[:, :2], point,
                                  self.density / 100.0, 0, cmd.rstate)
            self._add_command(Move((..., slice(0, 2)), delta))
        elif isinstance(cmd, Magnet):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = -apply_attractor(self.__buffer[:, :2], point,
                                     self.density / 100.0, 0)
            self._add_command(Move((..., slice(0, 2)), delta))
        else:
            assert False, "unreachable"

    def _replot(self):
        def pen(color):
            pen = QPen(color, 1)
            pen.setCosmetic(True)
            return pen

        if self._scatter_item is not None:
            self.plot.removeItem(self._scatter_item)
            self._scatter_item = None

        x = self.__buffer[:, 0].copy()
        if self.hasAttr2:
            y = self.__buffer[:, 1].copy()
        else:
            y = np.zeros(self.__buffer.shape[0])

        colors = self.colors[self.__buffer[:, 2]]
        pens = [pen(c) for c in colors]
        brushes = [QBrush(c) for c in colors]

        self._scatter_item = pg.ScatterPlotItem(x,
                                                y,
                                                symbol="+",
                                                brush=brushes,
                                                pen=pens)
        self.plot.addItem(self._scatter_item)
        self.set_symbol_size()

    def _attr_name_changed(self):
        self.plot.getAxis("bottom").setLabel(self.attr1)
        self.plot.getAxis("left").setLabel(self.attr2)
        self.invalidate()

    def invalidate(self):
        self.data = self.__buffer.tolist()
        self.commit()

    def commit(self):
        self.Warning.renamed_vars.clear()

        if not self.data:
            self.Outputs.data.send(None)
            return
        data = np.array(self.data)
        if self.hasAttr2:
            X, Y = data[:, :2], data[:, 2]
            proposed = [self.attr1.strip(), self.attr2.strip()]
        else:
            X, Y = data[:, np.newaxis, 0], data[:, 2]
            proposed = [self.attr1.strip()]

        if len(np.unique(Y)) >= 2:
            proposed.append("Class")
            unique_names, renamed = get_unique_names_duplicates(proposed, True)
            domain = Domain((map(ContinuousVariable, unique_names[:-1])),
                            DiscreteVariable(unique_names[-1],
                                             values=tuple(self.class_model)))
            data = Table.from_numpy(domain, X, Y)
        else:
            unique_names, renamed = get_unique_names_duplicates(proposed, True)
            domain = Domain(map(ContinuousVariable, unique_names))
            data = Table.from_numpy(domain, X)

        if renamed:
            self.Warning.renamed_vars(", ".join(renamed))
            self.plot.getAxis("bottom").setLabel(unique_names[0])
            self.plot.getAxis("left").setLabel(unique_names[1])

        data.name = self.table_name
        self.Outputs.data.send(data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(570, 690))

    def onDeleteWidget(self):
        self.undo_stack.indexChanged.disconnect(self.invalidate)
        self.plot.clear()

    def send_report(self):
        if self.data is None:
            return
        settings = []
        if self.attr1 != "x" or self.attr2 != "y":
            settings += [("Axis x", self.attr1), ("Axis y", self.attr2)]
        settings += [("Number of points", len(self.data))]
        self.report_items("Painted data", settings)
        self.report_plot()
Exemplo n.º 8
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        form = self.layout().itemAt(0)
        assert isinstance(form, QFormLayout)

        #: A list model of discrete variable's values.
        self.values_model = itemmodels.PyListModel(
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)

        vlayout = QVBoxLayout(spacing=1, margin=0)
        self.values_edit = QListView(editTriggers=QListView.DoubleClicked
                                     | QListView.EditKeyPressed)
        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))
        self.values_edit.setModel(self.values_model)
        self.values_model.dataChanged.connect(self.on_values_changed)

        self.values_edit.selectionModel().selectionChanged.connect(
            self.on_value_selection_changed)
        self.values_model.layoutChanged.connect(
            self.on_value_selection_changed)
        self.values_model.rowsMoved.connect(self.on_value_selection_changed)

        vlayout.addWidget(self.values_edit)
        hlayout = QHBoxLayout(spacing=1, margin=0)

        self.categories_action_group = group = QActionGroup(
            self, objectName="action-group-categories", enabled=False)
        self.move_value_up = QAction(
            "\N{UPWARDS ARROW}",
            group,
            toolTip="Move the selected item up.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier
                                  | Qt.Key_BracketLeft),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_up.triggered.connect(self.move_up)

        self.move_value_down = QAction(
            "\N{DOWNWARDS ARROW}",
            group,
            toolTip="Move the selected item down.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier
                                  | Qt.Key_BracketRight),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_down.triggered.connect(self.move_down)

        self.add_new_item = QAction(
            "+",
            group,
            objectName="action-add-item",
            toolTip="Append a new item.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.remove_item = QAction(
            "\N{MINUS SIGN}",
            group,
            objectName="action-remove-item",
            toolTip="Delete the selected item.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut,
        )

        self.add_new_item.triggered.connect(self._add_category)
        self.remove_item.triggered.connect(self._remove_category)

        button1 = FixedSizeButton(self,
                                  defaultAction=self.move_value_up,
                                  accessibleName="Move up")
        button2 = FixedSizeButton(self,
                                  defaultAction=self.move_value_down,
                                  accessibleName="Move down")
        button3 = FixedSizeButton(self,
                                  defaultAction=self.add_new_item,
                                  accessibleName="Add")
        button4 = FixedSizeButton(self,
                                  defaultAction=self.remove_item,
                                  accessibleName="Remove")
        self.values_edit.addActions([
            self.move_value_up, self.move_value_down, self.add_new_item,
            self.remove_item
        ])
        hlayout.addWidget(button1)
        hlayout.addWidget(button2)
        hlayout.addSpacing(3)
        hlayout.addWidget(button3)
        hlayout.addWidget(button4)

        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        form.insertRow(1, "Values:", vlayout)

        QWidget.setTabOrder(self.name_edit, self.values_edit)
        QWidget.setTabOrder(self.values_edit, button1)
        QWidget.setTabOrder(button1, button2)
        QWidget.setTabOrder(button2, button3)
        QWidget.setTabOrder(button3, button4)
Exemplo n.º 9
0
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.var = None  # type: Optional[Variable]

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.form = form = QFormLayout(
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
            objectName="editor-form-layout")
        layout.addLayout(self.form)

        self.name_edit = QLineEdit(objectName="name-editor")
        self.name_edit.editingFinished.connect(
            lambda: self.name_edit.isModified() and self.on_name_changed())
        form.addRow("Name:", self.name_edit)

        vlayout = QVBoxLayout(margin=0, spacing=1)
        self.labels_edit = view = QTreeView(
            objectName="annotation-pairs-edit",
            rootIsDecorated=False,
            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,
        )
        self.labels_model = model = DictItemsModel()
        view.setModel(model)

        view.selectionModel().selectionChanged.connect(
            self.on_label_selection_changed)

        agrp = QActionGroup(view, objectName="annotate-action-group")
        action_add = QAction("+",
                             self,
                             objectName="action-add-label",
                             toolTip="Add a new label.",
                             shortcut=QKeySequence(QKeySequence.New),
                             shortcutContext=Qt.WidgetShortcut)
        action_delete = QAction("\N{MINUS SIGN}",
                                self,
                                objectName="action-delete-label",
                                toolTip="Remove selected label.",
                                shortcut=QKeySequence(QKeySequence.Delete),
                                shortcutContext=Qt.WidgetShortcut)
        agrp.addAction(action_add)
        agrp.addAction(action_delete)
        view.addActions([action_add, action_delete])

        def add_label():
            row = [QStandardItem(), QStandardItem()]
            model.appendRow(row)
            idx = model.index(model.rowCount() - 1, 0)
            view.setCurrentIndex(idx)
            view.edit(idx)

        def remove_label():
            rows = view.selectionModel().selectedRows(0)
            if rows:
                assert len(rows) == 1
                idx = rows[0].row()
                model.removeRow(idx)

        action_add.triggered.connect(add_label)
        action_delete.triggered.connect(remove_label)
        agrp.setEnabled(False)

        self.add_label_action = action_add
        self.remove_label_action = action_delete

        # Necessary signals to know when the labels change
        model.dataChanged.connect(self.on_labels_changed)
        model.rowsInserted.connect(self.on_labels_changed)
        model.rowsRemoved.connect(self.on_labels_changed)

        vlayout.addWidget(self.labels_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        button = FixedSizeButton(
            self,
            defaultAction=self.add_label_action,
            accessibleName="Add",
        )
        hlayout.addWidget(button)

        button = FixedSizeButton(
            self,
            defaultAction=self.remove_label_action,
            accessibleName="Remove",
        )

        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)
        form.addRow("Labels:", vlayout)
Exemplo n.º 10
0
    def _init_ui(self):
        namesBox = gui.vBox(self.controlArea, "Names")

        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        gui.lineEdit(hbox, self, "attr1", "Variable X: ",
                     controlWidth=80, orientation=Qt.Horizontal,
                     callback=self._attr_name_changed)
        gui.separator(hbox, 21)
        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        attr2 = gui.lineEdit(hbox, self, "attr2", "Variable Y: ",
                             controlWidth=80, orientation=Qt.Horizontal,
                             callback=self._attr_name_changed)
        gui.separator(hbox)
        gui.checkBox(hbox, self, "hasAttr2", '', disables=attr2,
                     labelWidth=0,
                     callback=self.set_dimensions)
        gui.separator(namesBox)

        gui.widgetLabel(namesBox, "Labels")
        self.classValuesView = listView = QListView(
            selectionMode=QListView.SingleSelection,
            sizePolicy=QSizePolicy(QSizePolicy.Ignored,
                                   QSizePolicy.Maximum)
        )
        listView.setModel(self.class_model)
        itemmodels.select_row(listView, 0)
        namesBox.layout().addWidget(listView)

        self.addClassLabel = QAction(
            "+", self,
            toolTip="Add new class label",
            triggered=self.add_new_class_label
        )

        self.removeClassLabel = QAction(
            unicodedata.lookup("MINUS SIGN"), self,
            toolTip="Remove selected class label",
            triggered=self.remove_selected_class_label
        )

        actionsWidget = itemmodels.ModelActionsWidget(
            [self.addClassLabel, self.removeClassLabel], self
        )
        actionsWidget.layout().addStretch(10)
        actionsWidget.layout().setSpacing(1)
        namesBox.layout().addWidget(actionsWidget)

        tBox = gui.vBox(self.controlArea, "Tools", addSpace=True)
        buttonBox = gui.hBox(tBox)
        toolsBox = gui.widgetBox(buttonBox, orientation=QGridLayout())

        self.toolActions = QActionGroup(self)
        self.toolActions.setExclusive(True)
        self.toolButtons = []

        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
            action = QAction(
                name, self,
                toolTip=tooltip,
                checkable=tool.checkable,
                icon=QIcon(icon),
            )
            action.triggered.connect(partial(self.set_current_tool, tool))

            button = QToolButton(
                iconSize=QSize(24, 24),
                toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                       QSizePolicy.Fixed)
            )
            button.setDefaultAction(action)
            self.toolButtons.append((button, tool))

            toolsBox.layout().addWidget(button, i / 3, i % 3)
            self.toolActions.addAction(action)

        for column in range(3):
            toolsBox.layout().setColumnMinimumWidth(column, 10)
            toolsBox.layout().setColumnStretch(column, 1)

        undo = self.undo_stack.createUndoAction(self)
        redo = self.undo_stack.createRedoAction(self)

        undo.setShortcut(QKeySequence.Undo)
        redo.setShortcut(QKeySequence.Redo)

        self.addActions([undo, redo])
        self.undo_stack.indexChanged.connect(lambda _: self.invalidate())

        gui.separator(tBox)
        indBox = gui.indentedBox(tBox, sep=8)
        form = QFormLayout(
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        indBox.layout().addLayout(form)
        slider = gui.hSlider(
            indBox, self, "brushRadius", minValue=1, maxValue=100,
            createLabel=False
        )
        form.addRow("Radius:", slider)

        slider = gui.hSlider(
            indBox, self, "density", None, minValue=1, maxValue=100,
            createLabel=False
        )

        form.addRow("Intensity:", slider)
        self.btResetToInput = gui.button(
            tBox, self, "Reset to Input Data", self.reset_to_input)
        self.btResetToInput.setDisabled(True)

        gui.rubber(self.controlArea)
        gui.auto_commit(self.left_side, self, "autocommit",
                        "Send")

        # main area GUI
        viewbox = PaintViewBox(enableMouse=False)
        self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
        self.plotview.sizeHint = lambda: QSize(200, 100)  # Minimum size for 1-d painting
        self.plot = self.plotview.getPlotItem()

        axis_color = self.palette().color(QPalette.Text)
        axis_pen = QPen(axis_color)

        tickfont = QFont(self.font())
        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))

        axis = self.plot.getAxis("bottom")
        axis.setLabel(self.attr1)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)

        axis = self.plot.getAxis("left")
        axis.setLabel(self.attr2)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)
        if not self.hasAttr2:
            self.plot.hideAxis('left')

        self.plot.hideButtons()
        self.plot.setXRange(0, 1, padding=0.01)

        self.mainArea.layout().addWidget(self.plotview)

        # enable brush tool
        self.toolActions.actions()[0].setChecked(True)
        self.set_current_tool(self.TOOLS[0][2])

        self.set_dimensions()
Exemplo n.º 11
0
class OWPaintData(OWWidget):
    TOOLS = [
        ("Brush", "Create multiple instances", AirBrushTool, _i("brush.svg")),
        ("Put", "Put individual instances", PutInstanceTool, _i("put.svg")),
        ("Select", "Select and move instances", SelectTool,
            _i("select-transparent_42px.png")),
        ("Jitter", "Jitter instances", JitterTool, _i("jitter.svg")),
        ("Magnet", "Attract multiple instances", MagnetTool, _i("magnet.svg")),
        ("Clear", "Clear the plot", ClearTool, _i("../../../icons/Dlg_clear.png"))
    ]

    name = "Paint Data"
    description = "Create data by painting data points on a plane."
    icon = "icons/PaintData.svg"
    priority = 15
    keywords = ["data", "paint", "create"]

    outputs = [("Data", Orange.data.Table)]
    inputs = [("Data", Orange.data.Table, "set_data")]

    autocommit = Setting(False)
    table_name = Setting("Painted data")
    attr1 = Setting("x")
    attr2 = Setting("y")
    hasAttr2 = Setting(True)

    brushRadius = Setting(75)
    density = Setting(7)

    data = Setting(None, schema_only=True)

    graph_name = "plot"

    class Warning(OWWidget.Warning):
        no_input_variables = Msg("Input data has no variables")
        continuous_target = Msg("Continuous target value can not be used.")

    class Information(OWWidget.Information):
        use_first_two = \
            Msg("Paint Data uses data from the first two attributes.")

    def __init__(self):
        super().__init__()

        self.input_data = None
        self.input_classes = []
        self.input_has_attr2 = True
        self.current_tool = None
        self._selected_indices = None
        self._scatter_item = None

        self.labels = ["C1", "C2"]

        self.undo_stack = QUndoStack(self)

        self.class_model = ColoredListModel(
            self.labels, self,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled |
                  Qt.ItemIsEditable)

        self.class_model.dataChanged.connect(self._class_value_changed)
        self.class_model.rowsInserted.connect(self._class_count_changed)
        self.class_model.rowsRemoved.connect(self._class_count_changed)

        if self.data is None:
            self.data = np.zeros((0, 3))
        self.colors = colorpalette.ColorPaletteGenerator(
            len(colorpalette.DefaultRGBColors))
        self.tools_cache = {}

        self._init_ui()
        self.commit()

    def _init_ui(self):
        namesBox = gui.vBox(self.controlArea, "Names")

        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        gui.lineEdit(hbox, self, "attr1", "Variable X: ",
                     controlWidth=80, orientation=Qt.Horizontal,
                     callback=self._attr_name_changed)
        gui.separator(hbox, 21)
        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        attr2 = gui.lineEdit(hbox, self, "attr2", "Variable Y: ",
                             controlWidth=80, orientation=Qt.Horizontal,
                             callback=self._attr_name_changed)
        gui.separator(hbox)
        gui.checkBox(hbox, self, "hasAttr2", '', disables=attr2,
                     labelWidth=0,
                     callback=self.set_dimensions)
        gui.separator(namesBox)

        gui.widgetLabel(namesBox, "Labels")
        self.classValuesView = listView = QListView(
            selectionMode=QListView.SingleSelection,
            sizePolicy=QSizePolicy(QSizePolicy.Ignored,
                                   QSizePolicy.Maximum)
        )
        listView.setModel(self.class_model)
        itemmodels.select_row(listView, 0)
        namesBox.layout().addWidget(listView)

        self.addClassLabel = QAction(
            "+", self,
            toolTip="Add new class label",
            triggered=self.add_new_class_label
        )

        self.removeClassLabel = QAction(
            unicodedata.lookup("MINUS SIGN"), self,
            toolTip="Remove selected class label",
            triggered=self.remove_selected_class_label
        )

        actionsWidget = itemmodels.ModelActionsWidget(
            [self.addClassLabel, self.removeClassLabel], self
        )
        actionsWidget.layout().addStretch(10)
        actionsWidget.layout().setSpacing(1)
        namesBox.layout().addWidget(actionsWidget)

        tBox = gui.vBox(self.controlArea, "Tools", addSpace=True)
        buttonBox = gui.hBox(tBox)
        toolsBox = gui.widgetBox(buttonBox, orientation=QGridLayout())

        self.toolActions = QActionGroup(self)
        self.toolActions.setExclusive(True)
        self.toolButtons = []

        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
            action = QAction(
                name, self,
                toolTip=tooltip,
                checkable=tool.checkable,
                icon=QIcon(icon),
            )
            action.triggered.connect(partial(self.set_current_tool, tool))

            button = QToolButton(
                iconSize=QSize(24, 24),
                toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                       QSizePolicy.Fixed)
            )
            button.setDefaultAction(action)
            self.toolButtons.append((button, tool))

            toolsBox.layout().addWidget(button, i / 3, i % 3)
            self.toolActions.addAction(action)

        for column in range(3):
            toolsBox.layout().setColumnMinimumWidth(column, 10)
            toolsBox.layout().setColumnStretch(column, 1)

        undo = self.undo_stack.createUndoAction(self)
        redo = self.undo_stack.createRedoAction(self)

        undo.setShortcut(QKeySequence.Undo)
        redo.setShortcut(QKeySequence.Redo)

        self.addActions([undo, redo])
        self.undo_stack.indexChanged.connect(lambda _: self.invalidate())

        gui.separator(tBox)
        indBox = gui.indentedBox(tBox, sep=8)
        form = QFormLayout(
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        indBox.layout().addLayout(form)
        slider = gui.hSlider(
            indBox, self, "brushRadius", minValue=1, maxValue=100,
            createLabel=False
        )
        form.addRow("Radius:", slider)

        slider = gui.hSlider(
            indBox, self, "density", None, minValue=1, maxValue=100,
            createLabel=False
        )

        form.addRow("Intensity:", slider)
        self.btResetToInput = gui.button(
            tBox, self, "Reset to Input Data", self.reset_to_input)
        self.btResetToInput.setDisabled(True)

        gui.rubber(self.controlArea)
        gui.auto_commit(self.left_side, self, "autocommit",
                        "Send")

        # main area GUI
        viewbox = PaintViewBox(enableMouse=False)
        self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
        self.plotview.sizeHint = lambda: QSize(200, 100)  # Minimum size for 1-d painting
        self.plot = self.plotview.getPlotItem()

        axis_color = self.palette().color(QPalette.Text)
        axis_pen = QPen(axis_color)

        tickfont = QFont(self.font())
        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))

        axis = self.plot.getAxis("bottom")
        axis.setLabel(self.attr1)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)

        axis = self.plot.getAxis("left")
        axis.setLabel(self.attr2)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)
        if not self.hasAttr2:
            self.plot.hideAxis('left')

        self.plot.hideButtons()
        self.plot.setXRange(0, 1, padding=0.01)

        self.mainArea.layout().addWidget(self.plotview)

        # enable brush tool
        self.toolActions.actions()[0].setChecked(True)
        self.set_current_tool(self.TOOLS[0][2])

        self.set_dimensions()

    def set_dimensions(self):
        if self.hasAttr2:
            self.plot.setYRange(0, 1, padding=0.01)
            self.plot.showAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
        else:
            self.plot.setYRange(-.5, .5, padding=0.01)
            self.plot.hideAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum)
        self._replot()
        for button, tool in self.toolButtons:
            if tool.only2d:
                button.setDisabled(not self.hasAttr2)

    def set_data(self, data):
        """Set the input_data and call reset_to_input"""
        def _check_and_set_data(data):
            self.clear_messages()
            if data is not None:
                if not data.domain.attributes:
                    self.Warning.no_input_variables()
                    data = None
                elif len(data.domain.attributes) > 2:
                    self.Information.use_first_two()
            self.input_data = data
            self.btResetToInput.setDisabled(data is None)
            return data is not None

        if not _check_and_set_data(data):
            return

        X = np.array([scale(vals) for vals in data.X[:, :2].T]).T
        try:
            y = next(cls for cls in data.domain.class_vars if cls.is_discrete)
        except StopIteration:
            if data.domain.class_vars:
                self.Warning.continuous_target()
            self.input_classes = ["C1"]
            y = np.zeros(len(data))
        else:
            self.input_classes = y.values
            y = data[:, y].Y

        self.input_has_attr2 = len(data.domain.attributes) >= 2
        if not self.input_has_attr2:
            self.input_data = np.column_stack((X, np.zeros(len(data)), y))
        else:
            self.input_data = np.column_stack((X, y))
        self.reset_to_input()

    def reset_to_input(self):
        """Reset the painting to input data if present."""
        if self.input_data is None:
            return
        self.undo_stack.clear()

        index = self.selected_class_label()
        self.class_model[:] = self.input_classes
        newindex = min(max(index, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

        self.data = self.input_data
        prev_attr2 = self.hasAttr2
        self.hasAttr2 = self.input_has_attr2
        if prev_attr2 != self.hasAttr2:
            self.set_dimensions()
        else:  # set_dimensions already calls _replot, no need to call it again
            self._replot()

    def add_new_class_label(self, undoable=True):

        newlabel = next(label for label in namegen('C', 1)
                        if label not in self.class_model)

        command = SimpleUndoCommand(
            lambda: self.class_model.append(newlabel),
            lambda: self.class_model.__delitem__(-1)
        )
        if undoable:
            self.undo_stack.push(command)
        else:
            command.redo()

    def remove_selected_class_label(self):
        index = self.selected_class_label()

        if index is None:
            return

        label = self.class_model[index]
        mask = self.data[:, 2] == index
        move_mask = self.data[~mask][:, 2] > index

        self.undo_stack.beginMacro("Delete class label")
        self.undo_stack.push(UndoCommand(DeleteIndices(mask), self))
        self.undo_stack.push(UndoCommand(Move((move_mask, 2), -1), self))
        self.undo_stack.push(
            SimpleUndoCommand(lambda: self.class_model.__delitem__(index),
                              lambda: self.class_model.insert(index, label)))
        self.undo_stack.endMacro()

        newindex = min(max(index - 1, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

    def _class_count_changed(self):
        self.labels = list(self.class_model)
        self.removeClassLabel.setEnabled(len(self.class_model) > 1)
        self.addClassLabel.setEnabled(
            len(self.class_model) < self.colors.number_of_colors)
        if self.selected_class_label() is None:
            itemmodels.select_row(self.classValuesView, 0)

    def _class_value_changed(self, index, _):
        index = index.row()
        newvalue = self.class_model[index]
        oldvalue = self.labels[index]
        if newvalue != oldvalue:
            self.labels[index] = newvalue
#             command = Command(
#                 lambda: self.class_model.__setitem__(index, newvalue),
#                 lambda: self.class_model.__setitem__(index, oldvalue),
#             )
#             self.undo_stack.push(command)

    def selected_class_label(self):
        rows = self.classValuesView.selectedIndexes()
        if rows:
            return rows[0].row()
        else:
            return None

    def set_current_tool(self, tool):
        prev_tool = self.current_tool.__class__

        if self.current_tool is not None:
            self.current_tool.deactivate()
            self.current_tool.editingStarted.disconnect(
                self._on_editing_started)
            self.current_tool.editingFinished.disconnect(
                self._on_editing_finished)
            self.current_tool = None
            self.plot.getViewBox().tool = None

        if tool not in self.tools_cache:
            newtool = tool(self, self.plot)
            self.tools_cache[tool] = newtool
            newtool.issueCommand.connect(self._add_command)

        self._selected_region = QRectF()
        self.current_tool = tool = self.tools_cache[tool]
        self.plot.getViewBox().tool = tool
        tool.editingStarted.connect(self._on_editing_started)
        tool.editingFinished.connect(self._on_editing_finished)
        tool.activate()

        if not tool.checkable:
            self.set_current_tool(prev_tool)

    def _on_editing_started(self):
        self.undo_stack.beginMacro("macro")

    def _on_editing_finished(self):
        self.undo_stack.endMacro()

    def execute(self, command):
        if isinstance(command, (Append, DeleteIndices, Insert, Move)):
            if isinstance(command, (DeleteIndices, Insert)):
                self._selected_indices = None

                if isinstance(self.current_tool, SelectTool):
                    self.current_tool._reset()

            self.data, undo = transform(command, self.data)
            self._replot()
            return undo
        else:
            assert False, "Non normalized command"

    def _add_command(self, cmd):
        name = "Name"

        if (not self.hasAttr2 and
            isinstance(cmd, (Move, MoveSelection, Jitter, Magnet))):
            # tool only supported if both x and y are enabled
            return

        if isinstance(cmd, Append):
            cls = self.selected_class_label()
            points = np.array([(p.x(), p.y() if self.hasAttr2 else 0, cls)
                                  for p in cmd.points])
            self.undo_stack.push(UndoCommand(Append(points), self, text=name))
        elif isinstance(cmd, Move):
            self.undo_stack.push(UndoCommand(cmd, self, text=name))
        elif isinstance(cmd, SelectRegion):
            indices = [i for i, (x, y) in enumerate(self.data[:, :2])
                       if cmd.region.contains(QPointF(x, y))]
            indices = np.array(indices, dtype=int)
            self._selected_indices = indices
        elif isinstance(cmd, DeleteSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(DeleteIndices(indices), self, text="Delete")
                )
        elif isinstance(cmd, MoveSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(
                        Move((self._selected_indices, slice(0, 2)),
                             np.array([cmd.delta.x(), cmd.delta.y()])),
                        self, text="Move")
                )
        elif isinstance(cmd, DeleteIndices):
            self.undo_stack.push(UndoCommand(cmd, self, text="Delete"))
        elif isinstance(cmd, Insert):
            self.undo_stack.push(UndoCommand(cmd, self))
        elif isinstance(cmd, AirBrush):
            data = create_data(cmd.pos.x(), cmd.pos.y(),
                               self.brushRadius / 1000,
                               int(1 + self.density / 20), cmd.rstate)
            self._add_command(Append([QPointF(*p) for p in zip(*data.T)]))
        elif isinstance(cmd, Jitter):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = - apply_jitter(self.data[:, :2], point,
                                   self.density / 100.0, 0, cmd.rstate)
            self._add_command(Move((..., slice(0, 2)), delta))
        elif isinstance(cmd, Magnet):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = - apply_attractor(self.data[:, :2], point,
                                      self.density / 100.0, 0)
            self._add_command(Move((..., slice(0, 2)), delta))
        else:
            assert False, "unreachable"

    def _replot(self):
        def pen(color):
            pen = QPen(color, 1)
            pen.setCosmetic(True)
            return pen

        if self._scatter_item is not None:
            self.plot.removeItem(self._scatter_item)
            self._scatter_item = None

        nclasses = len(self.class_model)
        pens = [pen(self.colors[i]) for i in range(nclasses)]

        self._scatter_item = pg.ScatterPlotItem(
            self.data[:, 0],
            self.data[:, 1] if self.hasAttr2 else np.zeros(self.data.shape[0]),
            symbol="+",
            pen=[pens[int(ci)] for ci in self.data[:, 2]]
        )

        self.plot.addItem(self._scatter_item)

    def _attr_name_changed(self):
        self.plot.getAxis("bottom").setLabel(self.attr1)
        self.plot.getAxis("left").setLabel(self.attr2)
        self.invalidate()

    def invalidate(self):
        self.commit()

    def commit(self):
        if len(self.data) == 0:
            self.send("Data", None)
            return
        if self.hasAttr2:
            X, Y = self.data[:, :2], self.data[:, 2]
            attrs = (Orange.data.ContinuousVariable(self.attr1),
                     Orange.data.ContinuousVariable(self.attr2))
        else:
            X, Y = self.data[:, np.newaxis, 0], self.data[:, 2]
            attrs = (Orange.data.ContinuousVariable(self.attr1),)
        if len(np.unique(Y)) >= 2:
            domain = Orange.data.Domain(
                attrs,
                Orange.data.DiscreteVariable(
                    "Class", values=list(self.class_model))
            )
            data = Orange.data.Table.from_numpy(domain, X, Y)
        else:
            domain = Orange.data.Domain(attrs)
            data = Orange.data.Table.from_numpy(domain, X)
        data.name = self.table_name
        self.send("Data", data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(1200, 800))

    def onDeleteWidget(self):
        self.plot.clear()

    def send_report(self):
        if self.data is None:
            return
        settings = []
        if self.attr1 != "x" or self.attr2 != "y":
            settings += [("Axis x", self.attr1), ("Axis y", self.attr2)]
        settings += [("Number of points", len(self.data))]
        self.report_items("Painted data", settings)
        self.report_plot()
Exemplo n.º 12
0
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.var = None  # type: Optional[Variable]

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.form = form = QFormLayout(
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
            objectName="editor-form-layout"
        )
        layout.addLayout(self.form)

        self.name_edit = QLineEdit(objectName="name-editor")
        self.name_edit.editingFinished.connect(
            lambda: self.name_edit.isModified() and self.on_name_changed()
        )
        form.addRow("Name:", self.name_edit)

        vlayout = QVBoxLayout(margin=0, spacing=1)
        self.labels_edit = view = QTreeView(
            objectName="annotation-pairs-edit",
            rootIsDecorated=False,
            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,
        )
        self.labels_model = model = DictItemsModel()
        view.setModel(model)

        view.selectionModel().selectionChanged.connect(
            self.on_label_selection_changed)

        agrp = QActionGroup(view, objectName="annotate-action-group")
        action_add = QAction(
            "+", self, objectName="action-add-label",
            toolTip="Add a new label.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut
        )
        action_delete = QAction(
            "\N{MINUS SIGN}", self, objectName="action-delete-label",
            toolTip="Remove selected label.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut
        )
        agrp.addAction(action_add)
        agrp.addAction(action_delete)
        view.addActions([action_add, action_delete])

        def add_label():
            row = [QStandardItem(), QStandardItem()]
            model.appendRow(row)
            idx = model.index(model.rowCount() - 1, 0)
            view.setCurrentIndex(idx)
            view.edit(idx)

        def remove_label():
            rows = view.selectionModel().selectedRows(0)
            if rows:
                assert len(rows) == 1
                idx = rows[0].row()
                model.removeRow(idx)

        action_add.triggered.connect(add_label)
        action_delete.triggered.connect(remove_label)
        agrp.setEnabled(False)

        self.add_label_action = action_add
        self.remove_label_action = action_delete

        # Necessary signals to know when the labels change
        model.dataChanged.connect(self.on_labels_changed)
        model.rowsInserted.connect(self.on_labels_changed)
        model.rowsRemoved.connect(self.on_labels_changed)

        vlayout.addWidget(self.labels_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        button = FixedSizeButton(
            self, defaultAction=self.add_label_action,
            accessibleName="Add",
        )
        hlayout.addWidget(button)

        button = FixedSizeButton(
            self, defaultAction=self.remove_label_action,
            accessibleName="Remove",
        )

        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)
        form.addRow("Labels:", vlayout)
Exemplo n.º 13
0
class ToolBox(QFrame):
    """
    A tool box widget.
    """
    # Emitted when a tab is toggled.
    tabToogled = Signal(int, bool)

    def setExclusive(self, exclusive):
        """
        Set exclusive tabs (only one tab can be open at a time).
        """
        if self.__exclusive != exclusive:
            self.__exclusive = exclusive
            self.__tabActionGroup.setExclusive(exclusive)
            checked = self.__tabActionGroup.checkedAction()
            if checked is None:
                # The action group can be out of sync with the actions state
                # when switching between exclusive states.
                actions_checked = [page.action for page in self.__pages
                                   if page.action.isChecked()]
                if actions_checked:
                    checked = actions_checked[0]

            # Trigger/toggle remaining open pages
            if exclusive and checked is not None:
                for page in self.__pages:
                    if checked != page.action and page.action.isChecked():
                        page.action.trigger()

    def exclusive(self):
        """
        Are the tabs in the toolbox exclusive.
        """
        return self.__exclusive

    exclusive_ = Property(bool,
                          fget=exclusive,
                          fset=setExclusive,
                          designable=True,
                          doc="Exclusive tabs")

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        self.__pages = []
        self.__tabButtonHeight = -1
        self.__tabIconSize = QSize()
        self.__exclusive = False
        self.__setupUi()

    def __setupUi(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = \
                _ToolBoxScrollArea(self, objectName="toolbox-scroll-area")

        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
        self.__scrollArea.setWidgetResizable(True)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")
        self.__contentsLayout = _ToolBoxLayout(
            sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize,
            spacing=0
        )
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = \
                QActionGroup(self, objectName="toolbox-tab-action-group")

        self.__tabActionGroup.setExclusive(self.__exclusive)

        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)

    def setTabButtonHeight(self, height):
        """
        Set the tab button height.
        """
        if self.__tabButtonHeight != height:
            self.__tabButtonHeight = height
            for page in self.__pages:
                page.button.setFixedHeight(height)

    def tabButtonHeight(self):
        """
        Return the tab button height.
        """
        return self.__tabButtonHeight

    def setTabIconSize(self, size):
        """
        Set the tab button icon size.
        """
        if self.__tabIconSize != size:
            self.__tabIconSize = size
            for page in self.__pages:
                page.button.setIconSize(size)

    def tabIconSize(self):
        """
        Return the tab icon size.
        """
        return self.__tabIconSize

    def tabButton(self, index):
        """
        Return the tab button at `index`
        """
        return self.__pages[index].button

    def tabAction(self, index):
        """
        Return open/close action for the tab at `index`.
        """
        return self.__pages[index].action

    def addItem(self, widget, text, icon=None, toolTip=None):
        """
        Append the `widget` in a new tab and return its index.

        Parameters
        ----------
        widget : :class:`QWidget`
            A widget to be inserted. The toolbox takes ownership
            of the widget.

        text : str
            Name/title of the new tab.

        icon : :class:`QIcon`, optional
            An icon for the tab button.

        toolTip : str, optional
            Tool tip for the tab button.

        """
        return self.insertItem(self.count(), widget, text, icon, toolTip)

    def insertItem(self, index, widget, text, icon=None, toolTip=None):
        """
        Insert the `widget` in a new tab at position `index`.

        See also
        --------
        ToolBox.addItem

        """
        button = self.createTabButton(widget, text, icon, toolTip)

        self.__contentsLayout.insertWidget(index * 2, button)
        self.__contentsLayout.insertWidget(index * 2 + 1, widget)

        widget.hide()

        page = _ToolBoxPage(index, widget, button.defaultAction(), button)
        self.__pages.insert(index, page)

        for i in range(index + 1, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        self.__updatePositions()

        # Show (open) the first tab.
        if self.count() == 1 and index == 0:
            page.action.trigger()

        self.__updateSelected()

        self.updateGeometry()
        return index

    def removeItem(self, index):
        """
        Remove the widget at `index`.

        .. note:: The widget hidden but is is not deleted.

        """
        self.__contentsLayout.takeAt(2 * index + 1)
        self.__contentsLayout.takeAt(2 * index)
        page = self.__pages.pop(index)

        # Update the page indexes
        for i in range(index, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        page.button.deleteLater()

        # Hide the widget and reparent to self
        # This follows QToolBox.removeItem
        page.widget.hide()
        page.widget.setParent(self)

        self.__updatePositions()
        self.__updateSelected()

        self.updateGeometry()

    def count(self):
        """
        Return the number of widgets inserted in the toolbox.
        """
        return len(self.__pages)

    def widget(self, index):
        """
        Return the widget at `index`.
        """
        return self.__pages[index].widget

    def createTabButton(self, widget, text, icon=None, toolTip=None):
        """
        Create the tab button for `widget`.
        """
        action = QAction(text, self)
        action.setCheckable(True)

        if icon:
            action.setIcon(icon)

        if toolTip:
            action.setToolTip(toolTip)
        self.__tabActionGroup.addAction(action)
        self.__actionMapper.setMapping(action, action)
        action.toggled.connect(self.__actionMapper.map)

        button = ToolBoxTabButton(self, objectName="toolbox-tab-button")
        button.setDefaultAction(action)
        button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        button.setSizePolicy(QSizePolicy.Ignored,
                             QSizePolicy.Fixed)

        if self.__tabIconSize.isValid():
            button.setIconSize(self.__tabIconSize)

        if self.__tabButtonHeight > 0:
            button.setFixedHeight(self.__tabButtonHeight)

        return button

    def ensureWidgetVisible(self, child, xmargin=50, ymargin=50):
        """
        Scroll the contents so child widget instance is visible inside
        the viewport.

        """
        self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin)

    def sizeHint(self):
        hint = self.__contentsLayout.sizeHint()

        if self.count():
            # Compute max width of hidden widgets also.
            scroll = self.__scrollArea
            scroll_w = scroll.verticalScrollBar().sizeHint().width()
            frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2
            max_w = max([p.widget.sizeHint().width() for p in self.__pages])
            hint = QSize(max(max_w, hint.width()) + scroll_w + frame_w,
                         hint.height())

        return QSize(200, 200).expandedTo(hint)

    def __onTabActionToogled(self, action):
        page = find(self.__pages, action, key=attrgetter("action"))
        on = action.isChecked()
        page.widget.setVisible(on)
        index = page.index

        if index > 0:
            # Update the `previous` tab buttons style hints
            previous = self.__pages[index - 1].button
            flag = QStyleOptionToolBox.NextIsSelected
            if on:
                previous.selected |= flag
            else:
                previous.selected &= ~flag

            previous.update()

        if index < self.count() - 1:
            next = self.__pages[index + 1].button
            flag = QStyleOptionToolBox.PreviousIsSelected
            if on:
                next.selected |= flag
            else:
                next.selected &= ~flag

            next.update()

        self.tabToogled.emit(index, on)

        self.__contentsLayout.invalidate()

    def __updateSelected(self):
        """Update the tab buttons selected style flags.
        """
        if self.count() == 0:
            return

        opt = QStyleOptionToolBox

        def update(button, next_sel, prev_sel):
            if next_sel:
                button.selected |= opt.NextIsSelected
            else:
                button.selected &= ~opt.NextIsSelected

            if prev_sel:
                button.selected |= opt.PreviousIsSelected
            else:
                button.selected &= ~ opt.PreviousIsSelected

            button.update()

        if self.count() == 1:
            update(self.__pages[0].button, False, False)
        elif self.count() >= 2:
            pages = self.__pages
            for i in range(1, self.count() - 1):
                update(pages[i].button,
                       pages[i + 1].action.isChecked(),
                       pages[i - 1].action.isChecked())

    def __updatePositions(self):
        """Update the tab buttons position style flags.
        """
        if self.count() == 0:
            return
        elif self.count() == 1:
            self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab
        else:
            self.__pages[0].button.position = QStyleOptionToolBox.Beginning
            self.__pages[-1].button.position = QStyleOptionToolBox.End
            for p in self.__pages[1:-1]:
                p.button.position = QStyleOptionToolBox.Middle

        for p in self.__pages:
            p.button.update()