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)
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()
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)
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()
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()
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()
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)
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)
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()
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()
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)
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()