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 ColorButton(QPushButton): """ Color choosing push button """ colorChanged = Signal(QColor) def __init__(self, parent=None): QPushButton.__init__(self, parent) self.setFixedSize(20, 20) self.setIconSize(QSize(12, 12)) self.clicked.connect(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.colorChanged.emit(self._color) pixmap = QPixmap(self.iconSize()) pixmap.fill(color) self.setIcon(QIcon(pixmap)) color = Property("QColor", get_color, set_color)
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)
def Q_ENUMS_mock(classvars, enumclass): #do not use, PySide2 workaround values = [ a for a in dir(enumclass) if not a.startswith('__') and not callable(getattr(enumclass, a)) ] for v in values: classvars[f'{v}'] = Property(int, ConstGetter(getattr(enumclass, v)), constant=True)
def makeProperty(name, propertyType=str, **kwargs): storageVarName = "_" + name def g(self): return eval("self." + storageVarName) def s(self, val): exec("self." + storageVarName + "=val") if "notify" in kwargs.keys(): kwargs["notify"].emit() return Property(propertyType, g, s)
def ConstProperty(classvars, typename, name): ''' This function adds a QProperty named 'name' to a class's vars() dictionary. It create the getter. *Important* a member variable named '_name' will be expected by the getter. A QProperty is exposed to QML. ''' goc_member_variable(classvars, name) classvars[f'{name}'] = Property(typename, select_getter(typename, name), constant=True)
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 six.text_type(self.__placeholderText) placeholderText_ = Property(six.text_type, 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 REButton(CodeButton): runInThread = Property(bool, designable=False) # Disabling property def __init__(self, parent): super().__init__(parent) self._plots = "[]" self._plotFields = "[[]]" self._plotKwargsList = "[{}]" self._RunEngine = "RE" plots = makeProperty("plots") plotFields = makeProperty("plotFields") plotKwargsList = makeProperty("plotKwargsList") RunEngine = makeProperty("RunEngine")
def RWProperty(classvars, typename, name): ''' This function adds a QProperty named 'name' to a class's vars() dictionary. It create the getter, setter, and signal named 'nameChanged'. *Important* a member variable named '_name' will be expected by the getter and setter. A QProperty is exposed to QML. ''' goc_member_variable(classvars, name) notify = classvars[f'{name}Changed'] = Signal() classvars[f'{name}'] = Property(typename, select_getter(typename, name), Setter(name), notify=notify)
class Transform(Product.Product): def __init__(self, parent=None): super(Transform, self).__init__(parent) self._localTransform = QMatrix4x4() self._parentTransform = None Q_CLASSINFO('DefaultProperty', 'parentTransform') localTransformChanged = Signal() def local_transform(self): return self._localTransform localTransform = Property(QMatrix4x4, local_transform, notify=localTransformChanged) def set_local_transform(self, matrix4x4): if matrix4x4 is None: matrix4x4 = QMatrix4x4() Product.assign_input(self, "localTransform", matrix4x4) Product.InputProperty(vars(), Product.Product, 'parentTransform') @Slot(result=QMatrix4x4) def worldTransform(self, update=False): if update: self.update() assert not self.dirty return self.localTransform if self.parentTransform is None \ else self.parentTransform.worldTransform() * self.localTransform @Slot(QVector3D, float, result=QQuaternion) def qFromAA(self, axis, angle_rad): return QQuaternion.fromAxisAndAngle(axis, math.degrees(angle_rad)) @Slot(float, float, float, result=QQuaternion) def qFromEuler(self, roll, pitch, yaw): return QQuaternion.fromEulerAngles(math.degrees(roll), math.degrees(pitch), math.degrees(yaw)) @Slot(QQuaternion, QVector3D, result=QMatrix4x4) def mFromTQ(self, t=QVector3D(), q=QQuaternion()): m = QMatrix4x4() m.rotate(q) m.translate(t) return m
def ROProperty(classvars, typename, name): ''' This function adds a QProperty named 'name' to a class's vars() dictionary. It creates the getter, and signal named 'nameChanged'. It also creates a set_name() setter outside of the Qt property system. *Important* a member variable named '_name' will be expected by the getter. A QProperty is exposed to QML. ''' goc_member_variable(classvars, name) notify = classvars[f'{name}Changed'] = Signal() classvars[f'{name}'] = Property(typename, select_getter(typename, name), notify=notify) classvars[f'set_{name}'] = Setter(name)
def InputProperty(classvars, typename, name, callback=None): ''' This function adds a QProperty named 'name' to a class's vars() dictionary. It create the getter, setter, and signal named 'nameChanged'. *Important* a member variable named '_name' will be expected by the getter and setter. 'callback' will be called if (and only if) a new value is set. see InputSetter for more information on 'callback' A QProperty is exposed to QML. An InputProperty is a property that turns a product dirty when needed. It can be a primitive type (e.g. int, string, bool, etc) or a Product, or a collection containing products ''' goc_member_variable(classvars, name) notify = classvars[f'{name}Changed'] = Signal() classvars[f'{name}'] = Property(typename, select_getter(typename, name), InputSetter(classvars, name, callback), notify=notify)
class GraphicsWidgetView(QGraphicsView): """ A Graphics view with a single central QGraphicsWidget which is resized fo fit into the view. """ __centralWidget: Optional[QGraphicsWidget] = None __fitInView = True __aspectMode = Qt.KeepAspectRatio __widgetResizable = False __zoomFactor = 100 def __init__(self, *args, widgetResizable=False, **kwargs): super().__init__(*args, **kwargs) self.__widgetResizable = widgetResizable self.__zoomFactor = 100 zoomin = QAction( "Zoom in", self, objectName="zoom-in-action", shortcut=QKeySequence.ZoomIn, ) zoomout = QAction( "Zoom out", self, objectName="zoom-out-action", shortcut=QKeySequence.ZoomOut, ) zoomreset = QAction( "Actual Size", self, objectName="zoom-reset-action", shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0), ) fit = QAction( "Zoom to fit", self, objectName="zoom-to-fit-action", shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_9), checkable=True, ) if hasattr(QAction, "setShortcutVisibleInContextMenu"): # Qt 5.10 for a in [zoomin, zoomout, zoomreset, fit]: a.setShortcutVisibleInContextMenu(True) @zoomin.triggered.connect def _(): self.setZoomFactor(self.__zoomFactor + 10) @zoomout.triggered.connect def _(): self.setZoomFactor(self.__zoomFactor - 10) @zoomreset.triggered.connect def _(): self.__zoomFactor = -1 self.setZoomFactor(100.) @fit.toggled.connect def _(state): self.setFitInView(state) self.addActions([zoomin, zoomout, zoomreset, fit]) self._actions = SimpleNamespace(zoomin=zoomin, zoomout=zoomout, zoomreset=zoomreset, fit=fit) def viewActions(self) -> List[QAction]: return [ self._actions.zoomout, self._actions.zoomin, self._actions.zoomreset, self._actions.fit ] def setZoomFactor(self, factor: float) -> None: """ Set the zoom level `factor` Parameters ---------- factor: Zoom level where 100 is default 50 is half the size and 200 is twice the size """ if self.__zoomFactor != factor or self.__fitInView: self.__fitInView = False self._actions.fit.setChecked(False) self.__zoomFactor = factor self.setTransform( QTransform.fromScale(*(self.__zoomFactor / 100, ) * 2)) self._actions.zoomout.setEnabled(factor >= 20) self._actions.zoomin.setEnabled(factor <= 300) self.zoomFactorChanged.emit(factor) if self.__widgetResizable: self._resizeToFit() def zoomFactor(self) -> float: """ Returns ------- factor: float The zoom factor. """ return self.__zoomFactor zoomFactorChanged = Signal(int) zoomFactor_ = Property(int, zoomFactor, setZoomFactor, notify=zoomFactorChanged) def viewportEvent(self, event: QEvent) -> bool: if event.type() == QEvent.Resize: self._layout() return super().viewportEvent(event) def setCentralWidget(self, widget: Optional[QGraphicsWidget]) -> None: """ Set the central widget. Previous widget (if set) is unset. The widget needs to be in this view's `scene()` """ if self.__centralWidget is not None: self.__centralWidget.removeEventFilter(self) self.__centralWidget.destroyed.disconnect( self.__on_centralWidgetDestroyed) self.__centralWidget = widget if widget is not None: widget.installEventFilter(self) widget.destroyed.connect(self.__on_centralWidgetDestroyed) self._layout() def centralWidget(self) -> Optional[QGraphicsWidget]: """Return the central widget.""" return self.__centralWidget @Slot(QObject) def __on_centralWidgetDestroyed(self): self.__centralWidget = None def widgetResizable(self) -> bool: """ Should the central widget be resized (via .resize()) to match the view. or should the view's scale be updated instead. """ return self.__widgetResizable def setWidgetResizable(self, resizable: bool) -> None: """ Parameters ---------- resizable: bool """ if self.__widgetResizable != resizable: self.__widgetResizable = resizable QApplication.postEvent(self, QEvent(QEvent.LayoutRequest)) def setFitInView(self, enabled: bool) -> None: if self.__fitInView != enabled: self.__fitInView = enabled self._actions.fit.setChecked(enabled) if enabled: if self.__widgetResizable: self._resizeToFit() else: self._scaleToFit() def setAspectMode(self, mode: Qt.AspectRatioMode) -> None: if self.__aspectMode != mode: self.__aspectMode = mode if self.__fitInView: self._scaleToFit() elif self.__widgetResizable: self._resizeToFit() def eventFilter(self, recv: QObject, event: QEvent) -> bool: if event.type() == QEvent.LayoutRequest \ and recv is self.__centralWidget: self._layout() return super().eventFilter(recv, event) def _layout(self) -> None: widget = self.__centralWidget if widget is None: return if self.__widgetResizable: self._resizeToFit() else: self._scaleToFit() def _resizeToFit(self): widget = self.__centralWidget size = self.__viewportContentSize() vprect = self.viewport().geometry() vprect.setSize(size) margins = self.viewportMargins() vprect = vprect.marginsRemoved(margins) viewrect = self.mapToScene(vprect).boundingRect() targetsize = viewrect.size() maxsize = widget.maximumSize() minsize = widget.minimumSize() targetsize = targetsize.expandedTo(minsize).boundedTo(maxsize) sh = widget.effectiveSizeHint(Qt.PreferredSize) policy = widget.sizePolicy() vpolicy = policy.verticalPolicy() hpolicy = policy.horizontalPolicy() if not self.__fitInView: widget.resize(sh.expandedTo(minsize).boundedTo(maxsize)) return width = adjusted_size(sh.width(), targetsize.width(), minsize.width(), maxsize.width(), hpolicy) height = adjusted_size(sh.height(), targetsize.height(), minsize.height(), maxsize.height(), vpolicy) if policy.hasHeightForWidth(): constr = QSizeF(width, -1) height = adjusted_size( widget.effectiveSizeHint(Qt.PreferredSize, constr).height(), targetsize.height(), widget.effectiveSizeHint(Qt.MinimumSize, constr).height(), widget.effectiveSizeHint(Qt.MaximumSize, constr).height(), QSizePolicy.Fixed) widget.resize(QSizeF(width, height)) def _scaleToFit(self): widget = self.__centralWidget if widget is None or not self.__fitInView: return vpsize = self.__viewportContentSize() size = widget.size() if not size.isEmpty(): sc = scaled(size, vpsize, self.__aspectMode) sx = sc.width() / size.width() sy = sc.height() / size.height() self.setTransform(QTransform().scale(sx, sy)) def __viewportContentSize(self): msize = self.maximumViewportSize() vsbar = self.verticalScrollBar() hsbar = self.horizontalScrollBar() vsbpolicy = self.verticalScrollBarPolicy() hsbpolicy = self.horizontalScrollBarPolicy() htransient = hsbar.style().styleHint(QStyle.SH_ScrollBar_Transient, None, hsbar) vtransient = vsbar.style().styleHint(QStyle.SH_ScrollBar_Transient, None, vsbar) # always reserve room for scroll bars when they are possible if vsbpolicy == Qt.ScrollBarAsNeeded and not vtransient: msize.setWidth(msize.width() - vsbar.sizeHint().width()) if hsbpolicy == Qt.ScrollBarAsNeeded and not htransient: msize.setHeight(msize.height() - hsbar.sizeHint().height()) return msize
class SchemeArrowAnnotation(BaseSchemeAnnotation): """ An arrow annotation in the scheme. """ color_changed = Signal(six.text_type) 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, six.string_types) color = six.text_type(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(six.text_type, fget=color, fset=set_color)
class SchemeTextAnnotation(BaseSchemeAnnotation): """ Text annotation in the scheme. """ # Signal emitted when the annotation text changes. text_changed = Signal(six.text_type) # 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, six.string_types) text = six.text_type(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(six.text_type, fget=font, fset=set_font)
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.initFrom(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.initFrom(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 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 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 OpenWindowButton(CodeButton): runInThread = Property(bool, designable=False) # Disabling property def deleteSubWindow(self): if hasattr(self, "subWindow"): deleteWidgetAndChildren(self.subWindow) else: logger.info("no existing subwindow") def __init__(self, parent=None): super().__init__(parent) self._fileName = QUrl() self._macros = [] self._useRelativePath = True self._useThreading = False def default_code(self): return """ from PyQt5.QtWidgets import QDialog, QFrame, QWidget, QApplication from PyQt5 import uic, QtCore from PyQt5.QtCore import QDir from PyQt5.Qt import Qt from bsstudio.functions import openFileAsString from bsstudio.widgets.embedframe import absPath, EmbedFrame import time import io ui = self.ui #if hasattr(self, "subWindow"): # print("already has subwindow") # self.subWindow.close() #self.deleteSubWindow() #self.subWindow = QDialog(self) self.subWindow = EmbedFrame(self) self.subWindow.setWindowFlags(Qt.Window) #self.subWindow = QDialog(self) self.subWindow.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.subWindow.isTopLevel = True filename = self.fileName.toLocalFile() if not QDir.isAbsolutePath(filename): filename = absPath(self.windowFileName(), filename) self.subWindow.uiFilePath = filename fileContents = openFileAsString(filename, self.macros) fileObject = io.StringIO(fileContents) uic.loadUi(fileObject, self.subWindow) #self.resize(self.subWindow.size()) self.subWindow.show() self.subWindow.update() self.subWindow.repaint() #QApplication.instance().processEvents() self.resumeChildren() """[1:] def resumeWidget(self): self._paused = False @Property("QUrl") def fileName(self): return self._fileName @fileName.setter def fileName(self, val): path = convertPath(self, val, toRelative=self._useRelativePath) if path is not None: self._fileName = path @Property(bool) def useRelativePath(self): return self._useRelativePath @useRelativePath.setter def useRelativePath(self, val): self._useRelativePath = val self.fileName = self._fileName #fileName = makeProperty("fileName", QUrl) macros = makeProperty("macros", "QStringList")
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 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(six.text_type, 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 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 = QStyleOptionToolBox.OnlyOneTab self.selected = QStyleOptionToolBox.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() foregroundrole = QPalette.ButtonText if opt.state & QStyle.State_Sunken: # State 'down' pressed during a mouse press (slightly darker). background_brush = brush_darker(brush_highlight, 110) foregroundrole = QPalette.HighlightedText elif opt.state & QStyle.State_MouseOver: background_brush = brush_darker(brush_highlight, 95) foregroundrole = QPalette.HighlightedText elif opt.state & QStyle.State_On: background_brush = brush_highlight foregroundrole = QPalette.HighlightedText else: # The default button brush. background_brush = palette.button() rect = opt.rect 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 == QStyleOptionToolBox.OnlyOneTab or \ self.position == QStyleOptionToolBox.Beginning or \ self.selected & \ QStyleOptionToolBox.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(foregroundrole))) p.setFont(opt.font) p.drawText(text_rect, int(Qt.AlignVCenter | Qt.AlignLeft) | \ int(Qt.TextSingleLine), text) if not opt.icon.isNull(): if opt.state & QStyle.State_Enabled: mode = QIcon.Normal else: mode = QIcon.Disabled if opt.state & QStyle.State_On: state = QIcon.On else: state = QIcon.Off icon_area_rect = icon_area_rect icon_rect = QRect(QPoint(0, 0), opt.iconSize) icon_rect.moveCenter(icon_area_rect.center()) opt.icon.paint(p, icon_rect, Qt.AlignCenter, mode, state) p.restore()
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.dataChanged.disconnect(self.__on_dataChanged) 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.dataChanged.connect(self.__on_dataChanged) self.__model.rowsInserted.connect(self.__on_rowsInserted) self.__model.rowsRemoved.connect(self.__on_rowsRemoved) self.__initFromModel(self.__model) def __initFromModel(self, model): for row in range(model.rowCount()): self.__insertItem(model.index(row, 0), self.count()) def __insertItem(self, item, index): """ Insert category item (`QModelIndex`) at index. """ grid = WidgetToolGrid() grid.setModel(item.model(), item) grid.actionTriggered.connect(self.triggered) grid.actionHovered.connect(self.hovered) grid.setIconSize(self.__iconSize) grid.setButtonSize(self.__buttonSize) text = item_text(item) icon = item_icon(item) tooltip = item_tooltip(item) # 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 brush = qtcompat.qunwrap(item.data(Qt.BackgroundRole)) if not isinstance(brush, QBrush): brush = qtcompat.qunwrap(item.data(QtWidgetRegistry.BACKGROUND_ROLE)) if not isinstance(brush, QBrush): 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_dataChanged(self, topLeft, bottomRight): parent = topLeft.parent() if not parent.isValid(): for row in range(topLeft.row(), bottomRight.row() + 1): item = topLeft.sibling(row, topLeft.column()) button = self.tabButton(row) button.setIcon(item_icon(item)) button.setText(item_text(item)) button.setToolTip(item_tooltip(item)) 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.isValid(): for i in range(start, end + 1): item = self.__model.index(i, 0) 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.isValid(): for i in range(end, start - 1, -1): self.removeItem(i)
def pyqtProperty(*args, **kwargs): kwargs.pop('doc', None) return Property(*args, **kwargs)
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, six.string_types): 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, six.string_types): 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 ToolBox(QFrame): """ A tool box widget. """ # Emitted when a tab is toggled. tabToogled = Signal(int, bool) def setExclusive(self, exclusive): """ Set exclusive tabs (only one tab can be open at a time). """ if self.__exclusive != exclusive: self.__exclusive = exclusive self.__tabActionGroup.setExclusive(exclusive) checked = self.__tabActionGroup.checkedAction() if checked is None: # The action group can be out of sync with the actions state # when switching between exclusive states. actions_checked = [ page.action for page in self.__pages if page.action.isChecked() ] if actions_checked: checked = actions_checked[0] # Trigger/toggle remaining open pages if exclusive and checked is not None: for page in self.__pages: if checked != page.action and page.action.isChecked(): page.action.trigger() def exclusive(self): """ Are the tabs in the toolbox exclusive. """ return self.__exclusive exclusive_ = Property(bool, fget=exclusive, fset=setExclusive, designable=True, doc="Exclusive tabs") def __init__(self, parent=None, **kwargs): QFrame.__init__(self, parent, **kwargs) self.__pages = [] self.__tabButtonHeight = -1 self.__tabIconSize = QSize() self.__exclusive = False self.__setupUi() def __setupUi(self): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) # Scroll area for the contents. self.__scrollArea = \ _ToolBoxScrollArea(self, objectName="toolbox-scroll-area") self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.__scrollArea.setFrameStyle(QScrollArea.NoFrame) self.__scrollArea.setWidgetResizable(True) # A widget with all of the contents. # The tabs/contents are placed in the layout inside this widget self.__contents = QWidget(self.__scrollArea, objectName="toolbox-contents") self.__contentsLayout = _ToolBoxLayout( sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize, spacing=0) self.__contentsLayout.setContentsMargins(0, 0, 0, 0) self.__contents.setLayout(self.__contentsLayout) self.__scrollArea.setWidget(self.__contents) layout.addWidget(self.__scrollArea) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self.__tabActionGroup = \ QActionGroup(self, objectName="toolbox-tab-action-group") self.__tabActionGroup.setExclusive(self.__exclusive) self.__actionMapper = QSignalMapper(self) self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled) def setTabButtonHeight(self, height): """ Set the tab button height. """ if self.__tabButtonHeight != height: self.__tabButtonHeight = height for page in self.__pages: page.button.setFixedHeight(height) def tabButtonHeight(self): """ Return the tab button height. """ return self.__tabButtonHeight def setTabIconSize(self, size): """ Set the tab button icon size. """ if self.__tabIconSize != size: self.__tabIconSize = size for page in self.__pages: page.button.setIconSize(size) def tabIconSize(self): """ Return the tab icon size. """ return self.__tabIconSize def tabButton(self, index): """ Return the tab button at `index` """ return self.__pages[index].button def tabAction(self, index): """ Return open/close action for the tab at `index`. """ return self.__pages[index].action def addItem(self, widget, text, icon=None, toolTip=None): """ Append the `widget` in a new tab and return its index. Parameters ---------- widget : :class:`QWidget` A widget to be inserted. The toolbox takes ownership of the widget. text : str Name/title of the new tab. icon : :class:`QIcon`, optional An icon for the tab button. toolTip : str, optional Tool tip for the tab button. """ return self.insertItem(self.count(), widget, text, icon, toolTip) def insertItem(self, index, widget, text, icon=None, toolTip=None): """ Insert the `widget` in a new tab at position `index`. See also -------- ToolBox.addItem """ button = self.createTabButton(widget, text, icon, toolTip) self.__contentsLayout.insertWidget(index * 2, button) self.__contentsLayout.insertWidget(index * 2 + 1, widget) widget.hide() page = _ToolBoxPage(index, widget, button.defaultAction(), button) self.__pages.insert(index, page) for i in range(index + 1, self.count()): self.__pages[i] = self.__pages[i]._replace(index=i) self.__updatePositions() # Show (open) the first tab. if self.count() == 1 and index == 0: page.action.trigger() self.__updateSelected() self.updateGeometry() return index def removeItem(self, index): """ Remove the widget at `index`. .. note:: The widget hidden but is is not deleted. """ self.__contentsLayout.takeAt(2 * index + 1) self.__contentsLayout.takeAt(2 * index) page = self.__pages.pop(index) # Update the page indexes for i in range(index, self.count()): self.__pages[i] = self.__pages[i]._replace(index=i) page.button.deleteLater() # Hide the widget and reparent to self # This follows QToolBox.removeItem page.widget.hide() page.widget.setParent(self) self.__updatePositions() self.__updateSelected() self.updateGeometry() def count(self): """ Return the number of widgets inserted in the toolbox. """ return len(self.__pages) def widget(self, index): """ Return the widget at `index`. """ return self.__pages[index].widget def createTabButton(self, widget, text, icon=None, toolTip=None): """ Create the tab button for `widget`. """ action = QAction(text, self) action.setCheckable(True) if icon: action.setIcon(icon) if toolTip: action.setToolTip(toolTip) self.__tabActionGroup.addAction(action) self.__actionMapper.setMapping(action, action) action.toggled.connect(self.__actionMapper.map) button = ToolBoxTabButton(self, objectName="toolbox-tab-button") button.setDefaultAction(action) button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) if self.__tabIconSize.isValid(): button.setIconSize(self.__tabIconSize) if self.__tabButtonHeight > 0: button.setFixedHeight(self.__tabButtonHeight) return button def ensureWidgetVisible(self, child, xmargin=50, ymargin=50): """ Scroll the contents so child widget instance is visible inside the viewport. """ self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin) def sizeHint(self): hint = self.__contentsLayout.sizeHint() if self.count(): # Compute max width of hidden widgets also. scroll = self.__scrollArea scroll_w = scroll.verticalScrollBar().sizeHint().width() frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2 max_w = max([p.widget.sizeHint().width() for p in self.__pages]) hint = QSize( max(max_w, hint.width()) + scroll_w + frame_w, hint.height()) return QSize(200, 200).expandedTo(hint) def __onTabActionToogled(self, action): page = find(self.__pages, action, key=attrgetter("action")) on = action.isChecked() page.widget.setVisible(on) index = page.index if index > 0: # Update the `previous` tab buttons style hints previous = self.__pages[index - 1].button flag = QStyleOptionToolBox.NextIsSelected if on: previous.selected |= flag else: previous.selected &= ~flag previous.update() if index < self.count() - 1: next = self.__pages[index + 1].button flag = QStyleOptionToolBox.PreviousIsSelected if on: next.selected |= flag else: next.selected &= ~flag next.update() self.tabToogled.emit(index, on) self.__contentsLayout.invalidate() def __updateSelected(self): """Update the tab buttons selected style flags. """ if self.count() == 0: return opt = QStyleOptionToolBox def update(button, next_sel, prev_sel): if next_sel: button.selected |= opt.NextIsSelected else: button.selected &= ~opt.NextIsSelected if prev_sel: button.selected |= opt.PreviousIsSelected else: button.selected &= ~opt.PreviousIsSelected button.update() if self.count() == 1: update(self.__pages[0].button, False, False) elif self.count() >= 2: pages = self.__pages for i in range(1, self.count() - 1): update(pages[i].button, pages[i + 1].action.isChecked(), pages[i - 1].action.isChecked()) def __updatePositions(self): """Update the tab buttons position style flags. """ if self.count() == 0: return elif self.count() == 1: self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab else: self.__pages[0].button.position = QStyleOptionToolBox.Beginning self.__pages[-1].button.position = QStyleOptionToolBox.End for p in self.__pages[1:-1]: p.button.position = QStyleOptionToolBox.Middle for p in self.__pages: p.button.update()
class Product(QObject): ''' classdocs ''' productDirty = Signal() productClean = Signal() def __init__(self, parent=None): super(Product, self).__init__(parent) ''' Constructor ''' self._dirty = False self._dependsOn = [] self._dependencies = [] self._error = None self._producer = None self._autoUpdate = False @Property(QObject, notify=productClean) def bind(self): return self def set_dirty(self, d): if self._dirty != d: self._dirty = d self.dirtyChanged.emit() if self._dirty and self._autoUpdate: QTimer.singleShot( 0, self.update ) # schedule an update as soon as we go back to event loop, but not before dirtyChanged = Signal() dirty = Property(bool, Getter('dirty'), set_dirty, dirtyChanged) RWProperty(vars(), bool, 'autoUpdate') def _update(self): ''' update function to override ''' pass @Slot() def update(self): if self.dirty: self._error = None for d in self._dependencies: if not d.update(): self._error = d._error return False try: self._update() except Exception as e: self._error = e print(traceback.format_exc()) self.makeClean() return self._error is None @Slot() def makeDirty(self): if not self.dirty: self.dirty = True self.productDirty.emit() @Slot() def makeClean(self): if self.dirty: self.dirty = False self.productClean.emit() def set_dependsOn(self, v): ''' *Important" this property is meant to be used only from QML. Use add/remove_dependency() from python. ''' old = self._dependsOn if assign_input(self, "dependsOn", v): for d in old: self.remove_dependency(d) for d in self._dependsOn: self.add_dependency(d) dependsOnChanged = Signal() dependsOn = Property(list, Getter('dependsOn'), set_dependsOn, dependsOnChanged) def add_dependency(self, d): if d is not None: self._dependencies.append(d) d.productDirty.connect(self.makeDirty) self.makeDirty() def remove_dependency(self, d): if d is not None: self._dependencies.remove(d) d.productDirty.disconnect(self.makeDirty) self.makeDirty() def set_producer(self, producer): if self._producer is not None: raise RuntimeError( "Error: tried to call set a set_producer() twice on " + str(self) + ".") assert (issubclass(type(producer), Product)) self._producer = producer self.add_dependency(producer) producer.productClean.connect(self.makeClean)
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 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)