class TabButton(QToolButton): def __init__(self, parent=None, **kwargs): QToolButton.__init__(self, parent, **kwargs) self.setToolButtonStyle(Qt.ToolButtonIconOnly) self.setCheckable(True) self.__flat = True self.__showMenuIndicator = False def setFlat(self, flat): if self.__flat != flat: self.__flat = flat self.update() def flat(self): return self.__flat flat_ = Property(bool, fget=flat, fset=setFlat, designable=True) def setShownMenuIndicator(self, show): if self.__showMenuIndicator != show: self.__showMenuIndicator = show self.update() def showMenuIndicator(self): return self.__showMenuIndicator showMenuIndicator_ = Property(bool, fget=showMenuIndicator, fset=setShownMenuIndicator, designable=True) def paintEvent(self, event): opt = QStyleOptionToolButton() self.initStyleOption(opt) if self.__showMenuIndicator and self.isChecked(): opt.features |= QStyleOptionToolButton.HasMenu if self.__flat: # Use default widget background/border styling. StyledWidget_paintEvent(self, event) p = QStylePainter(self) p.drawControl(QStyle.CE_ToolButtonLabel, opt) else: p = QStylePainter(self) p.drawComplexControl(QStyle.CC_ToolButton, opt) def sizeHint(self): opt = QStyleOptionToolButton() self.initStyleOption(opt) if self.__showMenuIndicator and self.isChecked(): opt.features |= QStyleOptionToolButton.HasMenu style = self.style() hint = style.sizeFromContents(QStyle.CT_ToolButton, opt, opt.iconSize, self) return hint
class SchemeArrowAnnotation(BaseSchemeAnnotation): """ An arrow annotation in the scheme. """ color_changed = Signal(unicode) def __init__(self, start_pos, end_pos, color="red", anchor=None, parent=None): BaseSchemeAnnotation.__init__(self, parent) self.__start_pos = start_pos self.__end_pos = end_pos self.__color = color self.__anchor = anchor def set_line(self, start_pos, end_pos): """ Set arrow lines start and end position (``(x, y)`` tuples). """ if self.__start_pos != start_pos or self.__end_pos != end_pos: self.__start_pos = start_pos self.__end_pos = end_pos self.geometry_changed.emit() def start_pos(self): """ Start position of the arrow (base point). """ return self.__start_pos start_pos = Property(tuple, fget=start_pos) def end_pos(self): """ End position of the arrow (arrow head points toward the end). """ return self.__end_pos end_pos = Property(tuple, fget=end_pos) def set_geometry(self, (start_pos, end_pos)): """ Set the geometry of the arrow as a start and end position tuples (e.g. ``set_geometry(((0, 0), (100, 0))``). """ self.set_line(start_pos, end_pos)
class LineEditButton(QToolButton): """ A button in the :class:`LineEdit`. """ def __init__(self, parent=None, flat=True, **kwargs): QToolButton.__init__(self, parent, **kwargs) self.__flat = flat def setFlat(self, flat): if self.__flat != flat: self.__flat = flat self.update() def flat(self): return self.__flat flat_ = Property(bool, fget=flat, fset=setFlat, designable=True) def paintEvent(self, event): if self.__flat: opt = QStyleOptionToolButton() self.initStyleOption(opt) p = QStylePainter(self) p.drawControl(QStyle.CE_ToolButtonLabel, opt) else: QToolButton.paintEvent(self, event)
class StrengthIndicator(QLabel): """A password strength indicator. This is a label that gives feedback on the strength of a password. """ Poor, Good, Excellent = range(3) stylesheet = """ StrengthIndicator { border: 1px solid black; } StrengthIndicator[strength="0"] { background-color: #ff2929; } StrengthIndicator[strength="1"] { background-color: #4dd133; } StrengthIndicator[strength="2"] { background-color: #4dd133; } """ def __init__(self, parent=None): super(StrengthIndicator, self).__init__(parent) self._strength = 0 self.setStyleSheet(self.stylesheet) def getStrength(self): return self._strength def setStrength(self, strength): self._strength = strength if strength == self.Poor: self.setText('Poor') elif strength == self.Good: self.setText('Good') elif strength == self.Excellent: self.setText('Excellent') self.setStyleSheet(self.stylesheet) strength = Property(int, getStrength, setStrength)
class ColorButton(QPushButton): """ Color choosing push button """ __pyqtSignals__ = ("colorChanged(QColor)", ) def __init__(self, parent=None): QPushButton.__init__(self, parent) self.setFixedSize(20, 20) self.setIconSize(QSize(12, 12)) self.connect(self, SIGNAL("clicked()"), self.choose_color) self._color = QColor() def choose_color(self): color = QColorDialog.getColor(self._color, self.parentWidget()) if color.isValid(): self.set_color(color) def get_color(self): return self._color @Slot(QColor) def set_color(self, color): if color != self._color: self._color = color self.emit(SIGNAL("colorChanged(QColor)"), self._color) pixmap = QPixmap(self.iconSize()) pixmap.fill(color) self.setIcon(QIcon(pixmap)) color = Property("QColor", get_color, set_color)
class FeatureEditor(QtGui.QFrame): featureChanged = Signal() featureEdited = Signal() modifiedChanged = Signal([], [bool]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) layout = QtGui.QFormLayout( fieldGrowthPolicy=QtGui.QFormLayout.ExpandingFieldsGrow ) self.nameedit = QtGui.QLineEdit( sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) ) self.expressionedit = QtGui.QPlainTextEdit( tabChangesFocus=True, ) high = PythonSyntaxHighlighter(self.expressionedit.document()) layout.addRow(self.tr("Name"), self.nameedit) layout.addRow(self.tr("Expression"), self.expressionedit) self.setLayout(layout) self.nameedit.editingFinished.connect(self._invalidate) self.expressionedit.textChanged.connect(self._invalidate) self._modified = False def setModified(self, modified): if not type(modified) is bool: raise TypeError if self._modified != modified: self._modified = modified self.modifiedChanged.emit() self.modifiedChanged[bool].emit(modified) def modified(self): return self._modified modified = Property(bool, modified, setModified, notify=modifiedChanged) def setEditorData(self, data): self.nameedit.setText(data.name) self.expressionedit.setPlainText(data.expression) self.setModified(False) self.featureChanged.emit() def editorData(self): return FeatureDescriptor(name=self.nameedit.text(), expression=self.nameedit.toPlainText()) def _invalidate(self): self.setModified(True) self.featureEdited.emit() self.featureChanged.emit()
class GraphicsTextEdit(QGraphicsTextItem): """ QGraphicsTextItem subclass defining an additional placeholderText property (text displayed when no text is set). """ def __init__(self, *args, **kwargs): QGraphicsTextItem.__init__(self, *args, **kwargs) self.__placeholderText = "" def setPlaceholderText(self, text): """ Set the placeholder text. This is shown when the item has no text, i.e when `toPlainText()` returns an empty string. """ if self.__placeholderText != text: self.__placeholderText = text if not self.toPlainText(): self.update() def placeholderText(self): """ Return the placeholder text. """ return str(self.__placeholderText) placeholderText_ = Property(str, placeholderText, setPlaceholderText, doc="Placeholder text") def paint(self, painter, option, widget=None): QGraphicsTextItem.paint(self, painter, option, widget) # Draw placeholder text if necessary if not (self.toPlainText() and self.toHtml()) and \ self.__placeholderText and \ not (self.hasFocus() and \ self.textInteractionFlags() & Qt.TextEditable): brect = self.boundingRect() painter.setFont(self.font()) metrics = painter.fontMetrics() text = metrics.elidedText(self.__placeholderText, Qt.ElideRight, brect.width()) color = self.defaultTextColor() color.setAlpha(min(color.alpha(), 150)) painter.setPen(QPen(color)) painter.drawText(brect, Qt.AlignTop | Qt.AlignLeft, text)
class SplitterResizer(QObject): """An object able to control the size of a widget in a QSpliter instance. """ def __init__(self, parent=None): QObject.__init__(self, parent) self.__splitter = None self.__widget = None self.__animationEnabled = True self.__size = -1 self.__expanded = False self.__animation = QPropertyAnimation(self, b"size_", self) self.__action = QAction("toogle-expanded", self, checkable=True) self.__action.triggered[bool].connect(self.setExpanded) def setSize(self, size): """Set the size of the controlled widget (either width or height depending on the orientation). """ if self.__size != size: self.__size = size self.__update() def size(self): """Return the size of the widget in the splitter (either height of width) depending on the splitter orientation. """ if self.__splitter and self.__widget: index = self.__splitter.indexOf(self.__widget) sizes = self.__splitter.sizes() return sizes[index] else: return -1 size_ = Property(int, fget=size, fset=setSize) def setAnimationEnabled(self, enable): """Enable/disable animation. """ self.__animation.setDuration(0 if enable else 200) def animationEnabled(self): return self.__animation.duration() == 0 def setSplitterAndWidget(self, splitter, widget): """Set the QSplitter and QWidget instance the resizer should control. .. note:: the widget must be in the splitter. """ if splitter and widget and not splitter.indexOf(widget) > 0: raise ValueError("Widget must be in a spliter.") if self.__widget: self.__widget.removeEventFilter() self.__splitter = splitter self.__widget = widget if widget: widget.installEventFilter(self) self.__update() def toogleExpandedAction(self): """Return a QAction that can be used to toggle expanded state. """ return self.__action def open(self): """Open the controlled widget (expand it to it sizeHint). """ self.__expanded = True self.__action.setChecked(True) if not (self.__splitter and self.__widget): return size = self.size() if size > 0: # Already has non zero size. return hint = self.__widget.sizeHint() if self.__splitter.orientation() == Qt.Vertical: end = hint.height() else: end = hint.width() self.__animation.setStartValue(0) self.__animation.setEndValue(end) self.__animation.start() def close(self): """Close the controlled widget (shrink to size 0). """ self.__expanded = False self.__action.setChecked(False) if not (self.__splitter and self.__widget): return self.__animation.setStartValue(self.size()) self.__animation.setEndValue(0) self.__animation.start() def setExpanded(self, expanded): """Set the expanded state. """ if self.__expanded != expanded: if expanded: self.open() else: self.close() def expanded(self): """Return the expanded state. """ return self.__expanded def __update(self): """Update the splitter sizes. """ if self.__splitter and self.__widget: splitter = self.__splitter index = splitter.indexOf(self.__widget) sizes = splitter.sizes() current = sizes[index] diff = current - self.__size sizes[index] = self.__size sizes[index - 1] = sizes[index - 1] + diff self.__splitter.setSizes(sizes) def eventFilter(self, obj, event): if event.type() == QEvent.Resize: if self.__splitter.orientation() == Qt.Vertical: size = event.size().height() else: size = event.size().width() if self.__expanded and size == 0: self.__action.setChecked(False) self.__expanded = False elif not self.__expanded and size > 0: self.__action.setChecked(True) self.__expanded = True return QObject.eventFilter(self, obj, event)
class OASYSWidgetsScheme(WidgetsScheme): #: Signal emitted when the working directory changes. working_directory_changed = Signal(str) workspace_units_changed = Signal(int) def __init__(self, parent=None, title=None, description=None, working_directory=None, workspace_units=None): settings = QSettings() self.__working_directory = ( working_directory or settings.value("output/default-working-directory", os.path.expanduser("~/Oasys"), type=str)) if not os.path.exists(self.__working_directory): os.makedirs(self.__working_directory, exist_ok=True) #QSettings().setValue("output/default-units", 1) self.__workspace_units = ( workspace_units or settings.value("output/default-units", 1, type=int)) super().__init__(parent, title=title, description=description) # Replace the signal manager from. self.signal_manager.setParent(None) self.signal_manager.deleteLater() sip.delete(self.signal_manager) sip.delete(self.widget_manager) self.set_loop_flags(Scheme.AllowLoops) self.signal_manager = OASYSSignalManager(self) self.widget_manager = OASYSWidgetManager() self.widget_manager.set_scheme(self) def set_working_directory(self, working_directory): """ Set the scheme working_directory. """ if self.__working_directory != working_directory: self.__working_directory = working_directory self.working_directory_changed.emit(working_directory) def working_directory(self): """ The working_directory of the scheme. """ return self.__working_directory def set_workspace_units(self, units): """ Set the scheme units. """ if self.__workspace_units != units: self.__workspace_units = units self.workspace_units_changed.emit(units) def workspace_units(self): """ The units of the scheme. """ return self.__workspace_units working_directory = Property(str, fget=working_directory, fset=set_working_directory) workspace_units = Property(str, fget=workspace_units, fset=set_workspace_units) def save_to(self, stream, pretty=True, pickle_fallback=False): """ Reimplemented from Scheme.save_to. """ if isinstance(stream, str): stream = open(stream, "wb") self.sync_node_properties() tree = readwrite.scheme_to_etree(self, pickle_fallback=pickle_fallback) root = tree.getroot() root.set("working_directory", self.working_directory or "") root.set("workspace_units", str(self.workspace_units) or "") if pretty: readwrite.indent(tree.getroot(), 0) if sys.version_info < (2, 7): # in Python 2.6 the write does not have xml_declaration parameter. tree.write(stream, encoding="utf-8") else: tree.write(stream, encoding="utf-8", xml_declaration=True)
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__statusMessage = "" self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, -self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = NameTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(str, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0 or not self.__processingState: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setStatusMessage(self, message): """ Set the node status message text. This text is displayed below the node's title. """ if self.__statusMessage != message: self.__statusMessage = message self.__updateTitleText() def statusMessage(self): return self.__statusMessage def setStateMessage(self, message): """ Set a state message to display over the item. Parameters ---------- message : UserMessage Message to display. `message.severity` is used to determine the icon and `message.contents` is used as a tool tip. """ # TODO: Group messages by message_id not by severity # and deprecate set[Error|Warning|Error]Message if message.severity == UserMessage.Info: self.setInfoMessage(message.contents) elif message.severity == UserMessage.Warning: self.setWarningMessage(message.contents) elif message.severity == UserMessage.Error: self.setErrorMessage(message.contents) def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. return self.childrenBoundingRect() def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ text = ['<div align="center">%s' % escape(self.title())] status_text = [] progress_included = False if self.__statusMessage: msg = escape(self.__statusMessage) format_fields = dict(parse_format_fields(msg)) if "progress" in format_fields and len(format_fields) == 1: # Insert progress into the status text format string. spec, _ = format_fields["progress"] if spec != None: progress_included = True progress_str = "{0:.0f}%".format(self.progress()) status_text.append(msg.format(progress=progress_str)) else: status_text.append(msg) if self.progress() >= 0 and not progress_included: status_text.append("%i%%" % int(self.progress())) if status_text: text += [ "<br/>", '<span style="font-style: italic">', "<br/>".join(status_text), "</span>" ] text += ["</div>"] text = "".join(text) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: self.shapeItem.setSelected(value) self.captionTextItem.setSelectionState(value) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)
def set_geometry(self, (start_pos, end_pos)): """ Set the geometry of the arrow as a start and end position tuples (e.g. ``set_geometry(((0, 0), (100, 0))``). """ self.set_line(start_pos, end_pos) def geometry(self): """ Return the start and end positions of the arrow. """ return (self.start_pos, self.end_pos) geometry = Property(tuple, fget=geometry, fset=set_geometry) def set_color(self, color): """ Set the fill color for the arrow as a string (`#RGB`, `#RRGGBB`, `#RRRGGGBBB`, `#RRRRGGGGBBBB` format or one of SVG color keyword names). """ check_type(color, (basestring, QString)) color = unicode(color) if self.__color != color: self.__color = color self.color_changed.emit(color) def color(self):
class PasswordItem(QLabel): stylesheet = """ PasswordItem[selected="false"] { color: palette(window-text); background-color: white; } PasswordItem[selected="true"] { color: palette(highlighted-text); background-color: palette(highlight); } QLabel { height: 18px; } """ def __init__(self, vault, version, parent=None): super(PasswordItem, self).__init__(parent) self.vault = vault self._selected = False self.updateData(version) def getSelected(self): return self._selected def setSelected(self, selected): self._selected = selected # Re-calculate style self.setStyleSheet(self.stylesheet) selected = Property(bool, getSelected, setSelected) clicked = Signal(str, str) @Slot(dict) def updateData(self, version): self.version = version self.setText(version.get('name', '')) @Slot() def copyUsernameToClipboard(self): username = self.version.get('username', '') QApplication.instance().copyToClipboard(username) @Slot() def copyPasswordToClipboard(self): password = self.version.get('password', '') QApplication.instance().copyToClipboard(password, 60) @Slot() def editPassword(self): pwview = QApplication.instance().mainWindow().passwordView() pwview.editPassword(self.vault, self.version) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.clicked.emit(self.vault, self.version['id']) elif event.button() == Qt.RightButton: self.showContextMenu(event.pos()) def mouseDoubleClickEvent(self, event): self.editPassword() def showContextMenu(self, pos): menu = QMenu(self) action = menu.addAction('Copy Username') action.setShortcut(QKeySequence('CTRL-U')) action.triggered.connect(self.copyUsernameToClipboard) action = menu.addAction('Copy Password') action.setShortcut(QKeySequence('CTRL-C')) action.triggered.connect(self.copyPasswordToClipboard) menu.addSeparator() action = menu.addAction('Edit') action.triggered.connect(self.editPassword) action = menu.addAction('Delete') action.triggered.connect(self.deleteItem) menu.popup(self.mapToGlobal(pos)) @Slot() def deleteItem(self): backend = QApplication.instance().backend() version = { 'id': self.version['id'], 'name': self.version['name'], '_type': self.version['_type'] } backend.delete_version(self.vault, version)
class SchemeNode(QObject): """ A node in a :class:`.Scheme`. Parameters ---------- description : :class:`WidgetDescription` Node description instance. title : str, optional Node title string (if None `description.name` is used). position : tuple (x, y) tuple of floats for node position in a visual display. properties : dict Additional extra instance properties (settings, widget geometry, ...) parent : :class:`QObject` Parent object. """ def __init__(self, description, title=None, position=None, properties=None, parent=None): QObject.__init__(self, parent) self.description = description if title is None: title = description.name self.__title = title self.__position = position or (0, 0) self.__progress = -1 self.__processing_state = 0 self.__status_message = "" self.__state_messages = {} self.properties = properties or {} def input_channels(self): """ Return a list of input channels (:class:`InputSignal`) for the node. """ return list(self.description.inputs) def output_channels(self): """ Return a list of output channels (:class:`OutputSignal`) for the node. """ return list(self.description.outputs) def input_channel(self, name): """ Return the input channel matching `name`. Raise a `ValueError` if not found. """ for channel in self.input_channels(): if channel.name == name: return channel raise ValueError("%r is not a valid input channel name for %r." % \ (name, self.description.name)) def output_channel(self, name): """ Return the output channel matching `name`. Raise an `ValueError` if not found. """ for channel in self.output_channels(): if channel.name == name: return channel raise ValueError("%r is not a valid output channel name for %r." % \ (name, self.description.name)) #: The title of the node has changed title_changed = Signal(unicode) def set_title(self, title): """ Set the node title. """ if self.__title != title: self.__title = unicode(title) self.title_changed.emit(self.__title) def title(self): """ The node title. """ return self.__title title = Property(unicode, fset=set_title, fget=title) #: Position of the node in the scheme has changed position_changed = Signal(tuple) def set_position(self, pos): """ Set the position (``(x, y)`` tuple) of the node. """ if self.__position != pos: self.__position = pos self.position_changed.emit(pos) def position(self): """ ``(x, y)`` tuple containing the position of the node in the scheme. """ return self.__position position = Property(tuple, fset=set_position, fget=position) #: Node's progress value has changed. progress_changed = Signal(float) def set_progress(self, value): """ Set the progress value. """ if self.__progress != value: self.__progress = value self.progress_changed.emit(value) def progress(self): """ The current progress value. -1 if progress is not set. """ return self.__progress progress = Property(float, fset=set_progress, fget=progress) #: Node's processing state has changed. processing_state_changed = Signal(int) def set_processing_state(self, state): """ Set the node processing state. """ if self.__processing_state != state: self.__processing_state = state self.processing_state_changed.emit(state) def processing_state(self): """ The node processing state, 0 for not processing, 1 the node is busy. """ return self.__processing_state processing_state = Property(int, fset=set_processing_state, fget=processing_state) def set_tool_tip(self, tool_tip): if self.__tool_tip != tool_tip: self.__tool_tip = tool_tip def tool_tip(self): return self.__tool_tip tool_tip = Property(str, fset=set_tool_tip, fget=tool_tip) #: The node's status tip has changes status_message_changed = Signal(unicode) def set_status_message(self, text): if self.__status_message != text: self.__status_message = text self.status_message_changed.emit(text) def status_message(self): return self.__status_message #: The node's state message has changed state_message_changed = Signal(UserMessage) def set_state_message(self, message): """ Set a message to be displayed by a scheme view for this node. """ if message.message_id in self.__state_messages and \ not message.contents: del self.__state_messages[message.message_id] self.__state_messages[message.message_id] = message self.state_message_changed.emit(message) def state_messages(self): """ Return a list of all state messages. """ return self.__state_messages.values() def __str__(self): return u"SchemeNode(description_id=%s, title=%r, ...)" % \ (str(self.description.id), self.title) def __repr__(self): return str(self)
class SchemeTextAnnotation(BaseSchemeAnnotation): """ Text annotation in the scheme. """ # Signal emitted when the annotation text changes. text_changed = Signal(str) # Signal emitted when the annotation text font changes. font_changed = Signal(dict) def __init__(self, rect, text="", font=None, anchor=None, parent=None): BaseSchemeAnnotation.__init__(self, parent) self.__rect = rect self.__text = text self.__font = {} if font is None else font self.__anchor = anchor def set_rect(self, rect): """ Set the text geometry bounding rectangle (``(x, y, width, height)`` tuple). """ if self.__rect != rect: self.__rect = rect self.geometry_changed.emit() def rect(self): """ Text bounding rectangle """ return self.__rect rect = Property(tuple, fget=rect, fset=set_rect) def set_geometry(self, rect): """ Set the text geometry (same as ``set_rect``) """ self.set_rect(rect) def geometry(self): """ Text annotation geometry (same as ``rect`` """ return self.rect geometry = Property(tuple, fget=geometry, fset=set_geometry) def set_text(self, text): """ Set the annotation text. """ check_type(text, str) text = str(text) if self.__text != text: self.__text = text self.text_changed.emit(text) def text(self): """ Annotation text. """ return self.__text text = Property(tuple, fget=text, fset=set_text) def set_font(self, font): """ Set the annotation's font as a dictionary of font properties (at the moment only family and size are used). >>> annotation.set_font({"family": "Helvetica", "size": 16}) """ check_type(font, dict) font = dict(font) if self.__font != font: self.__font = font self.font_changed.emit(font) def font(self): """ Annotation's font property dictionary. """ return dict(self.__font) font = Property(str, fget=font, fset=set_font)
class FeatureEditor(QtGui.QFrame): FUNCTIONS = dict( chain( [(key, val) for key, val in math.__dict__.items() if not key.startswith("_")], [("str", str)])) featureChanged = Signal() featureEdited = Signal() modifiedChanged = Signal(bool) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) layout = QtGui.QFormLayout( fieldGrowthPolicy=QtGui.QFormLayout.ExpandingFieldsGrow) layout.setContentsMargins(0, 0, 0, 0) self.nameedit = QtGui.QLineEdit(placeholderText="Name...", sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.Fixed)) self.expressionedit = QtGui.QLineEdit(placeholderText="Expression...") self.attrs_model = itemmodels.VariableListModel(["Select feature"], parent=self) self.attributescb = QtGui.QComboBox( minimumContentsLength=16, sizeAdjustPolicy=QtGui.QComboBox. AdjustToMinimumContentsLengthWithIcon, sizePolicy=QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)) self.attributescb.setModel(self.attrs_model) sorted_funcs = sorted(self.FUNCTIONS) self.funcs_model = itemmodels.PyListModelTooltip() self.funcs_model.setParent(self) self.funcs_model[:] = chain(["Select function"], sorted_funcs) self.funcs_model.tooltips[:] = chain( [''], [self.FUNCTIONS[func].__doc__ for func in sorted_funcs]) self.functionscb = QtGui.QComboBox( minimumContentsLength=16, sizeAdjustPolicy=QtGui.QComboBox. AdjustToMinimumContentsLengthWithIcon, sizePolicy=QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)) self.functionscb.setModel(self.funcs_model) hbox = QtGui.QHBoxLayout() hbox.addWidget(self.attributescb) hbox.addWidget(self.functionscb) layout.addRow(self.nameedit, self.expressionedit) layout.addRow(self.tr(""), hbox) self.setLayout(layout) self.nameedit.editingFinished.connect(self._invalidate) self.expressionedit.textChanged.connect(self._invalidate) self.attributescb.currentIndexChanged.connect(self.on_attrs_changed) self.functionscb.currentIndexChanged.connect(self.on_funcs_changed) self._modified = False def setModified(self, modified): if not type(modified) is bool: raise TypeError if self._modified != modified: self._modified = modified self.modifiedChanged.emit(modified) def modified(self): return self._modified modified = Property(bool, modified, setModified, notify=modifiedChanged) def setEditorData(self, data, domain): self.nameedit.setText(data.name) self.expressionedit.setText(data.expression) self.setModified(False) self.featureChanged.emit() self.attrs_model[:] = ["Select feature"] if domain: self.attrs_model[:] += chain(domain.attributes, domain.class_vars, domain.metas) def editorData(self): return FeatureDescriptor(name=self.nameedit.text(), expression=self.nameedit.text()) def _invalidate(self): self.setModified(True) self.featureEdited.emit() self.featureChanged.emit() def on_attrs_changed(self): index = self.attributescb.currentIndex() if index > 0: attr = sanitized_name(self.attrs_model[index].name) self.insert_into_expression(attr) self.attributescb.setCurrentIndex(0) def on_funcs_changed(self): index = self.functionscb.currentIndex() if index > 0: func = self.funcs_model[index] if func in [ "atan2", "fmod", "ldexp", "log", "pow", "copysign", "hypot" ]: self.insert_into_expression(func + "(,)") self.expressionedit.cursorBackward(False, 2) elif func in ["e", "pi"]: self.insert_into_expression(func) else: self.insert_into_expression(func + "()") self.expressionedit.cursorBackward(False) self.functionscb.setCurrentIndex(0) def insert_into_expression(self, what): cp = self.expressionedit.cursorPosition() ct = self.expressionedit.text() text = ct[:cp] + what + ct[cp:] self.expressionedit.setText(text) self.expressionedit.setFocus()
class SchemeArrowAnnotation(BaseSchemeAnnotation): """ An arrow annotation in the scheme. """ color_changed = Signal(str) def __init__(self, start_pos, end_pos, color="red", anchor=None, parent=None): BaseSchemeAnnotation.__init__(self, parent) self.__start_pos = start_pos self.__end_pos = end_pos self.__color = color self.__anchor = anchor def set_line(self, start_pos, end_pos): """ Set arrow lines start and end position (``(x, y)`` tuples). """ if self.__start_pos != start_pos or self.__end_pos != end_pos: self.__start_pos = start_pos self.__end_pos = end_pos self.geometry_changed.emit() def start_pos(self): """ Start position of the arrow (base point). """ return self.__start_pos start_pos = Property(tuple, fget=start_pos) def end_pos(self): """ End position of the arrow (arrow head points toward the end). """ return self.__end_pos end_pos = Property(tuple, fget=end_pos) def set_geometry(self, geometry): """ Set the geometry of the arrow as a start and end position tuples (e.g. ``set_geometry(((0, 0), (100, 0))``). """ (start_pos, end_pos) = geometry self.set_line(start_pos, end_pos) def geometry(self): """ Return the start and end positions of the arrow. """ return (self.start_pos, self.end_pos) geometry = Property(tuple, fget=geometry, fset=set_geometry) def set_color(self, color): """ Set the fill color for the arrow as a string (`#RGB`, `#RRGGBB`, `#RRRGGGBBB`, `#RRRRGGGGBBBB` format or one of SVG color keyword names). """ check_type(color, str) color = str(color) if self.__color != color: self.__color = color self.color_changed.emit(color) def color(self): """ The arrow's fill color. """ return self.__color color = Property(str, fget=color, fset=set_color)
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") # 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) 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.Expanding, 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 = QStyleOptionToolBoxV2.NextIsSelected if on: previous.selected |= flag else: previous.selected &= ~flag previous.update() if index < self.count() - 1: next = self.__pages[index + 1].button flag = QStyleOptionToolBoxV2.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 = QStyleOptionToolBoxV2 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 = QStyleOptionToolBoxV2.OnlyOneTab else: self.__pages[0].button.position = QStyleOptionToolBoxV2.Beginning self.__pages[-1].button.position = QStyleOptionToolBoxV2.End for p in self.__pages[1:-1]: p.button.position = QStyleOptionToolBoxV2.Middle for p in self.__pages: p.button.update()
class WidgetToolBox(ToolBox): """ `WidgetToolBox` widget shows a tool box containing button grids of actions for a :class:`QtWidgetRegistry` item model. """ triggered = Signal(QAction) hovered = Signal(QAction) def __init__(self, parent=None): ToolBox.__init__(self, parent) self.__model = None self.__iconSize = QSize(25, 25) self.__buttonSize = QSize(50, 50) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) def setIconSize(self, size): """ Set the widget icon size (icons in the button grid). """ self.__iconSize = size for widget in map(self.widget, range(self.count())): widget.setIconSize(size) def iconSize(self): """ Return the widget buttons icon size. """ return self.__iconSize iconSize_ = Property(QSize, fget=iconSize, fset=setIconSize, designable=True) def setButtonSize(self, size): """ Set fixed widget button size. """ self.__buttonSize = size for widget in map(self.widget, range(self.count())): widget.setButtonSize(size) def buttonSize(self): """Return the widget button size """ return self.__buttonSize buttonSize_ = Property(QSize, fget=buttonSize, fset=setButtonSize, designable=True) def saveState(self): """ Return the toolbox state (as a `QByteArray`). .. note:: Individual tabs are stored by their action's text. """ version = 2 actions = map(self.tabAction, range(self.count())) expanded = [action for action in actions if action.isChecked()] expanded = [action.text() for action in expanded] byte_array = QByteArray() stream = QDataStream(byte_array, QIODevice.WriteOnly) stream.writeInt(version) stream.writeQStringList(expanded) return byte_array def restoreState(self, state): """ Restore the toolbox from a :class:`QByteArray` `state`. .. note:: The toolbox should already be populated for the state changes to take effect. """ # In version 1 of saved state the state was saved in # a simple dict repr string. if isinstance(state, QByteArray): stream = QDataStream(state, QIODevice.ReadOnly) version = stream.readInt() if version == 2: expanded = stream.readQStringList() for action in map(self.tabAction, range(self.count())): if (action.text() in expanded) != action.isChecked(): action.trigger() return True return False def setModel(self, model): """ Set the widget registry model (:class:`QStandardItemModel`) for this toolbox. """ if self.__model is not None: self.__model.itemChanged.disconnect(self.__on_itemChanged) self.__model.rowsInserted.disconnect(self.__on_rowsInserted) self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved) self.__model = model if self.__model is not None: self.__model.itemChanged.connect(self.__on_itemChanged) self.__model.rowsInserted.connect(self.__on_rowsInserted) self.__model.rowsRemoved.connect(self.__on_rowsRemoved) self.__initFromModel(self.__model) def __initFromModel(self, model): for cat_item in iter_item(model.invisibleRootItem()): self.__insertItem(cat_item, self.count()) def __insertItem(self, item, index): """ Insert category item at index. """ grid = WidgetToolGrid() grid.setModel(item.model(), item.index()) grid.actionTriggered.connect(self.triggered) grid.actionHovered.connect(self.hovered) grid.setIconSize(self.__iconSize) grid.setButtonSize(self.__buttonSize) text = item.text() icon = item.icon() tooltip = item.toolTip() # Set the 'tab-title' property to text. grid.setProperty("tab-title", text) grid.setObjectName("widgets-toolbox-grid") self.insertItem(index, grid, text, icon, tooltip) button = self.tabButton(index) # Set the 'highlight' color if item.data(Qt.BackgroundRole) is not None: brush = item.background() elif item.data(QtWidgetRegistry.BACKGROUND_ROLE) is not None: brush = item.data(QtWidgetRegistry.BACKGROUND_ROLE) else: brush = self.palette().brush(QPalette.Button) if not brush.gradient(): gradient = create_gradient(brush.color()) brush = QBrush(gradient) palette = button.palette() palette.setBrush(QPalette.Highlight, brush) button.setPalette(palette) def __on_itemChanged(self, item): """ Item contents have changed. """ parent = item.parent() if parent is self.__model.invisibleRootItem(): button = self.tabButton(item.row()) button.setIcon(item.icon()) button.setText(item.text()) button.setToolTip(item.toolTip()) def __on_rowsInserted(self, parent, start, end): """ Items have been inserted in the model. """ # Only the top level items (categories) are handled here. if not parent is not None: root = self.__model.invisibleRootItem() for i in range(start, end + 1): item = root.child(i) self.__insertItem(item, i) def __on_rowsRemoved(self, parent, start, end): """ Rows have been removed from the model. """ # Only the top level items (categories) are handled here. if not parent is not None: for i in range(end, start - 1, -1): self.removeItem(i)
class Scheme(QObject): """ An :class:`QObject` subclass representing the scheme widget workflow with annotations. Parameters ---------- parent : :class:`QObject` A parent QObject item (default `None`). title : str The scheme title. description : str A longer description of the scheme. Attributes ---------- nodes : list of :class:`.SchemeNode` A list of all the nodes in the scheme. links : list of :class:`.SchemeLink` A list of all links in the scheme. annotations : list of :class:`BaseSchemeAnnotation` A list of all the annotations in the scheme. """ # Signal emitted when a `node` is added to the scheme. node_added = Signal(SchemeNode) # Signal emitted when a `node` is removed from the scheme. node_removed = Signal(SchemeNode) # Signal emitted when a `link` is added to the scheme. link_added = Signal(SchemeLink) # Signal emitted when a `link` is removed from the scheme. link_removed = Signal(SchemeLink) # Signal emitted when a `annotation` is added to the scheme. annotation_added = Signal(BaseSchemeAnnotation) # Signal emitted when a `annotation` is removed from the scheme. annotation_removed = Signal(BaseSchemeAnnotation) # Signal emitted when the title of scheme changes. title_changed = Signal(unicode) # Signal emitted when the description of scheme changes. description_changed = Signal(unicode) node_state_changed = Signal() channel_state_changed = Signal() topology_changed = Signal() def __init__(self, parent=None, title=None, description=None): QObject.__init__(self, parent) self.__title = title or "" "Scheme title (empty string by default)." self.__description = description or "" "Scheme description (empty string by default)." self.__annotations = [] self.__nodes = [] self.__links = [] @property def nodes(self): """ A list of all nodes (:class:`.SchemeNode`) currently in the scheme. """ return list(self.__nodes) @property def links(self): """ A list of all links (:class:`.SchemeLink`) currently in the scheme. """ return list(self.__links) @property def annotations(self): """ A list of all annotations (:class:`.BaseSchemeAnnotation`) in the scheme. """ return list(self.__annotations) def set_title(self, title): """ Set the scheme title text. """ if self.__title != title: self.__title = title self.title_changed.emit(title) def title(self): """ The title (human readable string) of the scheme. """ return self.__title title = Property(unicode, fget=title, fset=set_title) def set_description(self, description): """ Set the scheme description text. """ if self.__description != description: self.__description = description self.description_changed.emit(description) def description(self): """ Scheme description text. """ return self.__description description = Property(unicode, fget=description, fset=set_description) def add_node(self, node): """ Add a node to the scheme. An error is raised if the node is already in the scheme. Parameters ---------- node : :class:`.SchemeNode` Node instance to add to the scheme. """ check_arg(node not in self.__nodes, "Node already in scheme.") check_type(node, SchemeNode) self.__nodes.append(node) log.info("Added node %r to scheme %r." % (node.title, self.title)) self.node_added.emit(node) def new_node(self, description, title=None, position=None, properties=None): """ Create a new :class:`.SchemeNode` and add it to the scheme. Same as:: scheme.add_node(SchemeNode(description, title, position, properties)) Parameters ---------- description : :class:`WidgetDescription` The new node's description. title : str, optional Optional new nodes title. By default `description.name` is used. position : `(x, y)` tuple of floats, optional Optional position in a 2D space. properties : dict, optional A dictionary of optional extra properties. See also -------- .SchemeNode, Scheme.add_node """ if isinstance(description, WidgetDescription): node = SchemeNode(description, title=title, position=position, properties=properties) else: raise TypeError("Expected %r, got %r." % \ (WidgetDescription, type(description))) self.add_node(node) return node def remove_node(self, node): """ Remove a `node` from the scheme. All links into and out of the `node` are also removed. If the node in not in the scheme an error is raised. Parameters ---------- node : :class:`.SchemeNode` Node instance to remove. """ check_arg(node in self.__nodes, "Node is not in the scheme.") self.__remove_node_links(node) self.__nodes.remove(node) log.info("Removed node %r from scheme %r." % (node.title, self.title)) self.node_removed.emit(node) return node def __remove_node_links(self, node): """ Remove all links for node. """ links_in, links_out = [], [] for link in self.__links: if link.source_node is node: links_out.append(link) elif link.sink_node is node: links_in.append(link) for link in links_out + links_in: self.remove_link(link) def add_link(self, link): """ Add a `link` to the scheme. Parameters ---------- link : :class:`.SchemeLink` An initialized link instance to add to the scheme. """ check_type(link, SchemeLink) self.check_connect(link) self.__links.append(link) log.info("Added link %r (%r) -> %r (%r) to scheme %r." % \ (link.source_node.title, link.source_channel.name, link.sink_node.title, link.sink_channel.name, self.title) ) self.link_added.emit(link) def new_link(self, source_node, source_channel, sink_node, sink_channel): """ Create a new :class:`.SchemeLink` from arguments and add it to the scheme. The new link is returned. Parameters ---------- source_node : :class:`.SchemeNode` Source node of the new link. source_channel : :class:`.OutputSignal` Source channel of the new node. The instance must be from ``source_node.output_channels()`` sink_node : :class:`.SchemeNode` Sink node of the new link. sink_channel : :class:`.InputSignal` Sink channel of the new node. The instance must be from ``sink_node.input_channels()`` See also -------- .SchemeLink, Scheme.add_link """ link = SchemeLink(source_node, source_channel, sink_node, sink_channel) self.add_link(link) return link def remove_link(self, link): """ Remove a link from the scheme. Parameters ---------- link : :class:`.SchemeLink` Link instance to remove. """ check_arg(link in self.__links, "Link is not in the scheme.") self.__links.remove(link) log.info("Removed link %r (%r) -> %r (%r) from scheme %r." % \ (link.source_node.title, link.source_channel.name, link.sink_node.title, link.sink_channel.name, self.title) ) self.link_removed.emit(link) def check_connect(self, link): """ Check if the `link` can be added to the scheme and raise an appropriate exception. Can raise: - :class:`TypeError` if `link` is not an instance of :class:`.SchemeLink` - :class:`.SchemeCycleError` if the `link` would introduce a cycle - :class:`.IncompatibleChannelTypeError` if the channel types are not compatible - :class:`.SinkChannelError` if a sink channel has a `Single` flag specification and the channel is already connected. - :class:`.DuplicatedLinkError` if a `link` duplicates an already present link. """ check_type(link, SchemeLink) if self.creates_cycle(link): raise SchemeCycleError("Cannot create cycles in the scheme") if not self.compatible_channels(link): raise IncompatibleChannelTypeError( "Cannot connect %r to %r." \ % (link.source_channel.type, link.sink_channel.type) ) links = self.find_links(source_node=link.source_node, source_channel=link.source_channel, sink_node=link.sink_node, sink_channel=link.sink_channel) if links: raise DuplicatedLinkError( "A link from %r (%r) -> %r (%r) already exists" \ % (link.source_node.title, link.source_channel.name, link.sink_node.title, link.sink_channel.name) ) if link.sink_channel.single: links = self.find_links(sink_node=link.sink_node, sink_channel=link.sink_channel) if links: raise SinkChannelError("%r is already connected." % link.sink_channel.name) def creates_cycle(self, link): """ Return `True` if `link` would introduce a cycle in the scheme. Parameters ---------- link : :class:`.SchemeLink` """ check_type(link, SchemeLink) source_node, sink_node = link.source_node, link.sink_node upstream = self.upstream_nodes(source_node) upstream.add(source_node) return sink_node in upstream def compatible_channels(self, link): """ Return `True` if the channels in `link` have compatible types. Parameters ---------- link : :class:`.SchemeLink` """ check_type(link, SchemeLink) return compatible_channels(link.source_channel, link.sink_channel) def can_connect(self, link): """ Return `True` if `link` can be added to the scheme. See also -------- Scheme.check_connect """ check_type(link, SchemeLink) try: self.check_connect(link) return True except (SchemeCycleError, IncompatibleChannelTypeError, SinkChannelError, DuplicatedLinkError): return False def upstream_nodes(self, start_node): """ Return a set of all nodes upstream from `start_node` (i.e. all ancestor nodes). Parameters ---------- start_node : :class:`.SchemeNode` """ visited = set() queue = deque([start_node]) while queue: node = queue.popleft() snodes = [link.source_node for link in self.input_links(node)] for source_node in snodes: if source_node not in visited: queue.append(source_node) visited.add(node) visited.remove(start_node) return visited def downstream_nodes(self, start_node): """ Return a set of all nodes downstream from `start_node`. Parameters ---------- start_node : :class:`.SchemeNode` """ visited = set() queue = deque([start_node]) while queue: node = queue.popleft() snodes = [link.sink_node for link in self.output_links(node)] for source_node in snodes: if source_node not in visited: queue.append(source_node) visited.add(node) visited.remove(start_node) return visited def is_ancestor(self, node, child): """ Return True if `node` is an ancestor node of `child` (is upstream of the child in the workflow). Both nodes must be in the scheme. Parameters ---------- node : :class:`.SchemeNode` child : :class:`.SchemeNode` """ return child in self.downstream_nodes(node) def children(self, node): """ Return a set of all children of `node`. """ return set(link.sink_node for link in self.output_links(node)) def parents(self, node): """ Return a set of all parents of `node`. """ return set(link.source_node for link in self.input_links(node)) def input_links(self, node): """ Return a list of all input links (:class:`.SchemeLink`) connected to the `node` instance. """ return self.find_links(sink_node=node) def output_links(self, node): """ Return a list of all output links (:class:`.SchemeLink`) connected to the `node` instance. """ return self.find_links(source_node=node) def find_links(self, source_node=None, source_channel=None, sink_node=None, sink_channel=None): # TODO: Speedup - keep index of links by nodes and channels result = [] match = lambda query, value: (query is None or value == query) for link in self.__links: if match(source_node, link.source_node) and \ match(sink_node, link.sink_node) and \ match(source_channel, link.source_channel) and \ match(sink_channel, link.sink_channel): result.append(link) return result def propose_links(self, source_node, sink_node): """ Return a list of ordered (:class:`OutputSignal`, :class:`InputSignal`, weight) tuples that could be added to the scheme between `source_node` and `sink_node`. .. note:: This can depend on the links already in the scheme. """ if source_node is sink_node or \ self.is_ancestor(sink_node, source_node): # Cyclic connections are not possible. return [] outputs = source_node.output_channels() inputs = sink_node.input_channels() # Get existing links to sink channels that are Single. links = self.find_links(None, None, sink_node) already_connected_sinks = [link.sink_channel for link in links \ if link.sink_channel.single] def weight(out_c, in_c): if out_c.explicit or in_c.explicit: # Zero weight for explicit links weight = 0 else: check = [ not out_c.dynamic, # Dynamic signals are last in_c not in already_connected_sinks, bool(in_c.default), bool(out_c.default) ] weights = [2**i for i in range(len(check), 0, -1)] weight = sum([w for w, c in zip(weights, check) if c]) return weight proposed_links = [] for out_c in outputs: for in_c in inputs: if compatible_channels(out_c, in_c): proposed_links.append((out_c, in_c, weight(out_c, in_c))) return sorted(proposed_links, key=itemgetter(-1), reverse=True) def add_annotation(self, annotation): """ Add an annotation (:class:`BaseSchemeAnnotation` subclass) instance to the scheme. """ check_arg(annotation not in self.__annotations, "Cannot add the same annotation multiple times.") check_type(annotation, BaseSchemeAnnotation) self.__annotations.append(annotation) self.annotation_added.emit(annotation) def remove_annotation(self, annotation): """ Remove the `annotation` instance from the scheme. """ check_arg(annotation in self.__annotations, "Annotation is not in the scheme.") self.__annotations.remove(annotation) self.annotation_removed.emit(annotation) def clear(self): """ Remove all nodes, links, and annotation items from the scheme. """ def is_terminal(node): return not bool(self.find_links(source_node=node)) while self.nodes: terminal_nodes = filter(is_terminal, self.nodes) for node in terminal_nodes: self.remove_node(node) for annotation in self.annotations: self.remove_annotation(annotation) assert (not (self.nodes or self.links or self.annotations)) def save_to(self, stream, pretty=True, pickle_fallback=False): """ Save the scheme as an xml formated file to `stream` See also -------- .scheme_to_ows_stream """ if isinstance(stream, basestring): stream = open(stream, "wb") scheme_to_ows_stream(self, stream, pretty, pickle_fallback=pickle_fallback) def load_from(self, stream): """ Load the scheme from xml formated stream. """ if self.__nodes or self.__links or self.__annotations: # TODO: should we clear the scheme and load it. raise ValueError("Scheme is not empty.") if isinstance(stream, basestring): stream = open(stream, "rb") parse_scheme(self, stream)
class CollapsibleDockWidget(QDockWidget): """ This :class:`QDockWidget` subclass overrides the `close` header button to instead collapse to a smaller size. The contents contents to show when in each state can be set using the ``setExpandedWidget`` and ``setCollapsedWidget``. .. note:: Do not use the base class ``QDockWidget.setWidget`` method to set the docks contents. Use set[Expanded|Collapsed]Widget instead. """ #: Emitted when the dock widget's expanded state changes. expandedChanged = Signal(bool) def __init__(self, *args, **kwargs): QDockWidget.__init__(self, *args, **kwargs) self.__expandedWidget = None self.__collapsedWidget = None self.__expanded = True self.__trueMinimumWidth = -1 self.setFeatures(QDockWidget.DockWidgetClosable | \ QDockWidget.DockWidgetMovable) self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.featuresChanged.connect(self.__onFeaturesChanged) self.dockLocationChanged.connect(self.__onDockLocationChanged) # Use the toolbar horizontal extension button icon as the default # for the expand/collapse button pm = self.style().standardPixmap( QStyle.SP_ToolBarHorizontalExtensionButton ) # Rotate the icon transform = QTransform() transform.rotate(180) pm_rev = pm.transformed(transform) self.__iconRight = QIcon(pm) self.__iconLeft = QIcon(pm_rev) close = self.findChild(QAbstractButton, name="qt_dockwidget_closebutton") close.installEventFilter(self) self.__closeButton = close self.__stack = AnimatedStackedWidget() self.__stack.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.__stack.transitionStarted.connect(self.__onTransitionStarted) self.__stack.transitionFinished.connect(self.__onTransitionFinished) QDockWidget.setWidget(self, self.__stack) self.__closeButton.setIcon(self.__iconLeft) def setExpanded(self, state): """ Set the widgets `expanded` state. """ if self.__expanded != state: self.__expanded = state if state and self.__expandedWidget is not None: log.debug("Dock expanding.") self.__stack.setCurrentWidget(self.__expandedWidget) elif not state and self.__collapsedWidget is not None: log.debug("Dock collapsing.") self.__stack.setCurrentWidget(self.__collapsedWidget) self.__fixIcon() self.expandedChanged.emit(state) def expanded(self): """ Is the dock widget in expanded state. If `True` the ``expandedWidget`` will be shown, and ``collapsedWidget`` otherwise. """ return self.__expanded expanded_ = Property(bool, fset=setExpanded, fget=expanded) def setWidget(self, w): raise NotImplementedError( "Please use the 'setExpandedWidget'/'setCollapsedWidget' " "methods to set the contents of the dock widget." ) def setExpandedWidget(self, widget): """ Set the widget with contents to show while expanded. """ if widget is self.__expandedWidget: return if self.__expandedWidget is not None: self.__stack.removeWidget(self.__expandedWidget) self.__stack.insertWidget(0, widget) self.__expandedWidget = widget if self.__expanded: self.__stack.setCurrentWidget(widget) self.updateGeometry() def expandedWidget(self): """ Return the widget previously set with ``setExpandedWidget``, or ``None`` if no widget has been set. """ return self.__expandedWidget def setCollapsedWidget(self, widget): """ Set the widget with contents to show while collapsed. """ if widget is self.__collapsedWidget: return if self.__collapsedWidget is not None: self.__stack.removeWidget(self.__collapsedWidget) self.__stack.insertWidget(1, widget) self.__collapsedWidget = widget if not self.__expanded: self.__stack.setCurrentWidget(widget) self.updateGeometry() def collapsedWidget(self): """ Return the widget previously set with ``setCollapsedWidget``, or ``None`` if no widget has been set. """ return self.__collapsedWidget def setAnimationEnabled(self, animationEnabled): """ Enable/disable the transition animation. """ self.__stack.setAnimationEnabled(animationEnabled) def animationEnabled(self): """ Is transition animation enabled. """ return self.__stack.animationEnabled() def currentWidget(self): """ Return the current shown widget depending on the `expanded` state. """ if self.__expanded: return self.__expandedWidget else: return self.__collapsedWidget def expand(self): """ Expand the dock (same as ``setExpanded(True)``) """ self.setExpanded(True) def collapse(self): """ Collapse the dock (same as ``setExpanded(False)``) """ self.setExpanded(False) def eventFilter(self, obj, event): if obj is self.__closeButton: etype = event.type() if etype == QEvent.MouseButtonPress: self.setExpanded(not self.__expanded) return True elif etype == QEvent.MouseButtonDblClick or \ etype == QEvent.MouseButtonRelease: return True # TODO: which other events can trigger the button (is the button # focusable). return QDockWidget.eventFilter(self, obj, event) def event(self, event): if event.type() == QEvent.LayoutRequest: self.__fixMinimumWidth() return QDockWidget.event(self, event) def __onFeaturesChanged(self, features): pass def __onDockLocationChanged(self, area): if area == Qt.LeftDockWidgetArea: self.setLayoutDirection(Qt.LeftToRight) else: self.setLayoutDirection(Qt.RightToLeft) self.__stack.setLayoutDirection(self.parentWidget().layoutDirection()) self.__fixIcon() def __onTransitionStarted(self): log.debug("Dock transition started.") def __onTransitionFinished(self): log.debug("Dock transition finished (new width %i)", self.size().width()) def __fixMinimumWidth(self): # A workaround for forcing the QDockWidget layout to disregard the # default minimumSize which can be to wide for us (overriding the # minimumSizeHint or setting the minimum size directly does not # seem to have an effect (Qt 4.8.3). size = self.__stack.sizeHint() if size.isValid() and not size.isEmpty(): left, _, right, _ = self.getContentsMargins() width = size.width() + left + right if width < self.minimumSizeHint().width(): if not self.__hasFixedWidth(): log.debug("Overriding default minimum size " "(setFixedWidth(%i))", width) self.__trueMinimumWidth = self.minimumSizeHint().width() self.setFixedWidth(width) else: if self.__hasFixedWidth(): if width >= self.__trueMinimumWidth: # Unset the fixed size. log.debug("Restoring default minimum size " "(setFixedWidth(%i))", QWIDGETSIZE_MAX) self.__trueMinimumWidth = -1 self.setFixedWidth(QWIDGETSIZE_MAX) self.updateGeometry() else: self.setFixedWidth(width) def __hasFixedWidth(self): return self.__trueMinimumWidth >= 0 def __fixIcon(self): """Fix the dock close icon. """ direction = self.layoutDirection() if direction == Qt.LeftToRight: if self.__expanded: icon = self.__iconLeft else: icon = self.__iconRight else: if self.__expanded: icon = self.__iconRight else: icon = self.__iconLeft self.__closeButton.setIcon(icon)
class SchemeLink(QObject): """ A instantiation of a link between two :class:`.SchemeNode` instances in a :class:`.Scheme`. Parameters ---------- source_node : :class:`.SchemeNode` Source node. source_channel : :class:`OutputSignal` The source widget's signal. sink_node : :class:`.SchemeNode` The sink node. sink_channel : :class:`InputSignal` The sink widget's input signal. properties : `dict` Additional link properties. """ #: The link enabled state has changed enabled_changed = Signal(bool) #: The link dynamic enabled state has changed. dynamic_enabled_changed = Signal(bool) def __init__(self, source_node, source_channel, sink_node, sink_channel, enabled=True, properties=None, parent=None): QObject.__init__(self, parent) self.source_node = source_node if isinstance(source_channel, basestring): source_channel = source_node.output_channel(source_channel) elif source_channel not in source_node.output_channels(): raise ValueError("%r not in in nodes output channels." \ % source_channel) self.source_channel = source_channel self.sink_node = sink_node if isinstance(sink_channel, basestring): sink_channel = sink_node.input_channel(sink_channel) elif sink_channel not in sink_node.input_channels(): raise ValueError("%r not in in nodes input channels." \ % source_channel) self.sink_channel = sink_channel if not compatible_channels(source_channel, sink_channel): raise IncompatibleChannelTypeError( "Cannot connect %r to %r" \ % (source_channel.type, sink_channel.type) ) self.__enabled = enabled self.__dynamic_enabled = False self.__tool_tip = "" self.properties = properties or {} def source_type(self): """ Return the type of the source channel. """ return name_lookup(self.source_channel.type) def sink_type(self): """ Return the type of the sink channel. """ return name_lookup(self.sink_channel.type) def is_dynamic(self): """ Is this link dynamic. """ return self.source_channel.dynamic and \ issubclass(self.sink_type(), self.source_type()) and \ not (self.sink_type() is self.source_type()) def set_enabled(self, enabled): """ Enable/disable the link. """ if self.__enabled != enabled: self.__enabled = enabled self.enabled_changed.emit(enabled) def enabled(self): """ Is this link enabled. """ return self.__enabled enabled = Property(bool, fget=enabled, fset=set_enabled) def set_dynamic_enabled(self, enabled): """ Enable/disable the dynamic link. Has no effect if the link is not dynamic. """ if self.is_dynamic() and self.__dynamic_enabled != enabled: self.__dynamic_enabled = enabled self.dynamic_enabled_changed.emit(enabled) def dynamic_enabled(self): """ Is this a dynamic link and is `dynamic_enabled` set to `True` """ return self.is_dynamic() and self.__dynamic_enabled dynamic_enabled = Property(bool, fget=dynamic_enabled, fset=set_dynamic_enabled) def set_tool_tip(self, tool_tip): """ Set the link tool tip. """ if self.__tool_tip != tool_tip: self.__tool_tip = tool_tip def tool_tip(self): """ Link tool tip. """ return self.__tool_tip tool_tip = Property(str, fget=tool_tip, fset=set_tool_tip) def __str__(self): return u"{0}(({1}, {2}) -> ({3}, {4}))".format( type(self).__name__, self.source_node.title, self.source_channel.name, self.sink_node.title, self.sink_channel.name)
class ControlPointRect(QGraphicsObject): Free = 0 KeepAspectRatio = 1 KeepCenter = 2 rectChanged = Signal(QRectF) rectEdited = Signal(QRectF) def __init__(self, parent=None, rect=None, constraints=0, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setFlag(QGraphicsItem.ItemIsFocusable) self.__rect = rect if rect is not None else QRectF() self.__margins = QMargins() points = \ [ControlPoint(self, ControlPoint.Left), ControlPoint(self, ControlPoint.Top), ControlPoint(self, ControlPoint.TopLeft), ControlPoint(self, ControlPoint.Right), ControlPoint(self, ControlPoint.TopRight), ControlPoint(self, ControlPoint.Bottom), ControlPoint(self, ControlPoint.BottomLeft), ControlPoint(self, ControlPoint.BottomRight) ] assert(points == sorted(points, key=lambda p: p.anchor())) self.__points = dict((p.anchor(), p) for p in points) if self.scene(): self.__installFilter() for p in points: p.setFlag(QGraphicsItem.ItemIsFocusable) p.setFocusProxy(self) self.controlPoint(ControlPoint.Top).setConstraint(Qt.Vertical) self.controlPoint(ControlPoint.Bottom).setConstraint(Qt.Vertical) self.controlPoint(ControlPoint.Left).setConstraint(Qt.Horizontal) self.controlPoint(ControlPoint.Right).setConstraint(Qt.Horizontal) self.__constraints = constraints self.__activeControl = None self.__pointsLayout() def controlPoint(self, anchor): """ Return the anchor point (:class:`ControlPoint`) at anchor position or `None` if an anchor point is not set. """ return self.__points.get(anchor) def setRect(self, rect): """ Set the control point rectangle (:class:`QRectF`) """ if self.__rect != rect: self.__rect = QRectF(rect) self.__pointsLayout() self.prepareGeometryChange() self.rectChanged.emit(rect.normalized()) def rect(self): """ Return the control point rectangle. """ # Return the rect normalized. During the control point move the # rect can change to an invalid size, but the layout must still # know to which point does an unnormalized rect side belong, # so __rect is left unnormalized. # NOTE: This means all signal emits (rectChanged/Edited) must # also emit normalized rects return self.__rect.normalized() rect_ = Property(QRectF, fget=rect, fset=setRect, user=True) def setControlMargins(self, *margins): """Set the controls points on the margins around `rect` """ if len(margins) > 1: margins = QMargins(*margins) else: margins = margins[0] if isinstance(margins, int): margins = QMargins(margins, margins, margins, margins) if self.__margins != margins: self.__margins = margins self.__pointsLayout() def controlMargins(self): return self.__margins def setConstraints(self, constraints): raise NotImplementedError def isControlActive(self): """Return the state of the control. True if the control is active (user is dragging one of the points) False otherwise. """ return self.__activeControl is not None def itemChange(self, change, value): if change == QGraphicsItem.ItemSceneHasChanged and self.scene(): self.__installFilter() return QGraphicsObject.itemChange(self, change, value) def sceneEventFilter(self, obj, event): try: obj = toGraphicsObjectIfPossible(obj) if isinstance(obj, ControlPoint): etype = event.type() if etype == QEvent.GraphicsSceneMousePress and \ event.button() == Qt.LeftButton: self.__setActiveControl(obj) elif etype == QEvent.GraphicsSceneMouseRelease and \ event.button() == Qt.LeftButton: self.__setActiveControl(None) except Exception: log.error("Error in 'ControlPointRect.sceneEventFilter'", exc_info=True) return QGraphicsObject.sceneEventFilter(self, obj, event) def __installFilter(self): # Install filters on the control points. try: for p in self.__points.values(): p.installSceneEventFilter(self) except Exception: log.error("Error in ControlPointRect.__installFilter", exc_info=True) def __pointsLayout(self): """Layout the control points """ rect = self.__rect margins = self.__margins rect = rect.adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) center = rect.center() cx, cy = center.x(), center.y() left, top, right, bottom = \ rect.left(), rect.top(), rect.right(), rect.bottom() self.controlPoint(ControlPoint.Left).setPos(left, cy) self.controlPoint(ControlPoint.Right).setPos(right, cy) self.controlPoint(ControlPoint.Top).setPos(cx, top) self.controlPoint(ControlPoint.Bottom).setPos(cx, bottom) self.controlPoint(ControlPoint.TopLeft).setPos(left, top) self.controlPoint(ControlPoint.TopRight).setPos(right, top) self.controlPoint(ControlPoint.BottomLeft).setPos(left, bottom) self.controlPoint(ControlPoint.BottomRight).setPos(right, bottom) def __setActiveControl(self, control): if self.__activeControl != control: if self.__activeControl is not None: self.__activeControl.positionChanged[QPointF].disconnect( self.__activeControlMoved ) self.__activeControl = control if control is not None: control.positionChanged[QPointF].connect( self.__activeControlMoved ) def __activeControlMoved(self, pos): # The active control point has moved, update the control # rectangle control = self.__activeControl pos = control.pos() rect = QRectF(self.__rect) margins = self.__margins # TODO: keyboard modifiers and constraints. anchor = control.anchor() if anchor & ControlPoint.Top: rect.setTop(pos.y() + margins.top()) elif anchor & ControlPoint.Bottom: rect.setBottom(pos.y() - margins.bottom()) if anchor & ControlPoint.Left: rect.setLeft(pos.x() + margins.left()) elif anchor & ControlPoint.Right: rect.setRight(pos.x() - margins.right()) changed = self.__rect != rect self.blockSignals(True) self.setRect(rect) self.blockSignals(False) if changed: self.rectEdited.emit(rect.normalized()) def boundingRect(self): return QRectF()
class DropShadowFrame(QWidget): """ A widget drawing a drop shadow effect around the geometry of another widget (works similar to :class:`QFocusFrame`). Parameters ---------- parent : :class:`QObject` Parent object. color : :class:`QColor` The color of the drop shadow. radius : float Shadow radius. """ def __init__(self, parent=None, color=None, radius=5, **kwargs): QWidget.__init__(self, parent, **kwargs) self.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.setAttribute(Qt.WA_NoChildEventsForParent, True) self.setFocusPolicy(Qt.NoFocus) if color is None: color = self.palette().color(QPalette.Dark) self.__color = color self.__radius = radius self.__widget = None self.__widgetParent = None self.__updatePixmap() def setColor(self, color): """ Set the color of the shadow. """ if not isinstance(color, QColor): color = QColor(color) if self.__color != color: self.__color = QColor(color) self.__updatePixmap() def color(self): """ Return the color of the drop shadow. """ return QColor(self.__color) color_ = Property(QColor, fget=color, fset=setColor, designable=True, doc="Drop shadow color") def setRadius(self, radius): """ Set the drop shadow's blur radius. """ if self.__radius != radius: self.__radius = radius self.__updateGeometry() self.__updatePixmap() def radius(self): """ Return the shadow blur radius. """ return self.__radius radius_ = Property(int, fget=radius, fset=setRadius, designable=True, doc="Drop shadow blur radius.") def setWidget(self, widget): """ Set the widget around which to show the shadow. """ if self.__widget: self.__widget.removeEventFilter(self) self.__widget = widget if self.__widget: self.__widget.installEventFilter(self) # Find the parent for the frame # This is the top level window a toolbar or a viewport # of a scroll area parent = widget.parentWidget() while not (isinstance(parent, (QAbstractScrollArea, QToolBar)) or \ parent.isWindow()): parent = parent.parentWidget() if isinstance(parent, QAbstractScrollArea): parent = parent.viewport() self.__widgetParent = parent self.setParent(parent) self.stackUnder(widget) self.__updateGeometry() self.setVisible(widget.isVisible()) def widget(self): """ Return the widget that was set by `setWidget`. """ return self.__widget def paintEvent(self, event): # TODO: Use QPainter.drawPixmapFragments on Qt 4.7 opt = QStyleOption() opt.initFrom(self) pixmap = self.__shadowPixmap shadow_rect = QRectF(opt.rect) widget_rect = QRectF(self.widget().geometry()) widget_rect.moveTo(self.radius_, self.radius_) left = top = right = bottom = self.radius_ pixmap_rect = QRectF(QPointF(0, 0), QSizeF(pixmap.size())) # Shadow casting rectangle in the source pixmap. pixmap_shadow_rect = pixmap_rect.adjusted(left, top, -right, -bottom) source_rects = self.__shadowPixmapFragments(pixmap_rect, pixmap_shadow_rect) target_rects = self.__shadowPixmapFragments(shadow_rect, widget_rect) painter = QPainter(self) for source, target in zip(source_rects, target_rects): painter.drawPixmap(target, pixmap, source) painter.end() def eventFilter(self, obj, event): etype = event.type() if etype == QEvent.Move or etype == QEvent.Resize: self.__updateGeometry() elif etype == QEvent.Show: self.__updateGeometry() self.show() elif etype == QEvent.Hide: self.hide() return QWidget.eventFilter(self, obj, event) def __updateGeometry(self): """ Update the shadow geometry to fit the widget's changed geometry. """ widget = self.__widget parent = self.__widgetParent radius = self.radius_ pos = widget.pos() if parent != widget.parentWidget(): pos = widget.parentWidget().mapTo(parent, pos) geom = QRect(pos, widget.size()) geom.adjust(-radius, -radius, radius, radius) if geom != self.geometry(): self.setGeometry(geom) # Set the widget mask (punch a hole through to the `widget` instance. rect = self.rect() mask = QRegion(rect) transparent = QRegion(rect.adjusted(radius, radius, -radius, -radius)) mask = mask.subtracted(transparent) self.setMask(mask) def __updatePixmap(self): """ Update the cached shadow pixmap. """ rect_size = QSize(50, 50) left = top = right = bottom = self.radius_ # Size of the pixmap. pixmap_size = QSize(rect_size.width() + left + right, rect_size.height() + top + bottom) shadow_rect = QRect(QPoint(left, top), rect_size) pixmap = QPixmap(pixmap_size) pixmap.fill(QColor(0, 0, 0, 0)) rect_fill_color = self.palette().color(QPalette.Window) pixmap = render_drop_shadow_frame( pixmap, QRectF(shadow_rect), shadow_color=self.color_, offset=QPointF(0, 0), radius=self.radius_, rect_fill_color=rect_fill_color ) self.__shadowPixmap = pixmap self.update() def __shadowPixmapFragments(self, pixmap_rect, shadow_rect): """ Return a list of 8 QRectF fragments for drawing a shadow. """ s_left, s_top, s_right, s_bottom = \ shadow_rect.left(), shadow_rect.top(), \ shadow_rect.right(), shadow_rect.bottom() s_width, s_height = shadow_rect.width(), shadow_rect.height() p_width, p_height = pixmap_rect.width(), pixmap_rect.height() top_left = QRectF(0.0, 0.0, s_left, s_top) top = QRectF(s_left, 0.0, s_width, s_top) top_right = QRectF(s_right, 0.0, p_width - s_width, s_top) right = QRectF(s_right, s_top, p_width - s_right, s_height) right_bottom = QRectF(shadow_rect.bottomRight(), pixmap_rect.bottomRight()) bottom = QRectF(shadow_rect.bottomLeft(), pixmap_rect.bottomRight() - \ QPointF(p_width - s_right, 0.0)) bottom_left = QRectF(shadow_rect.bottomLeft() - QPointF(s_left, 0.0), pixmap_rect.bottomLeft() + QPointF(s_left, 0.0)) left = QRectF(pixmap_rect.topLeft() + QPointF(0.0, s_top), shadow_rect.bottomLeft()) return [top_left, top, top_right, right, right_bottom, bottom, bottom_left, left]
class CrossFadePixmapWidget(QWidget): """ A widget for cross fading between two pixmaps. """ def __init__(self, parent=None, pixmap1=None, pixmap2=None): QWidget.__init__(self, parent) self.setPixmap(pixmap1) self.setPixmap2(pixmap2) self.blendingFactor_ = 0.0 self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def setPixmap(self, pixmap): """ Set pixmap 1 """ self.pixmap1 = pixmap self.updateGeometry() def setPixmap2(self, pixmap): """ Set pixmap 2 """ self.pixmap2 = pixmap self.updateGeometry() def setBlendingFactor(self, factor): """ Set the blending factor between the two pixmaps. """ self.__blendingFactor = factor self.updateGeometry() def blendingFactor(self): """ Pixmap blending factor between 0.0 and 1.0 """ return self.__blendingFactor blendingFactor_ = Property(float, fget=blendingFactor, fset=setBlendingFactor) def sizeHint(self): """ Return an interpolated size between pixmap1.size() and pixmap2.size() """ if self.pixmap1 and self.pixmap2: size1 = self.pixmap1.size() size2 = self.pixmap2.size() return size1 + self.blendingFactor_ * (size2 - size1) else: return QWidget.sizeHint(self) def paintEvent(self, event): """ Paint the interpolated pixmap image. """ p = QPainter(self) p.setClipRect(event.rect()) factor = self.blendingFactor_ ** 2 if self.pixmap1 and 1. - factor: p.setOpacity(1. - factor) p.drawPixmap(QPoint(0, 0), self.pixmap1) if self.pixmap2 and factor: p.setOpacity(factor) p.drawPixmap(QPoint(0, 0), self.pixmap2)
class MenuPage(ToolTree): """ A menu page in a :class:`QuickMenu` widget, showing a list of actions. Shown actions can be disabled by setting a filtering function using the :func:`setFilterFunc`. """ def __init__(self, parent=None, title=None, icon=None, **kwargs): ToolTree.__init__(self, parent, **kwargs) if title is None: title = "" if icon is None: icon = QIcon() self.__title = title self.__icon = icon self.__sizeHint = None self.view().setItemDelegate(_MenuItemDelegate(self.view())) self.view().entered.connect(self.__onEntered) self.view().viewport().setMouseTracking(True) # Make sure the initial model is wrapped in a ItemDisableFilter. self.setModel(self.model()) def setTitle(self, title): """ Set the title of the page. """ if self.__title != title: self.__title = title self.update() def title(self): """ Return the title of this page. """ return self.__title title_ = Property(unicode, fget=title, fset=setTitle, doc="Title of the page.") def setIcon(self, icon): """ Set icon for this menu page. """ if self.__icon != icon: self.__icon = icon self.update() def icon(self): """ Return the icon of this manu page. """ return self.__icon icon_ = Property(QIcon, fget=icon, fset=setIcon, doc="Page icon") def setFilterFunc(self, func): """ Set the filtering function. `func` should a function taking a single :class:`QModelIndex` argument and returning True if the item at index should be disabled and False otherwise. To disable filtering `func` can be set to ``None``. """ proxyModel = self.view().model() proxyModel.setFilterFunc(func) def setModel(self, model): """ Reimplemented from :func:`ToolTree.setModel`. """ proxyModel = ItemDisableFilter(self) proxyModel.setSourceModel(model) ToolTree.setModel(self, proxyModel) self.__invalidateSizeHint() def setRootIndex(self, index): """ Reimplemented from :func:`ToolTree.setRootIndex` """ proxyModel = self.view().model() mappedIndex = proxyModel.mapFromSource(index) ToolTree.setRootIndex(self, mappedIndex) self.__invalidateSizeHint() def rootIndex(self): """ Reimplemented from :func:`ToolTree.rootIndex` """ proxyModel = self.view().model() return proxyModel.mapToSource(ToolTree.rootIndex(self)) def sizeHint(self): """ Reimplemented from :func:`QWidget.sizeHint`. """ if self.__sizeHint is None: view = self.view() model = view.model() # This will not work for nested items (tree). count = model.rowCount(view.rootIndex()) # 'sizeHintForColumn' is the reason for size hint caching # since it must traverse all items in the column. width = view.sizeHintForColumn(0) if count: height = view.sizeHintForRow(0) height = height * count else: height = 0 self.__sizeHint = QSize(width, height) return self.__sizeHint def __invalidateSizeHint(self): self.__sizeHint = None self.updateGeometry() def __onEntered(self, index): if not index.isValid(): return if self.view().state() != QTreeView.NoState: # The item view can emit an 'entered' signal while the model/view # is being changed (rows removed). When this happens, setting the # current item can segfault (in QTreeView::scrollTo). return if index.flags() & Qt.ItemIsEnabled: self.view().selectionModel().setCurrentIndex( index, QItemSelectionModel.ClearAndSelect)
class FramelessWindow(QWidget): """ A basic frameless window widget with rounded corners (if supported by the windowing system). """ def __init__(self, parent=None, **kwargs): QWidget.__init__(self, parent, **kwargs) self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) self.__radius = 6 self.__isTransparencySupported = is_transparency_supported() self.setAttribute(Qt.WA_TranslucentBackground, self.__isTransparencySupported) def setRadius(self, radius): """ Set the window rounded border radius. """ if self.__radius != radius: self.__radius = radius if not self.__isTransparencySupported: self.__updateMask() self.update() def radius(self): """ Return the border radius. """ return self.__radius radius_ = Property(int, fget=radius, fset=setRadius, designable=True, doc="Window border radius") def resizeEvent(self, event): QWidget.resizeEvent(self, event) if not self.__isTransparencySupported: self.__updateMask() def __updateMask(self): opt = QStyleOption() opt.init(self) rect = opt.rect size = rect.size() mask = QBitmap(size) p = QPainter(mask) p.setRenderHint(QPainter.Antialiasing) p.setBrush(Qt.black) p.setPen(Qt.NoPen) p.drawRoundedRect(rect, self.__radius, self.__radius) p.end() self.setMask(mask) def paintEvent(self, event): if self.__isTransparencySupported: opt = QStyleOption() opt.init(self) rect = opt.rect p = QPainter(self) p.setRenderHint(QPainter.Antialiasing, True) p.setBrush(opt.palette.brush(QPalette.Window)) p.setPen(Qt.NoPen) p.drawRoundedRect(rect, self.__radius, self.__radius) p.end() else: StyledWidget_paintEvent(self, event)
class ToolBoxTabButton(QToolButton): """ A tab button for an item in a :class:`ToolBox`. """ def setNativeStyling(self, state): """ Render tab buttons as native (or css styled) :class:`QToolButtons`. If set to `False` (default) the button is pained using a custom paint routine. """ self.__nativeStyling = state self.update() def nativeStyling(self): """ Use :class:`QStyle`'s to paint the class:`QToolButton` look. """ return self.__nativeStyling nativeStyling_ = Property(bool, fget=nativeStyling, fset=setNativeStyling, designable=True) def __init__(self, *args, **kwargs): self.__nativeStyling = False self.position = QStyleOptionToolBoxV2.OnlyOneTab self.selected = QStyleOptionToolBoxV2.NotAdjacent QToolButton.__init__(self, *args, **kwargs) def paintEvent(self, event): if self.__nativeStyling: QToolButton.paintEvent(self, event) else: self.__paintEventNoStyle() def __paintEventNoStyle(self): p = QPainter(self) opt = QStyleOptionToolButton() self.initStyleOption(opt) fm = QFontMetrics(opt.font) palette = opt.palette # highlight brush is used as the background for the icon and background # when the tab is expanded and as mouse hover color (lighter). brush_highlight = palette.highlight() if opt.state & QStyle.State_Sunken: # State 'down' pressed during a mouse press (slightly darker). background_brush = brush_darker(brush_highlight, 110) elif opt.state & QStyle.State_MouseOver: background_brush = brush_darker(brush_highlight, 95) elif opt.state & QStyle.State_On: background_brush = brush_highlight else: # The default button brush. background_brush = palette.button() rect = opt.rect icon = opt.icon icon_size = opt.iconSize # TODO: add shift for pressed as set by the style (PM_ButtonShift...) pm = None if not icon.isNull(): if opt.state & QStyle.State_Enabled: mode = QIcon.Normal else: mode = QIcon.Disabled pm = opt.icon.pixmap( rect.size().boundedTo(icon_size), mode, QIcon.On if opt.state & QStyle.State_On else QIcon.Off) icon_area_rect = QRect(rect) icon_area_rect.setRight(int(icon_area_rect.height() * 1.26)) text_rect = QRect(rect) text_rect.setLeft(icon_area_rect.right() + 10) # Background (TODO: Should the tab button have native # toolbutton shape, drawn using PE_PanelButtonTool or even # QToolBox tab shape) # Default outline pen pen = QPen(palette.color(QPalette.Mid)) p.save() p.setPen(Qt.NoPen) p.setBrush(QBrush(background_brush)) p.drawRect(rect) # Draw the background behind the icon if the background_brush # is different. if not opt.state & QStyle.State_On: p.setBrush(brush_highlight) p.drawRect(icon_area_rect) # Line between the icon and text p.setPen(pen) p.drawLine(icon_area_rect.topRight(), icon_area_rect.bottomRight()) if opt.state & QStyle.State_HasFocus: # Set the focus frame pen and draw the border pen = QPen(QColor(FOCUS_OUTLINE_COLOR)) p.setPen(pen) p.setBrush(Qt.NoBrush) # Adjust for pen rect = rect.adjusted(0, 0, -1, -1) p.drawRect(rect) else: p.setPen(pen) # Draw the top/bottom border if self.position == QStyleOptionToolBoxV2.OnlyOneTab or \ self.position == QStyleOptionToolBoxV2.Beginning or \ self.selected & \ QStyleOptionToolBoxV2.PreviousIsSelected: p.drawLine(rect.topLeft(), rect.topRight()) p.drawLine(rect.bottomLeft(), rect.bottomRight()) p.restore() p.save() text = fm.elidedText(opt.text, Qt.ElideRight, text_rect.width()) p.setPen(QPen(palette.color(QPalette.ButtonText))) p.setFont(opt.font) p.drawText(text_rect, int(Qt.AlignVCenter | Qt.AlignLeft) | \ int(Qt.TextSingleLine), text) if pm: pm_rect = QRect(QPoint(0, 0), pm.size()) centered_rect = QRect(pm_rect) centered_rect.moveCenter(icon_area_rect.center()) p.drawPixmap(centered_rect, pm, pm_rect) p.restore()
class SchemeLink(QObject): """ A instantiation of a link between two :class:`.SchemeNode` instances in a :class:`.Scheme`. Parameters ---------- source_node : :class:`.SchemeNode` Source node. source_channel : :class:`OutputSignal` The source widget's signal. sink_node : :class:`.SchemeNode` The sink node. sink_channel : :class:`InputSignal` The sink widget's input signal. properties : `dict` Additional link properties. """ #: The link enabled state has changed enabled_changed = Signal(bool) #: The link dynamic enabled state has changed. dynamic_enabled_changed = Signal(bool) #: Runtime link state has changed state_changed = Signal(int) class State(enum.IntEnum): """ Flags indicating the runtime state of a link """ #: A link is empty when it has no value on it Empty = 0 #: A link is active when the source node provides a value on output Active = 1 #: A link is pending when it's sink node has not yet been notified #: of a change (note that Empty|Pending is a valid state) Pending = 2 Empty, Active, Pending = State def __init__(self, source_node, source_channel, sink_node, sink_channel, enabled=True, properties=None, parent=None): QObject.__init__(self, parent) self.source_node = source_node if isinstance(source_channel, str): source_channel = source_node.output_channel(source_channel) elif source_channel not in source_node.output_channels(): raise ValueError("%r not in in nodes output channels." \ % source_channel) self.source_channel = source_channel self.sink_node = sink_node if isinstance(sink_channel, str): sink_channel = sink_node.input_channel(sink_channel) elif sink_channel not in sink_node.input_channels(): raise ValueError("%r not in in nodes input channels." \ % source_channel) self.sink_channel = sink_channel if not compatible_channels(source_channel, sink_channel): raise IncompatibleChannelTypeError( "Cannot connect %r to %r" \ % (source_channel.type, sink_channel.type) ) self.__enabled = enabled self.__dynamic_enabled = False self.__state = SchemeLink.Empty self.__tool_tip = "" self.properties = properties or {} def source_type(self): """ Return the type of the source channel. """ return name_lookup(self.source_channel.type) def sink_type(self): """ Return the type of the sink channel. """ return name_lookup(self.sink_channel.type) def is_dynamic(self): """ Is this link dynamic. """ return self.source_channel.dynamic and \ issubclass(self.sink_type(), self.source_type()) and \ not (self.sink_type() is self.source_type()) def set_enabled(self, enabled): """ Enable/disable the link. """ if self.__enabled != enabled: self.__enabled = enabled self.enabled_changed.emit(enabled) def enabled(self): """ Is this link enabled. """ return self.__enabled enabled = Property(bool, fget=enabled, fset=set_enabled) def set_dynamic_enabled(self, enabled): """ Enable/disable the dynamic link. Has no effect if the link is not dynamic. """ if self.is_dynamic() and self.__dynamic_enabled != enabled: self.__dynamic_enabled = enabled self.dynamic_enabled_changed.emit(enabled) def dynamic_enabled(self): """ Is this a dynamic link and is `dynamic_enabled` set to `True` """ return self.is_dynamic() and self.__dynamic_enabled dynamic_enabled = Property(bool, fget=dynamic_enabled, fset=set_dynamic_enabled) def set_runtime_state(self, state): """ Set the link's runtime state. Parameters ---------- state : SchemeLink.State """ if self.__state != state: self.__state = state self.state_changed.emit(state) def runtime_state(self): """ Returns ------- state : SchemeLink.State """ return self.__state def set_tool_tip(self, tool_tip): """ Set the link tool tip. """ if self.__tool_tip != tool_tip: self.__tool_tip = tool_tip def tool_tip(self): """ Link tool tip. """ return self.__tool_tip tool_tip = Property(str, fget=tool_tip, fset=set_tool_tip) def __str__(self): return "{0}(({1}, {2}) -> ({3}, {4}))".format( type(self).__name__, self.source_node.title, self.source_channel.name, self.sink_node.title, self.sink_channel.name)