class XFindWidget(QWidget): """ """ __designer_icon__ = resources.find('img/search.png') def __init__( self, parent = None ): super(XFindWidget, self).__init__( parent ) # define custom properties self._textEdit = None self._webView = None self._lastCursor = QTextCursor() self._lastText = '' self._closeButton = QToolButton(self) self._closeButton.setIcon(QIcon(resources.find('img/close.png'))) self._closeButton.setAutoRaise(True) self._closeButton.setToolTip('Hide the Find Field.') self._searchEdit = XLineEdit(self) self._searchEdit.setHint('search for...') self._previousButton = QToolButton(self) self._previousButton.setIcon(QIcon(resources.find('img/back.png'))) self._previousButton.setAutoRaise(True) self._previousButton.setToolTip('Find Previous') self._nextButton = QToolButton(self) self._nextButton.setIcon(QIcon(resources.find('img/forward.png'))) self._nextButton.setAutoRaise(True) self._nextButton.setToolTip('Find Next') self._caseSensitiveCheckbox = QCheckBox(self) self._caseSensitiveCheckbox.setText('Case Sensitive') self._wholeWordsCheckbox = QCheckBox(self) self._wholeWordsCheckbox.setText('Whole Words Only') self._regexCheckbox = QCheckBox(self) self._regexCheckbox.setText('Use Regex') self._findAction = QAction(self) self._findAction.setText('Find...') self._findAction.setIcon(QIcon(resources.find('img/search.png'))) self._findAction.setToolTip('Find in Text') self._findAction.setShortcut(QKeySequence('Ctrl+F')) self._findAction.setShortcutContext(Qt.WidgetWithChildrenShortcut) # layout the widgets layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget( self._closeButton ) layout.addWidget( self._searchEdit ) layout.addWidget( self._previousButton ) layout.addWidget( self._nextButton ) layout.addWidget( self._caseSensitiveCheckbox ) layout.addWidget( self._wholeWordsCheckbox ) layout.addWidget( self._regexCheckbox ) self.setLayout(layout) # create connections self._findAction.triggered.connect( self.show ) self._searchEdit.textChanged.connect( self.findNext ) self._closeButton.clicked.connect( self.hide ) self._previousButton.clicked.connect( self.findPrev ) self._nextButton.clicked.connect( self.findNext ) self._caseSensitiveCheckbox.clicked.connect( self.findNext ) self._wholeWordsCheckbox.clicked.connect( self.findNext ) self._searchEdit.returnPressed.connect( self.findNext ) self._regexCheckbox.clicked.connect( self.findNext ) def find( self, flags = 0 ): """ Looks throught the text document based on the current criteria. The \ inputed flags will be merged with the generated search flags. :param flags | <QTextDocument.FindFlag> :return <bool> | success """ # check against the web and text views if ( not (self._textEdit or self._webView) ): fg = QColor('darkRed') bg = QColor('red').lighter(180) palette = self.palette() palette.setColor(palette.Text, fg) palette.setColor(palette.Base, bg) self._searchEdit.setPalette(palette) self._searchEdit.setToolTip( 'No Text Edit is linked.' ) return False if ( self._caseSensitiveCheckbox.isChecked() ): flags |= QTextDocument.FindCaseSensitively if ( self._textEdit and self._wholeWordsCheckbox.isChecked() ): flags |= QTextDocument.FindWholeWords terms = self._searchEdit.text() if ( terms != self._lastText ): self._lastCursor = QTextCursor() if ( self._regexCheckbox.isChecked() ): terms = QRegExp(terms) palette = self.palette() # search on a text edit if ( self._textEdit ): cursor = self._textEdit.document().find(terms, self._lastCursor, QTextDocument.FindFlags(flags)) found = not cursor.isNull() self._lastCursor = cursor self._textEdit.setTextCursor(cursor) elif ( QWebPage ): flags = QWebPage.FindFlags(flags) flags |= QWebPage.FindWrapsAroundDocument found = self._webView.findText(terms, flags) self._lastText = self._searchEdit.text() if ( not terms or found ): fg = palette.color(palette.Text) bg = palette.color(palette.Base) else: fg = QColor('darkRed') bg = QColor('red').lighter(180) palette.setColor(palette.Text, fg) palette.setColor(palette.Base, bg) self._searchEdit.setPalette(palette) return found def findNext( self ): """ Looks for the search terms that come up next based on the criteria. :return <bool> | success """ return self.find() def findPrev( self ): """ Looks for the search terms that come up last based on the criteria. :return <bool> | success """ return self.find(QTextDocument.FindBackward) def setTextEdit( self, textEdit ): """ Sets the text edit that this find widget will use to search. :param textEdit | <QTextEdit> """ if ( self._textEdit ): self._textEdit.removeAction(self._findAction) self._textEdit = textEdit if ( textEdit ): textEdit.addAction(self._findAction) def setWebView( self, webView ): """ Sets the web view edit that this find widget will use to search. :param webView | <QWebView> """ if ( self._webView ): self._webView.removeAction(self._findAction) self._webView = webView if ( webView ): webView.addAction(self._findAction) def show( self ): """ Sets this widget visible and then makes the find field have focus. """ super(XFindWidget, self).show() self._searchEdit.setFocus() def textEdit( self ): """ Returns the text edit linked with this find widget. :return <QTextEdit> """ return self._textEdit def webView( self ): """ Returns the text edit linked with this find widget. :return <QWebView> """ return self._webView
class XPopupWidget(QWidget): """ """ Direction = enum('North', 'South', 'East', 'West') Mode = enum('Popup', 'Dialog', 'ToolTip') Anchor = enum('TopLeft', 'TopCenter', 'TopRight', 'LeftTop', 'LeftCenter', 'LeftBottom', 'RightTop', 'RightCenter', 'RightBottom', 'BottomLeft', 'BottomCenter', 'BottomRight') aboutToShow = Signal() accepted = Signal() closed = Signal() rejected = Signal() resetRequested = Signal() shown = Signal() buttonClicked = Signal(QAbstractButton) def __init__(self, parent=None, buttons=None): super(XPopupWidget, self).__init__(parent) # define custom properties self._anchor = XPopupWidget.Anchor.TopCenter self._autoCalculateAnchor = False self._autoCloseOnAccept = True self._autoCloseOnReject = True self._autoCloseOnFocusOut = False self._autoDefault = True self._first = True self._animated = False self._currentMode = None self._positionLinkedTo = [] self._possibleAnchors = XPopupWidget.Anchor.all() # define controls self._result = 0 self._resizable = True self._popupPadding = 10 self._titleBarVisible = True self._buttonBoxVisible = True self._dialogButton = QToolButton(self) self._closeButton = QToolButton(self) self._scrollArea = QScrollArea(self) self._sizeGrip = QSizeGrip(self) self._sizeGrip.setFixedWidth(12) self._sizeGrip.setFixedHeight(12) self._leftSizeGrip = QSizeGrip(self) self._leftSizeGrip.setFixedWidth(12) self._leftSizeGrip.setFixedHeight(12) if buttons is None: buttons = QDialogButtonBox.NoButton self._buttonBox = QDialogButtonBox(buttons, Qt.Horizontal, self) self._buttonBox.setContentsMargins(3, 0, 3, 9) self._scrollArea.setWidgetResizable(True) self._scrollArea.setFrameShape(QScrollArea.NoFrame) self._scrollArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) palette = self.palette() self._scrollArea.setPalette(palette) self._dialogButton.setToolTip('Popout to Dialog') self._closeButton.setToolTip('Close Popup') for btn in (self._dialogButton, self._closeButton): btn.setAutoRaise(True) btn.setIconSize(QSize(14, 14)) btn.setMaximumSize(16, 16) # setup the icons icon = QIcon(projexui.resources.find('img/dialog.png')) self._dialogButton.setIcon(icon) icon = QIcon(projexui.resources.find('img/close.png')) self._closeButton.setIcon(icon) # define the ui hlayout = QHBoxLayout() hlayout.setSpacing(0) hlayout.addStretch(1) hlayout.addWidget(self._dialogButton) hlayout.addWidget(self._closeButton) hlayout.setContentsMargins(0, 0, 0, 0) hlayout2 = QHBoxLayout() hlayout2.addWidget(self._buttonBox) hlayout2.setContentsMargins(0, 0, 3, 0) vlayout = QVBoxLayout() vlayout.addLayout(hlayout) vlayout.addWidget(self._scrollArea) vlayout.addLayout(hlayout2) vlayout.setContentsMargins(3, 2, 3, 2) vlayout.setSpacing(0) self.setLayout(vlayout) self.setPositionLinkedTo(parent) # set default properties self.setAutoFillBackground(True) self.setBackgroundRole(QPalette.Window) self.setWindowTitle('Popup') self.setFocusPolicy(Qt.StrongFocus) self.setCurrentMode(XPopupWidget.Mode.Popup) # create connections self._dialogButton.clicked.connect(self.setDialogMode) self._closeButton.clicked.connect(self.reject) self._buttonBox.accepted.connect(self.accept) self._buttonBox.rejected.connect(self.reject) self._buttonBox.clicked.connect(self.handleButtonClick) def addButton(self, button, role=QDialogButtonBox.ActionRole): """ Adds a custom button to the button box for this popup widget. :param button | <QAbstractButton> || <str> :return <button> || None (based on if a button or string was given) """ return self._buttonBox.addButton(button, role) def adjustContentsMargins( self ): """ Adjusts the contents for this widget based on the anchor and \ mode. """ anchor = self.anchor() mode = self.currentMode() # margins for a dialog if ( mode == XPopupWidget.Mode.Dialog ): self.setContentsMargins(0, 0, 0, 0) # margins for a top anchor point elif ( anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.TopRight) ): self.setContentsMargins(0, self.popupPadding() + 5, 0, 0) # margins for a bottom anchor point elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight) ): self.setContentsMargins(0, 0, 0, self.popupPadding()) # margins for a left anchor point elif ( anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.LeftBottom) ): self.setContentsMargins(self.popupPadding(), 0, 0, 0) # margins for a right anchor point else: self.setContentsMargins(0, 0, self.popupPadding(), 0) self.adjustMask() def adjustMask(self): """ Updates the alpha mask for this popup widget. """ if self.currentMode() == XPopupWidget.Mode.Dialog: self.clearMask() return path = self.borderPath() bitmap = QBitmap(self.width(), self.height()) bitmap.fill(QColor('white')) with XPainter(bitmap) as painter: painter.setRenderHint(XPainter.Antialiasing) pen = QPen(QColor('black')) pen.setWidthF(0.75) painter.setPen(pen) painter.setBrush(QColor('black')) painter.drawPath(path) self.setMask(bitmap) def adjustSize(self): """ Adjusts the size of this popup to best fit the new widget size. """ widget = self.centralWidget() if widget is None: super(XPopupWidget, self).adjustSize() return widget.adjustSize() hint = widget.minimumSizeHint() size = widget.minimumSize() width = max(size.width(), hint.width()) height = max(size.height(), hint.height()) width += 20 height += 20 if self._buttonBoxVisible: height += self.buttonBox().height() + 10 if self._titleBarVisible: height += max(self._dialogButton.height(), self._closeButton.height()) + 10 curr_w = self.width() curr_h = self.height() # determine if we need to move based on our anchor anchor = self.anchor() if anchor & (self.Anchor.LeftBottom | self.Anchor.RightBottom | \ self.Anchor.BottomLeft | self.Anchor.BottomCenter | \ self.Anchor.BottomRight): delta_y = height - curr_h elif anchor & (self.Anchor.LeftCenter | self.Anchor.RightCenter): delta_y = (height - curr_h) / 2 else: delta_y = 0 if anchor & (self.Anchor.RightTop | self.Anchor.RightCenter | \ self.Anchor.RightTop | self.Anchor.TopRight): delta_x = width - curr_w elif anchor & (self.Anchor.TopCenter | self.Anchor.BottomCenter): delta_x = (width - curr_w) / 2 else: delta_x = 0 self.setMinimumSize(width, height) self.resize(width, height) pos = self.pos() pos.setX(pos.x() - delta_x) pos.setY(pos.y() - delta_y) self.move(pos) @Slot() def accept(self): """ Emits the accepted signal and closes the popup. """ self._result = 1 if not self.signalsBlocked(): self.accepted.emit() if self.autoCloseOnAccept(): self.close() def anchor( self ): """ Returns the anchor point for this popup widget. :return <XPopupWidget.Anchor> """ return self._anchor def autoCalculateAnchor( self ): """ Returns whether or not this popup should calculate the anchor point on popup based on the parent widget and the popup point. :return <bool> """ return self._autoCalculateAnchor def autoCloseOnAccept( self ): """ Returns whether or not this popup widget manages its own close on accept behavior. :return <bool> """ return self._autoCloseOnAccept def autoCloseOnReject( self ): """ Returns whether or not this popup widget manages its own close on reject behavior. :return <bool> """ return self._autoCloseOnReject def autoCloseOnFocusOut(self): """ Returns whether or not this popup widget should auto-close when the user clicks off the view. :return <bool> """ return self._autoCloseOnFocusOut def autoDefault(self): """ Returns whether or not clicking enter should default to the accept key. :return <bool> """ return self._autoDefault def borderPath(self): """ Returns the border path that will be drawn for this widget. :return <QPainterPath> """ path = QPainterPath() x = 1 y = 1 w = self.width() - 2 h = self.height() - 2 pad = self.popupPadding() anchor = self.anchor() # create a path for a top-center based popup if anchor == XPopupWidget.Anchor.TopCenter: path.moveTo(x, y + pad) path.lineTo(x + ((w/2) - pad), y + pad) path.lineTo(x + (w/2), y) path.lineTo(x + ((w/2) + pad), y + pad) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a top-left based popup elif anchor == XPopupWidget.Anchor.TopLeft: path.moveTo(x, y + pad) path.lineTo(x + pad, y) path.lineTo(x + 2 * pad, y + pad) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a top-right based popup elif anchor == XPopupWidget.Anchor.TopRight: path.moveTo(x, y + pad) path.lineTo(x + w - 2 * pad, y + pad) path.lineTo(x + w - pad, y) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a bottom-left based popup elif anchor == XPopupWidget.Anchor.BottomLeft: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + 2 * pad, y + h - pad) path.lineTo(x + pad, y + h) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a south based popup elif anchor == XPopupWidget.Anchor.BottomCenter: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + ((w/2) + pad), y + h - pad) path.lineTo(x + (w/2), y + h) path.lineTo(x + ((w/2) - pad), y + h - pad) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a bottom-right based popup elif anchor == XPopupWidget.Anchor.BottomRight: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + w - pad, y + h) path.lineTo(x + w - 2 * pad, y + h - pad) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a right-top based popup elif anchor == XPopupWidget.Anchor.RightTop: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w, y + pad) path.lineTo(x + w - pad, y + 2 * pad) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a right-center based popup elif anchor == XPopupWidget.Anchor.RightCenter: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w - pad, y + ((h/2) - pad)) path.lineTo(x + w, y + (h/2)) path.lineTo(x + w - pad, y + ((h/2) + pad)) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a right-bottom based popup elif anchor == XPopupWidget.Anchor.RightBottom: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w - pad, y + h - 2 * pad) path.lineTo(x + w, y + h - pad) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a left-top based popup elif anchor == XPopupWidget.Anchor.LeftTop: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x + pad, y + 2 * pad) path.lineTo(x, y + pad) path.lineTo(x + pad, y) # create a path for an left-center based popup elif anchor == XPopupWidget.Anchor.LeftCenter: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x + pad, y + ((h/2) + pad)) path.lineTo(x, y + (h/2)) path.lineTo(x + pad, y + ((h/2) - pad)) path.lineTo(x + pad, y) # create a path for a left-bottom based popup elif anchor == XPopupWidget.Anchor.LeftBottom: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x, y + h - pad) path.lineTo(x + pad, y + h - 2 * pad) path.lineTo(x + pad, y) return path def buttonBox( self ): """ Returns the button box that is used to control this popup widget. :return <QDialogButtonBox> """ return self._buttonBox def centralWidget( self ): """ Returns the central widget that is being used by this popup. :return <QWidget> """ return self._scrollArea.widget() def close(self): """ Closes the popup widget and central widget. """ widget = self.centralWidget() if widget and not widget.close(): return super(XPopupWidget, self).close() def closeEvent(self, event): widget = self.centralWidget() if widget and not widget.close() and \ self.currentMode() != XPopupWidget.Mode.ToolTip: event.ignore() else: super(XPopupWidget, self).closeEvent(event) self.closed.emit() def currentMode( self ): """ Returns the current mode for this widget. :return <XPopupWidget.Mode> """ return self._currentMode @deprecatedmethod('XPopupWidget', 'Direction is no longer used, use anchor instead') def direction( self ): """ Returns the current direction parameter for this widget. :return <XPopupWidget.Direction> """ anchor = self.anchor() if ( anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.TopRight) ): return XPopupWidget.Direction.North elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight) ): return XPopupWidget.Direction.South elif ( anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.LeftBottom) ): return XPopupWidget.Direction.East else: return XPopupWidget.Direction.West def exec_(self, pos=None): self._result = 0 self.setWindowModality(Qt.ApplicationModal) self.popup(pos) while self.isVisible(): QApplication.processEvents() return self.result() def eventFilter(self, object, event): """ Processes when the window is moving to update the position for the popup if in popup mode. :param object | <QObject> event | <QEvent> """ if not self.isVisible(): return False links = self.positionLinkedTo() is_dialog = self.currentMode() == self.Mode.Dialog if object not in links: return False if event.type() == event.Close: self.close() return False if event.type() == event.Hide and not is_dialog: self.hide() return False if event.type() == event.Move and not is_dialog: deltaPos = event.pos() - event.oldPos() self.move(self.pos() + deltaPos) return False if self.currentMode() != self.Mode.ToolTip: return False if event.type() == event.Leave: pos = object.mapFromGlobal(QCursor.pos()) if (not object.rect().contains(pos)): self.close() event.accept() return True if event.type() in (event.MouseButtonPress, event.MouseButtonDblClick): self.close() event.accept() return True return False @Slot(QAbstractButton) def handleButtonClick(self, button): """ Handles the button click for this widget. If the Reset button was clicked, then the resetRequested signal will be emitted. All buttons will emit the buttonClicked signal. :param button | <QAbstractButton> """ if ( self.signalsBlocked() ): return if ( button == self._buttonBox.button(QDialogButtonBox.Reset) ): self.resetRequested.emit() self.buttonClicked.emit(button) def isAnimated(self): """ Returns whether or not the popup widget should animate its opacity when it is shown. :return <bool> """ return self._animated def isPossibleAnchor(self, anchor): return bool(anchor & self._possibleAnchors) def isResizable(self): """ Returns if this popup is resizable or not. :return <bool> """ return self._resizable def keyPressEvent( self, event ): """ Looks for the Esc key to close the popup. :param event | <QKeyEvent> """ if ( event.key() == Qt.Key_Escape ): self.reject() event.accept() return elif ( event.key() in (Qt.Key_Return, Qt.Key_Enter) ): if self._autoDefault: self.accept() event.accept() return super(XPopupWidget, self).keyPressEvent(event) def mapAnchorFrom(self, widget, point): """ Returns the anchor point that best fits within the given widget from the inputed global position. :param widget | <QWidget> point | <QPoint> :return <XPopupWidget.Anchor> """ screen_geom = QtGui.QDesktopWidget(self).screenGeometry() # calculate the end rect for each position Anchor = self.Anchor w = self.width() h = self.height() possible_rects = { # top anchors Anchor.TopLeft: QtCore.QRect(point.x(), point.y(), w, h), Anchor.TopCenter: QtCore.QRect(point.x() - w / 2, point.y(), w, h), Anchor.TopRight: QtCore.QRect(point.x() - w, point.y(), w, h), # left anchors Anchor.LeftTop: QtCore.QRect(point.x(), point.y(), w, h), Anchor.LeftCenter: QtCore.QRect(point.x(), point.y() - h / 2, w, h), Anchor.LeftBottom: QtCore.QRect(point.x(), point.y() - h, w, h), # bottom anchors Anchor.BottomLeft: QtCore.QRect(point.x(), point.y() - h, w, h), Anchor.BottomCenter: QtCore.QRect(point.x() - w / 2, point.y() - h, w, h), Anchor.BottomRight: QtCore.QRect(point.x() - w, point.y() - h, w, h), # right anchors Anchor.RightTop: QtCore.QRect(point.x() - self.width(), point.y(), w, h), Anchor.RightCenter: QtCore.QRect(point.x() - self.width(), point.y() - h / 2, w, h), Anchor.RightBottom: QtCore.QRect(point.x() - self.width(), point.y() - h, w ,h) } for anchor in (Anchor.TopCenter, Anchor.BottomCenter, Anchor.LeftCenter, Anchor.RightCenter, Anchor.TopLeft, Anchor.LeftTop, Anchor.BottomLeft, Anchor.LeftBottom, Anchor.TopRight, Anchor.RightTop, Anchor.BottomRight, Anchor.RightBottom): if not self.isPossibleAnchor(anchor): continue rect = possible_rects[anchor] if screen_geom.contains(rect): return anchor return self.anchor() def popup(self, pos=None): """ Pops up this widget at the inputed position. The inputed point should \ be in global space. :param pos | <QPoint> :return <bool> success """ if self._first and self.centralWidget() is not None: self.adjustSize() self._first = False if not self.signalsBlocked(): self.aboutToShow.emit() if not pos: pos = QCursor.pos() if self.currentMode() == XPopupWidget.Mode.Dialog and \ self.isVisible(): return False elif self.currentMode() == XPopupWidget.Mode.Dialog: self.setPopupMode() # auto-calculate the point if self.autoCalculateAnchor(): self.setAnchor(self.mapAnchorFrom(self.parent(), pos)) pad = self.popupPadding() # determine where to move based on the anchor anchor = self.anchor() # MODIFY X POSITION # align x-left if ( anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.BottomLeft) ): pos.setX(pos.x() - pad) # align x-center elif ( anchor & (XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.BottomCenter) ): pos.setX(pos.x() - self.width() / 2) # align x-right elif ( anchor & (XPopupWidget.Anchor.TopRight | XPopupWidget.Anchor.BottomRight) ): pos.setX(pos.x() - self.width() + pad) # align x-padded elif ( anchor & (XPopupWidget.Anchor.RightTop | XPopupWidget.Anchor.RightCenter | XPopupWidget.Anchor.RightBottom) ): pos.setX(pos.x() - self.width()) # MODIFY Y POSITION # align y-top if ( anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.RightTop) ): pos.setY(pos.y() - pad) # align y-center elif ( anchor & (XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.RightCenter) ): pos.setY(pos.y() - self.height() / 2) # align y-bottom elif ( anchor & (XPopupWidget.Anchor.LeftBottom | XPopupWidget.Anchor.RightBottom) ): pos.setY(pos.y() - self.height() + pad) # align y-padded elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight) ): pos.setY(pos.y() - self.height()) self.adjustMask() self.move(pos) self.update() self.setUpdatesEnabled(True) if self.isAnimated(): anim = QPropertyAnimation(self, 'windowOpacity') anim.setParent(self) anim.setStartValue(0.0) anim.setEndValue(self.windowOpacity()) anim.setDuration(500) anim.finished.connect(anim.deleteLater) self.setWindowOpacity(0.0) else: anim = None self.show() if self.currentMode() != XPopupWidget.Mode.ToolTip: self.activateWindow() widget = self.centralWidget() if widget: self.centralWidget().setFocus() if anim: anim.start() if not self.signalsBlocked(): self.shown.emit() return True def paintEvent(self, event): """ Overloads the paint event to handle painting pointers for the popup \ mode. :param event | <QPaintEvent> """ # use the base technique for the dialog mode if self.currentMode() == XPopupWidget.Mode.Dialog: super(XPopupWidget, self).paintEvent(event) return # setup the coloring options palette = self.palette() with XPainter(self) as painter: pen = QPen(palette.color(palette.Window).darker(130)) pen.setWidthF(1.75) painter.setPen(pen) painter.setRenderHint(painter.Antialiasing) painter.setBrush(palette.color(palette.Window)) painter.drawPath(self.borderPath()) def popupPadding(self): """ Returns the amount of pixels to pad the popup arrow for this widget. :return <int> """ return self._popupPadding def possibleAnchors(self): return self._possibleAnchors def positionLinkedTo(self): """ Returns the widget that this popup is linked to for positional changes. :return [<QWidget>, ..] """ return self._positionLinkedTo @Slot() def reject(self): """ Emits the accepted signal and closes the popup. """ self._result = 0 if not self.signalsBlocked(): self.rejected.emit() if self.autoCloseOnReject(): self.close() def result(self): return self._result def resizeEvent(self, event): """ Resizes this widget and updates the mask. :param event | <QResizeEvent> """ self.setUpdatesEnabled(False) super(XPopupWidget, self).resizeEvent(event) self.adjustMask() self.setUpdatesEnabled(True) x = self.width() - self._sizeGrip.width() y = self.height() - self._sizeGrip.height() self._leftSizeGrip.move(0, y) self._sizeGrip.move(x, y) def scrollArea(self): """ Returns the scroll area widget for this popup. :return <QScrollArea> """ return self._scrollArea def setAnimated(self, state): """ Sets whether or not the popup widget should animate its opacity when it is shown. :param state | <bool> """ self._animated = state self.setAttribute(Qt.WA_TranslucentBackground, state) def setAutoCloseOnAccept( self, state ): """ Sets whether or not the popup handles closing for accepting states. :param state | <bool> """ self._autoCloseOnAccept = state def setAutoCloseOnReject( self, state ): """ Sets whether or not the popup handles closing for rejecting states. :param state | <bool> """ self._autoCloseOnReject = state def setAutoDefault(self, state): """ Sets whether or not the buttons should respond to defaulting options when the user is interacting with it. :param state | <bool> """ self._autoDefault = state for button in self.buttonBox().buttons(): button.setAutoDefault(state) button.setDefault(state) def setAnchor( self, anchor ): """ Sets the anchor position for this popup widget to the inputed point. :param anchor | <XPopupWidget.Anchor> """ self._anchor = anchor self.adjustContentsMargins() def setAutoCalculateAnchor(self, state=True): """ Sets whether or not this widget should auto-calculate the anchor point based on the parent position when the popup is triggered. :param state | <bool> """ self._autoCalculateAnchor = state def setAutoCloseOnFocusOut(self, state): """ Sets whether or not this popup widget should auto-close when the user clicks off the view. :param state | <bool> """ self._autoCloseOnFocusOut = state self.updateModeSettings() def setCentralWidget( self, widget ): """ Sets the central widget that will be used by this popup. :param widget | <QWidget> || None """ self._scrollArea.takeWidget() self._scrollArea.setWidget(widget) self.adjustSize() def setCurrentMode( self, mode ): """ Sets the current mode for this dialog to the inputed mode. :param mode | <XPopupWidget.Mode> """ if ( self._currentMode == mode ): return self._currentMode = mode self.updateModeSettings() @Slot() def setDialogMode(self): """ Sets the current mode value to Dialog. """ self.setCurrentMode(XPopupWidget.Mode.Dialog) @deprecatedmethod('XPopupWidget', 'Direction is no longer used, use setAnchor instead') def setDirection( self, direction ): """ Sets the direction for this widget to the inputed direction. :param direction | <XPopupWidget.Direction> """ if ( direction == XPopupWidget.Direction.North ): self.setAnchor(XPopupWidget.Anchor.TopCenter) elif ( direction == XPopupWidget.Direction.South ): self.setAnchor(XPopupWidget.Anchor.BottomCenter) elif ( direction == XPopupWidget.Direction.East ): self.setAnchor(XPopupWidget.Anchor.LeftCenter) else: self.setAnchor(XPopupWidget.Anchor.RightCenter) def setPalette(self, palette): """ Sets the palette for this widget and the scroll area. :param palette | <QPalette> """ super(XPopupWidget, self).setPalette(palette) self._scrollArea.setPalette(palette) def setPopupMode( self ): """ Sets the current mode value to Popup. """ self.setCurrentMode(XPopupWidget.Mode.Popup) def setPopupPadding( self, padding ): """ Sets the amount to pad the popup area when displaying this widget. :param padding | <int> """ self._popupPadding = padding self.adjustContentsMargins() def setPossibleAnchors(self, anchors): self._possibleAnchors = anchors def setPositionLinkedTo(self, widgets): """ Sets the widget that this popup will be linked to for positional changes. :param widgets | <QWidget> || [<QWidget>, ..] """ if type(widgets) in (list, set, tuple): new_widgets = list(widgets) else: new_widgets = [] widget = widgets while widget: widget.installEventFilter(self) new_widgets.append(widget) widget = widget.parent() self._positionLinkedTo = new_widgets def setResizable( self, state ): self._resizable = state self._sizeGrip.setVisible(state) self._leftSizeGrip.setVisible(state) def setShowButtonBox(self, state): self._buttonBoxVisible = state self.buttonBox().setVisible(state) def setShowTitleBar(self, state): self._titleBarVisible = state self._dialogButton.setVisible(state) self._closeButton.setVisible(state) def setToolTipMode(self): """ Sets the mode for this popup widget to ToolTip """ self.setCurrentMode(XPopupWidget.Mode.ToolTip) def setVisible(self, state): super(XPopupWidget, self).setVisible(state) widget = self.centralWidget() if widget: widget.setVisible(state) def timerEvent( self, event ): """ When the timer finishes, hide the tooltip popup widget. :param event | <QEvent> """ if self.currentMode() == XPopupWidget.Mode.ToolTip: self.killTimer(event.timerId()) event.accept() self.close() else: super(XPopupWidget, self).timerEvent(event) def updateModeSettings(self): mode = self.currentMode() is_visible = self.isVisible() # display as a floating dialog if mode == XPopupWidget.Mode.Dialog: self.setWindowFlags(Qt.Dialog | Qt.Tool) self.setAttribute(Qt.WA_TransparentForMouseEvents, False) self._closeButton.setVisible(False) self._dialogButton.setVisible(False) # display as a user tooltip elif mode == XPopupWidget.Mode.ToolTip: flags = Qt.Popup | Qt.FramelessWindowHint self.setWindowFlags(flags) self.setBackgroundRole(QPalette.Window) self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setShowTitleBar(False) self.setShowButtonBox(False) self.setFocusPolicy(Qt.NoFocus) # hide the scrollbars policy = Qt.ScrollBarAlwaysOff self._scrollArea.setVerticalScrollBarPolicy(policy) self._scrollArea.setHorizontalScrollBarPolicy(policy) # display as a popup widget else: flags = Qt.Popup | Qt.FramelessWindowHint if not self.autoCloseOnFocusOut(): flags |= Qt.Tool self.setWindowFlags(flags) self._closeButton.setVisible(self._titleBarVisible) self._dialogButton.setVisible(self._titleBarVisible) self.setBackgroundRole(QPalette.Window) self.adjustContentsMargins() if ( is_visible ): self.show() @staticmethod @deprecatedmethod('XPopupWidget', 'This method no longer has an effect as we are not '\ 'storing references to the tooltip.') def hideToolTip(key = None): """ Hides any existing tooltip popup widgets. :warning This method is deprecated! """ pass @staticmethod def showToolTip( text, point = None, anchor = None, parent = None, background = None, foreground = None, key = None, seconds = 5 ): """ Displays a popup widget as a tooltip bubble. :param text | <str> point | <QPoint> || None anchor | <XPopupWidget.Mode.Anchor> || None parent | <QWidget> || None background | <QColor> || None foreground | <QColor> || None key | <str> || None seconds | <int> """ if point is None: point = QCursor.pos() if parent is None: parent = QApplication.activeWindow() if anchor is None and parent is None: anchor = XPopupWidget.Anchor.TopCenter # create a new tooltip widget widget = XPopupWidget(parent) widget.setToolTipMode() widget.setResizable(False) # create the tooltip label label = QLabel(text, widget) label.setOpenExternalLinks(True) label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) label.setMargin(3) label.setIndent(3) label.adjustSize() widget.setCentralWidget(label) # update the tip label.adjustSize() widget.adjustSize() palette = widget.palette() if not background: background = palette.color(palette.ToolTipBase) if not foreground: foreground = palette.color(palette.ToolTipText) palette.setColor(palette.Window, QColor(background)) palette.setColor(palette.WindowText, QColor(foreground)) widget.setPalette(palette) widget.centralWidget().setPalette(palette) if anchor is None: widget.setAutoCalculateAnchor(True) else: widget.setAnchor(anchor) widget.setAutoCloseOnFocusOut(True) widget.setAttribute(Qt.WA_DeleteOnClose) widget.popup(point) widget.startTimer(1000 * seconds) return widget
class XPopupWidget(QWidget): """ """ Direction = enum('North', 'South', 'East', 'West') Mode = enum('Popup', 'Dialog', 'ToolTip') Anchor = enum('TopLeft', 'TopCenter', 'TopRight', 'LeftTop', 'LeftCenter', 'LeftBottom', 'RightTop', 'RightCenter', 'RightBottom', 'BottomLeft', 'BottomCenter', 'BottomRight') aboutToShow = Signal() accepted = Signal() closed = Signal() rejected = Signal() resetRequested = Signal() shown = Signal() buttonClicked = Signal(QAbstractButton) def __init__(self, parent=None, buttons=None): super(XPopupWidget, self).__init__(parent) # define custom properties self._anchor = XPopupWidget.Anchor.TopCenter self._autoCalculateAnchor = False self._autoCloseOnAccept = True self._autoCloseOnReject = True self._autoCloseOnFocusOut = False self._autoDefault = True self._first = True self._animated = False self._currentMode = None self._positionLinkedTo = [] self._possibleAnchors = XPopupWidget.Anchor.all() # define controls self._result = 0 self._resizable = True self._popupPadding = 10 self._titleBarVisible = True self._buttonBoxVisible = True self._dialogButton = QToolButton(self) self._closeButton = QToolButton(self) self._scrollArea = QScrollArea(self) self._sizeGrip = QSizeGrip(self) self._sizeGrip.setFixedWidth(12) self._sizeGrip.setFixedHeight(12) self._leftSizeGrip = QSizeGrip(self) self._leftSizeGrip.setFixedWidth(12) self._leftSizeGrip.setFixedHeight(12) if buttons is None: buttons = QDialogButtonBox.NoButton self._buttonBox = QDialogButtonBox(buttons, Qt.Horizontal, self) self._buttonBox.setContentsMargins(3, 0, 3, 9) self._scrollArea.setWidgetResizable(True) self._scrollArea.setFrameShape(QScrollArea.NoFrame) self._scrollArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) palette = self.palette() self._scrollArea.setPalette(palette) self._dialogButton.setToolTip('Popout to Dialog') self._closeButton.setToolTip('Close Popup') for btn in (self._dialogButton, self._closeButton): btn.setAutoRaise(True) btn.setIconSize(QSize(14, 14)) btn.setMaximumSize(16, 16) # setup the icons icon = QIcon(projexui.resources.find('img/dialog.png')) self._dialogButton.setIcon(icon) icon = QIcon(projexui.resources.find('img/close.png')) self._closeButton.setIcon(icon) # define the ui hlayout = QHBoxLayout() hlayout.setSpacing(0) hlayout.addStretch(1) hlayout.addWidget(self._dialogButton) hlayout.addWidget(self._closeButton) hlayout.setContentsMargins(0, 0, 0, 0) hlayout2 = QHBoxLayout() hlayout2.addWidget(self._buttonBox) hlayout2.setContentsMargins(0, 0, 3, 0) vlayout = QVBoxLayout() vlayout.addLayout(hlayout) vlayout.addWidget(self._scrollArea) vlayout.addLayout(hlayout2) vlayout.setContentsMargins(3, 2, 3, 2) vlayout.setSpacing(0) self.setLayout(vlayout) self.setPositionLinkedTo(parent) # set default properties self.setAutoFillBackground(True) self.setBackgroundRole(QPalette.Window) self.setWindowTitle('Popup') self.setFocusPolicy(Qt.StrongFocus) self.setCurrentMode(XPopupWidget.Mode.Popup) # create connections self._dialogButton.clicked.connect(self.setDialogMode) self._closeButton.clicked.connect(self.reject) self._buttonBox.accepted.connect(self.accept) self._buttonBox.rejected.connect(self.reject) self._buttonBox.clicked.connect(self.handleButtonClick) def addButton(self, button, role=QDialogButtonBox.ActionRole): """ Adds a custom button to the button box for this popup widget. :param button | <QAbstractButton> || <str> :return <button> || None (based on if a button or string was given) """ return self._buttonBox.addButton(button, role) def adjustContentsMargins(self): """ Adjusts the contents for this widget based on the anchor and \ mode. """ anchor = self.anchor() mode = self.currentMode() # margins for a dialog if (mode == XPopupWidget.Mode.Dialog): self.setContentsMargins(0, 0, 0, 0) # margins for a top anchor point elif (anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.TopRight)): self.setContentsMargins(0, self.popupPadding() + 5, 0, 0) # margins for a bottom anchor point elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight)): self.setContentsMargins(0, 0, 0, self.popupPadding()) # margins for a left anchor point elif (anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.LeftBottom)): self.setContentsMargins(self.popupPadding(), 0, 0, 0) # margins for a right anchor point else: self.setContentsMargins(0, 0, self.popupPadding(), 0) self.adjustMask() def adjustMask(self): """ Updates the alpha mask for this popup widget. """ if self.currentMode() == XPopupWidget.Mode.Dialog: self.clearMask() return path = self.borderPath() bitmap = QBitmap(self.width(), self.height()) bitmap.fill(QColor('white')) with XPainter(bitmap) as painter: painter.setRenderHint(XPainter.Antialiasing) pen = QPen(QColor('black')) pen.setWidthF(0.75) painter.setPen(pen) painter.setBrush(QColor('black')) painter.drawPath(path) self.setMask(bitmap) def adjustSize(self): """ Adjusts the size of this popup to best fit the new widget size. """ widget = self.centralWidget() if widget is None: super(XPopupWidget, self).adjustSize() return widget.adjustSize() hint = widget.minimumSizeHint() size = widget.minimumSize() width = max(size.width(), hint.width()) height = max(size.height(), hint.height()) width += 20 height += 20 if self._buttonBoxVisible: height += self.buttonBox().height() + 10 if self._titleBarVisible: height += max(self._dialogButton.height(), self._closeButton.height()) + 10 curr_w = self.width() curr_h = self.height() # determine if we need to move based on our anchor anchor = self.anchor() if anchor & (self.Anchor.LeftBottom | self.Anchor.RightBottom | \ self.Anchor.BottomLeft | self.Anchor.BottomCenter | \ self.Anchor.BottomRight): delta_y = height - curr_h elif anchor & (self.Anchor.LeftCenter | self.Anchor.RightCenter): delta_y = (height - curr_h) / 2 else: delta_y = 0 if anchor & (self.Anchor.RightTop | self.Anchor.RightCenter | \ self.Anchor.RightTop | self.Anchor.TopRight): delta_x = width - curr_w elif anchor & (self.Anchor.TopCenter | self.Anchor.BottomCenter): delta_x = (width - curr_w) / 2 else: delta_x = 0 self.setMinimumSize(width, height) self.resize(width, height) pos = self.pos() pos.setX(pos.x() - delta_x) pos.setY(pos.y() - delta_y) self.move(pos) @Slot() def accept(self): """ Emits the accepted signal and closes the popup. """ self._result = 1 if not self.signalsBlocked(): self.accepted.emit() if self.autoCloseOnAccept(): self.close() def anchor(self): """ Returns the anchor point for this popup widget. :return <XPopupWidget.Anchor> """ return self._anchor def autoCalculateAnchor(self): """ Returns whether or not this popup should calculate the anchor point on popup based on the parent widget and the popup point. :return <bool> """ return self._autoCalculateAnchor def autoCloseOnAccept(self): """ Returns whether or not this popup widget manages its own close on accept behavior. :return <bool> """ return self._autoCloseOnAccept def autoCloseOnReject(self): """ Returns whether or not this popup widget manages its own close on reject behavior. :return <bool> """ return self._autoCloseOnReject def autoCloseOnFocusOut(self): """ Returns whether or not this popup widget should auto-close when the user clicks off the view. :return <bool> """ return self._autoCloseOnFocusOut def autoDefault(self): """ Returns whether or not clicking enter should default to the accept key. :return <bool> """ return self._autoDefault def borderPath(self): """ Returns the border path that will be drawn for this widget. :return <QPainterPath> """ path = QPainterPath() x = 1 y = 1 w = self.width() - 2 h = self.height() - 2 pad = self.popupPadding() anchor = self.anchor() # create a path for a top-center based popup if anchor == XPopupWidget.Anchor.TopCenter: path.moveTo(x, y + pad) path.lineTo(x + ((w / 2) - pad), y + pad) path.lineTo(x + (w / 2), y) path.lineTo(x + ((w / 2) + pad), y + pad) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a top-left based popup elif anchor == XPopupWidget.Anchor.TopLeft: path.moveTo(x, y + pad) path.lineTo(x + pad, y) path.lineTo(x + 2 * pad, y + pad) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a top-right based popup elif anchor == XPopupWidget.Anchor.TopRight: path.moveTo(x, y + pad) path.lineTo(x + w - 2 * pad, y + pad) path.lineTo(x + w - pad, y) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a bottom-left based popup elif anchor == XPopupWidget.Anchor.BottomLeft: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + 2 * pad, y + h - pad) path.lineTo(x + pad, y + h) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a south based popup elif anchor == XPopupWidget.Anchor.BottomCenter: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + ((w / 2) + pad), y + h - pad) path.lineTo(x + (w / 2), y + h) path.lineTo(x + ((w / 2) - pad), y + h - pad) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a bottom-right based popup elif anchor == XPopupWidget.Anchor.BottomRight: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + w - pad, y + h) path.lineTo(x + w - 2 * pad, y + h - pad) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a right-top based popup elif anchor == XPopupWidget.Anchor.RightTop: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w, y + pad) path.lineTo(x + w - pad, y + 2 * pad) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a right-center based popup elif anchor == XPopupWidget.Anchor.RightCenter: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w - pad, y + ((h / 2) - pad)) path.lineTo(x + w, y + (h / 2)) path.lineTo(x + w - pad, y + ((h / 2) + pad)) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a right-bottom based popup elif anchor == XPopupWidget.Anchor.RightBottom: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w - pad, y + h - 2 * pad) path.lineTo(x + w, y + h - pad) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a left-top based popup elif anchor == XPopupWidget.Anchor.LeftTop: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x + pad, y + 2 * pad) path.lineTo(x, y + pad) path.lineTo(x + pad, y) # create a path for an left-center based popup elif anchor == XPopupWidget.Anchor.LeftCenter: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x + pad, y + ((h / 2) + pad)) path.lineTo(x, y + (h / 2)) path.lineTo(x + pad, y + ((h / 2) - pad)) path.lineTo(x + pad, y) # create a path for a left-bottom based popup elif anchor == XPopupWidget.Anchor.LeftBottom: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x, y + h - pad) path.lineTo(x + pad, y + h - 2 * pad) path.lineTo(x + pad, y) return path def buttonBox(self): """ Returns the button box that is used to control this popup widget. :return <QDialogButtonBox> """ return self._buttonBox def centralWidget(self): """ Returns the central widget that is being used by this popup. :return <QWidget> """ return self._scrollArea.widget() def close(self): """ Closes the popup widget and central widget. """ widget = self.centralWidget() if widget and not widget.close(): return super(XPopupWidget, self).close() def closeEvent(self, event): widget = self.centralWidget() if widget and not widget.close() and \ self.currentMode() != XPopupWidget.Mode.ToolTip: event.ignore() else: super(XPopupWidget, self).closeEvent(event) self.closed.emit() def currentMode(self): """ Returns the current mode for this widget. :return <XPopupWidget.Mode> """ return self._currentMode @deprecatedmethod('XPopupWidget', 'Direction is no longer used, use anchor instead') def direction(self): """ Returns the current direction parameter for this widget. :return <XPopupWidget.Direction> """ anchor = self.anchor() if (anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.TopRight)): return XPopupWidget.Direction.North elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight)): return XPopupWidget.Direction.South elif (anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.LeftBottom)): return XPopupWidget.Direction.East else: return XPopupWidget.Direction.West def exec_(self, pos=None): self._result = 0 self.setWindowModality(Qt.ApplicationModal) self.popup(pos) while self.isVisible(): QApplication.processEvents() return self.result() def eventFilter(self, object, event): """ Processes when the window is moving to update the position for the popup if in popup mode. :param object | <QObject> event | <QEvent> """ if not self.isVisible(): return False links = self.positionLinkedTo() is_dialog = self.currentMode() == self.Mode.Dialog if object not in links: return False if event.type() == event.Close: self.close() return False if event.type() == event.Hide and not is_dialog: self.hide() return False if event.type() == event.Move and not is_dialog: deltaPos = event.pos() - event.oldPos() self.move(self.pos() + deltaPos) return False if self.currentMode() != self.Mode.ToolTip: return False if event.type() == event.Leave: pos = object.mapFromGlobal(QCursor.pos()) if (not object.rect().contains(pos)): self.close() event.accept() return True if event.type() in (event.MouseButtonPress, event.MouseButtonDblClick): self.close() event.accept() return True return False @Slot(QAbstractButton) def handleButtonClick(self, button): """ Handles the button click for this widget. If the Reset button was clicked, then the resetRequested signal will be emitted. All buttons will emit the buttonClicked signal. :param button | <QAbstractButton> """ if (self.signalsBlocked()): return if (button == self._buttonBox.button(QDialogButtonBox.Reset)): self.resetRequested.emit() self.buttonClicked.emit(button) def isAnimated(self): """ Returns whether or not the popup widget should animate its opacity when it is shown. :return <bool> """ return self._animated def isPossibleAnchor(self, anchor): return bool(anchor & self._possibleAnchors) def isResizable(self): """ Returns if this popup is resizable or not. :return <bool> """ return self._resizable def keyPressEvent(self, event): """ Looks for the Esc key to close the popup. :param event | <QKeyEvent> """ if (event.key() == Qt.Key_Escape): self.reject() event.accept() return elif (event.key() in (Qt.Key_Return, Qt.Key_Enter)): if self._autoDefault: self.accept() event.accept() return super(XPopupWidget, self).keyPressEvent(event) def mapAnchorFrom(self, widget, point): """ Returns the anchor point that best fits within the given widget from the inputed global position. :param widget | <QWidget> point | <QPoint> :return <XPopupWidget.Anchor> """ screen_geom = QtGui.QDesktopWidget(self).screenGeometry() # calculate the end rect for each position Anchor = self.Anchor w = self.width() h = self.height() possible_rects = { # top anchors Anchor.TopLeft: QtCore.QRect(point.x(), point.y(), w, h), Anchor.TopCenter: QtCore.QRect(point.x() - w / 2, point.y(), w, h), Anchor.TopRight: QtCore.QRect(point.x() - w, point.y(), w, h), # left anchors Anchor.LeftTop: QtCore.QRect(point.x(), point.y(), w, h), Anchor.LeftCenter: QtCore.QRect(point.x(), point.y() - h / 2, w, h), Anchor.LeftBottom: QtCore.QRect(point.x(), point.y() - h, w, h), # bottom anchors Anchor.BottomLeft: QtCore.QRect(point.x(), point.y() - h, w, h), Anchor.BottomCenter: QtCore.QRect(point.x() - w / 2, point.y() - h, w, h), Anchor.BottomRight: QtCore.QRect(point.x() - w, point.y() - h, w, h), # right anchors Anchor.RightTop: QtCore.QRect(point.x() - self.width(), point.y(), w, h), Anchor.RightCenter: QtCore.QRect(point.x() - self.width(), point.y() - h / 2, w, h), Anchor.RightBottom: QtCore.QRect(point.x() - self.width(), point.y() - h, w, h) } for anchor in (Anchor.TopCenter, Anchor.BottomCenter, Anchor.LeftCenter, Anchor.RightCenter, Anchor.TopLeft, Anchor.LeftTop, Anchor.BottomLeft, Anchor.LeftBottom, Anchor.TopRight, Anchor.RightTop, Anchor.BottomRight, Anchor.RightBottom): if not self.isPossibleAnchor(anchor): continue rect = possible_rects[anchor] if screen_geom.contains(rect): return anchor return self.anchor() def popup(self, pos=None): """ Pops up this widget at the inputed position. The inputed point should \ be in global space. :param pos | <QPoint> :return <bool> success """ if self._first and self.centralWidget() is not None: self.adjustSize() self._first = False if not self.signalsBlocked(): self.aboutToShow.emit() if not pos: pos = QCursor.pos() if self.currentMode() == XPopupWidget.Mode.Dialog and \ self.isVisible(): return False elif self.currentMode() == XPopupWidget.Mode.Dialog: self.setPopupMode() # auto-calculate the point if self.autoCalculateAnchor(): self.setAnchor(self.mapAnchorFrom(self.parent(), pos)) pad = self.popupPadding() # determine where to move based on the anchor anchor = self.anchor() # MODIFY X POSITION # align x-left if (anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.BottomLeft)): pos.setX(pos.x() - pad) # align x-center elif (anchor & (XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.BottomCenter)): pos.setX(pos.x() - self.width() / 2) # align x-right elif ( anchor & (XPopupWidget.Anchor.TopRight | XPopupWidget.Anchor.BottomRight)): pos.setX(pos.x() - self.width() + pad) # align x-padded elif (anchor & (XPopupWidget.Anchor.RightTop | XPopupWidget.Anchor.RightCenter | XPopupWidget.Anchor.RightBottom)): pos.setX(pos.x() - self.width()) # MODIFY Y POSITION # align y-top if (anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.RightTop)): pos.setY(pos.y() - pad) # align y-center elif (anchor & (XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.RightCenter)): pos.setY(pos.y() - self.height() / 2) # align y-bottom elif (anchor & (XPopupWidget.Anchor.LeftBottom | XPopupWidget.Anchor.RightBottom)): pos.setY(pos.y() - self.height() + pad) # align y-padded elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight)): pos.setY(pos.y() - self.height()) self.adjustMask() self.move(pos) self.update() self.setUpdatesEnabled(True) if self.isAnimated(): anim = QPropertyAnimation(self, 'windowOpacity') anim.setParent(self) anim.setStartValue(0.0) anim.setEndValue(self.windowOpacity()) anim.setDuration(500) anim.finished.connect(anim.deleteLater) self.setWindowOpacity(0.0) else: anim = None self.show() if self.currentMode() != XPopupWidget.Mode.ToolTip: self.activateWindow() widget = self.centralWidget() if widget: self.centralWidget().setFocus() if anim: anim.start() if not self.signalsBlocked(): self.shown.emit() return True def paintEvent(self, event): """ Overloads the paint event to handle painting pointers for the popup \ mode. :param event | <QPaintEvent> """ # use the base technique for the dialog mode if self.currentMode() == XPopupWidget.Mode.Dialog: super(XPopupWidget, self).paintEvent(event) return # setup the coloring options palette = self.palette() with XPainter(self) as painter: pen = QPen(palette.color(palette.Window).darker(130)) pen.setWidthF(1.75) painter.setPen(pen) painter.setRenderHint(painter.Antialiasing) painter.setBrush(palette.color(palette.Window)) painter.drawPath(self.borderPath()) def popupPadding(self): """ Returns the amount of pixels to pad the popup arrow for this widget. :return <int> """ return self._popupPadding def possibleAnchors(self): return self._possibleAnchors def positionLinkedTo(self): """ Returns the widget that this popup is linked to for positional changes. :return [<QWidget>, ..] """ return self._positionLinkedTo @Slot() def reject(self): """ Emits the accepted signal and closes the popup. """ self._result = 0 if not self.signalsBlocked(): self.rejected.emit() if self.autoCloseOnReject(): self.close() def result(self): return self._result def resizeEvent(self, event): """ Resizes this widget and updates the mask. :param event | <QResizeEvent> """ self.setUpdatesEnabled(False) super(XPopupWidget, self).resizeEvent(event) self.adjustMask() self.setUpdatesEnabled(True) x = self.width() - self._sizeGrip.width() y = self.height() - self._sizeGrip.height() self._leftSizeGrip.move(0, y) self._sizeGrip.move(x, y) def scrollArea(self): """ Returns the scroll area widget for this popup. :return <QScrollArea> """ return self._scrollArea def setAnimated(self, state): """ Sets whether or not the popup widget should animate its opacity when it is shown. :param state | <bool> """ self._animated = state self.setAttribute(Qt.WA_TranslucentBackground, state) def setAutoCloseOnAccept(self, state): """ Sets whether or not the popup handles closing for accepting states. :param state | <bool> """ self._autoCloseOnAccept = state def setAutoCloseOnReject(self, state): """ Sets whether or not the popup handles closing for rejecting states. :param state | <bool> """ self._autoCloseOnReject = state def setAutoDefault(self, state): """ Sets whether or not the buttons should respond to defaulting options when the user is interacting with it. :param state | <bool> """ self._autoDefault = state for button in self.buttonBox().buttons(): button.setAutoDefault(state) button.setDefault(state) def setAnchor(self, anchor): """ Sets the anchor position for this popup widget to the inputed point. :param anchor | <XPopupWidget.Anchor> """ self._anchor = anchor self.adjustContentsMargins() def setAutoCalculateAnchor(self, state=True): """ Sets whether or not this widget should auto-calculate the anchor point based on the parent position when the popup is triggered. :param state | <bool> """ self._autoCalculateAnchor = state def setAutoCloseOnFocusOut(self, state): """ Sets whether or not this popup widget should auto-close when the user clicks off the view. :param state | <bool> """ self._autoCloseOnFocusOut = state self.updateModeSettings() def setCentralWidget(self, widget): """ Sets the central widget that will be used by this popup. :param widget | <QWidget> || None """ self._scrollArea.takeWidget() self._scrollArea.setWidget(widget) self.adjustSize() def setCurrentMode(self, mode): """ Sets the current mode for this dialog to the inputed mode. :param mode | <XPopupWidget.Mode> """ if (self._currentMode == mode): return self._currentMode = mode self.updateModeSettings() @Slot() def setDialogMode(self): """ Sets the current mode value to Dialog. """ self.setCurrentMode(XPopupWidget.Mode.Dialog) @deprecatedmethod('XPopupWidget', 'Direction is no longer used, use setAnchor instead') def setDirection(self, direction): """ Sets the direction for this widget to the inputed direction. :param direction | <XPopupWidget.Direction> """ if (direction == XPopupWidget.Direction.North): self.setAnchor(XPopupWidget.Anchor.TopCenter) elif (direction == XPopupWidget.Direction.South): self.setAnchor(XPopupWidget.Anchor.BottomCenter) elif (direction == XPopupWidget.Direction.East): self.setAnchor(XPopupWidget.Anchor.LeftCenter) else: self.setAnchor(XPopupWidget.Anchor.RightCenter) def setPalette(self, palette): """ Sets the palette for this widget and the scroll area. :param palette | <QPalette> """ super(XPopupWidget, self).setPalette(palette) self._scrollArea.setPalette(palette) def setPopupMode(self): """ Sets the current mode value to Popup. """ self.setCurrentMode(XPopupWidget.Mode.Popup) def setPopupPadding(self, padding): """ Sets the amount to pad the popup area when displaying this widget. :param padding | <int> """ self._popupPadding = padding self.adjustContentsMargins() def setPossibleAnchors(self, anchors): self._possibleAnchors = anchors def setPositionLinkedTo(self, widgets): """ Sets the widget that this popup will be linked to for positional changes. :param widgets | <QWidget> || [<QWidget>, ..] """ if type(widgets) in (list, set, tuple): new_widgets = list(widgets) else: new_widgets = [] widget = widgets while widget: widget.installEventFilter(self) new_widgets.append(widget) widget = widget.parent() self._positionLinkedTo = new_widgets def setResizable(self, state): self._resizable = state self._sizeGrip.setVisible(state) self._leftSizeGrip.setVisible(state) def setShowButtonBox(self, state): self._buttonBoxVisible = state self.buttonBox().setVisible(state) def setShowTitleBar(self, state): self._titleBarVisible = state self._dialogButton.setVisible(state) self._closeButton.setVisible(state) def setToolTipMode(self): """ Sets the mode for this popup widget to ToolTip """ self.setCurrentMode(XPopupWidget.Mode.ToolTip) def setVisible(self, state): super(XPopupWidget, self).setVisible(state) widget = self.centralWidget() if widget: widget.setVisible(state) def timerEvent(self, event): """ When the timer finishes, hide the tooltip popup widget. :param event | <QEvent> """ if self.currentMode() == XPopupWidget.Mode.ToolTip: self.killTimer(event.timerId()) event.accept() self.close() else: super(XPopupWidget, self).timerEvent(event) def updateModeSettings(self): mode = self.currentMode() is_visible = self.isVisible() # display as a floating dialog if mode == XPopupWidget.Mode.Dialog: self.setWindowFlags(Qt.Dialog | Qt.Tool) self.setAttribute(Qt.WA_TransparentForMouseEvents, False) self._closeButton.setVisible(False) self._dialogButton.setVisible(False) # display as a user tooltip elif mode == XPopupWidget.Mode.ToolTip: flags = Qt.Popup | Qt.FramelessWindowHint self.setWindowFlags(flags) self.setBackgroundRole(QPalette.Window) self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setShowTitleBar(False) self.setShowButtonBox(False) self.setFocusPolicy(Qt.NoFocus) # hide the scrollbars policy = Qt.ScrollBarAlwaysOff self._scrollArea.setVerticalScrollBarPolicy(policy) self._scrollArea.setHorizontalScrollBarPolicy(policy) # display as a popup widget else: flags = Qt.Popup | Qt.FramelessWindowHint if not self.autoCloseOnFocusOut(): flags |= Qt.Tool self.setWindowFlags(flags) self._closeButton.setVisible(self._titleBarVisible) self._dialogButton.setVisible(self._titleBarVisible) self.setBackgroundRole(QPalette.Window) self.adjustContentsMargins() if (is_visible): self.show() @staticmethod @deprecatedmethod('XPopupWidget', 'This method no longer has an effect as we are not '\ 'storing references to the tooltip.') def hideToolTip(key=None): """ Hides any existing tooltip popup widgets. :warning This method is deprecated! """ pass @staticmethod def showToolTip(text, point=None, anchor=None, parent=None, background=None, foreground=None, key=None, seconds=5): """ Displays a popup widget as a tooltip bubble. :param text | <str> point | <QPoint> || None anchor | <XPopupWidget.Mode.Anchor> || None parent | <QWidget> || None background | <QColor> || None foreground | <QColor> || None key | <str> || None seconds | <int> """ if point is None: point = QCursor.pos() if parent is None: parent = QApplication.activeWindow() if anchor is None and parent is None: anchor = XPopupWidget.Anchor.TopCenter # create a new tooltip widget widget = XPopupWidget(parent) widget.setToolTipMode() widget.setResizable(False) # create the tooltip label label = QLabel(text, widget) label.setOpenExternalLinks(True) label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) label.setMargin(3) label.setIndent(3) label.adjustSize() widget.setCentralWidget(label) # update the tip label.adjustSize() widget.adjustSize() palette = widget.palette() if not background: background = palette.color(palette.ToolTipBase) if not foreground: foreground = palette.color(palette.ToolTipText) palette.setColor(palette.Window, QColor(background)) palette.setColor(palette.WindowText, QColor(foreground)) widget.setPalette(palette) widget.centralWidget().setPalette(palette) if anchor is None: widget.setAutoCalculateAnchor(True) else: widget.setAnchor(anchor) widget.setAutoCloseOnFocusOut(True) widget.setAttribute(Qt.WA_DeleteOnClose) widget.popup(point) widget.startTimer(1000 * seconds) return widget
class XViewProfileManager(QWidget): currentProfileChanged = Signal(PyObject) optionsMenuRequested = Signal(QPoint) def __init__(self, parent=None): super(XViewProfileManager, self).__init__(parent) # define custom properties self._profiles = [] self._optionsMenuPolicy = Qt.DefaultContextMenu self._viewWidget = None # define the interface self._profileCombo = QComboBox(self) self._optionsButton = QToolButton(self) self._optionsButton.setAutoRaise(True) self._optionsButton.setToolTip('Advanced Options') self._optionsButton.setIcon(QIcon(resources.find('img/advanced.png'))) layout = QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._profileCombo) layout.addWidget(self._optionsButton) self.setLayout(layout) # create connections self._profileCombo.currentIndexChanged.connect(self.handleProfileChange) self._optionsButton.clicked.connect(self.showOptionsMenu) def addProfile(self, profile): """ Adds the inputed profile to the system. :param profile | <XViewProfile> """ if ( profile in self._profiles ): return self._profiles.append(profile) self._profileCombo.blockSignals(True) self._profileCombo.addItem(profile.name()) self._profileCombo.setCurrentIndex(self._profileCombo.count()-1) self._profileCombo.blockSignals(False) def currentProfile(self): """ Returns the currently selected profile from the system. :return <XViewProfile> """ index = self._profileCombo.currentIndex() if 0 <= index and index < len(self._profiles): return self._profiles[index] return None def handleProfileChange(self): """ Emits that the current profile has changed. """ # restore the profile settings prof = self.currentProfile() vwidget = self.viewWidget() if vwidget: prof.restore(vwidget) if not self.signalsBlocked(): self.currentProfileChanged.emit(self.currentProfile()) def optionsMenuPolicy(self): """ Returns the option menu policy for this widget. :return <Qt.MenuPolicy> """ return self._optionsMenuPolicy def profiles(self): """ Returns a list of all the profiles for this system. :return [<XViewProfile>, ..] """ return self._profiles def removeProfile(self, profile): """ Adds the inputed profile to the system. :param profile | <XViewProfile> """ if not profile in self._profiles: return index = self._profiles.index(profile) self._profiles.remove(profile) self._profileCombo.blockSignals(True) self._profileCombo.takeItem(index) self._profileCombo.blockSignals(False) def restoreSettings(self, settings): """ Restores settings from the application. :param settings | <QSettings> """ settings.beginGroup(self.objectName()) curr_prof = None curr_name = unwrapVariant(settings.value('current')) profiles = [] for prof_name in settings.childGroups(): settings.beginGroup(prof_name) prof_str = unwrapVariant(settings.value('profile')) profile = XViewProfile.fromString(prof_str) profile.setName(prof_name) if prof_name == curr_name: curr_prof = profile profiles.append(profile) settings.endGroup() self.blockSignals(True) self._profileCombo.blockSignals(True) self.setProfiles(profiles) if curr_prof: self.setCurrentProfile(curr_prof) self._profileCombo.blockSignals(False) self.blockSignals(False) settings.endGroup() def saveSettings(self, settings): """ Saves the settings for this widget to the application :param settings | <QSettings> """ settings.beginGroup(self.objectName()) curr_prof = self.currentProfile() if curr_prof: settings.setValue('current', curr_prof.name()) for profile in self.profiles(): settings.beginGroup(profile.name()) settings.setValue('profile', wrapVariant(profile.toString())) settings.endGroup() settings.endGroup() def setCurrentProfile(self, profile): """ Sets the current profile to the inputed profile. :param profile | <XViewProfile> """ try: index = self._profiles.index(profile) except ValueError: index = -1 self._profileCombo.setCurrentIndex(index) def setOptionsMenuPolicy(self, menuPolicy): """ Sets the options menu policy for this item. :param menuPolicy | <Qt.MenuPolicy> """ self._optionsMenuPolicy = menuPolicy def setProfiles(self, profiles): """ Sets a list of profiles to be the options for the manager. :param profiles | [<XViewProfile>, ..] """ self.blockSignals(True) self._profileCombo.blockSignals(True) self._profiles = profiles[:] self._profileCombo.clear() self._profileCombo.addItems(map(lambda x: x.name(), profiles)) self._profileCombo.blockSignals(False) self.blockSignals(False) def setViewWidget(self, viewWidget): """ Sets the view widget instance linked with this manager. :param viewWidget | <XViewWidget> """ self._viewWidget = viewWidget def showOptionsMenu(self): """ Displays the options menu. If the option menu policy is set to CustomContextMenu, then the optionMenuRequested signal will be emitted, otherwise the default context menu will be displayed. """ point = QPoint(0, self._optionsButton.height()) global_point = self._optionsButton.mapToGlobal(point) # prompt the custom context menu if self.optionsMenuPolicy() == Qt.CustomContextMenu: if not self.signalsBlocked(): self.optionsMenuRequested.emit(global_point) return # use the default context menu menu = XViewProfileManagerMenu(self) menu.exec_(global_point) def viewWidget(self): """ Returns the view widget that is associated with this manager. :return <XViewWidget> """ return self._viewWidget
class XUrlWidget(QWidget): urlChanged = Signal(str) urlEdited = Signal() def __init__(self, parent): super(XUrlWidget, self).__init__(parent) # define the interface self._urlEdit = XLineEdit(self) self._urlButton = QToolButton(self) self._urlButton.setAutoRaise(True) self._urlButton.setIcon(QIcon(resources.find('img/web.png'))) self._urlButton.setToolTip('Browse Link') self._urlButton.setFocusPolicy(Qt.NoFocus) self._urlEdit.setHint('http://') layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._urlEdit) layout.addWidget(self._urlButton) self.setLayout(layout) self.setFocusPolicy(Qt.StrongFocus) # create connections self._urlEdit.textChanged.connect(self.urlChanged) self._urlEdit.textEdited.connect(self.urlEdited) self._urlButton.clicked.connect(self.browse) def blockSignals(self, state): """ Blocks the signals for this widget and its sub-parts. :param state | <bool> """ super(XUrlWidget, self).blockSignals(state) self._urlEdit.blockSignals(state) self._urlButton.blockSignals(state) def browse(self): """ Brings up a web browser with the address in a Google map. """ webbrowser.open(self.url()) def hint(self): """ Returns the hint associated with this widget. :return <str> """ return self._urlEdit.hint() def lineEdit(self): """ Returns the line edit linked with this widget. :return <XLineEdit> """ return self._urlEdit def setFocus(self): """ Sets the focus for this widget on its line edit. """ self._urlEdit.setFocus() @Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._urlEdit.setHint(hint) @Slot(str) def setUrl(self, url): """ Sets the url for this widget to the inputed url. :param url | <str> """ self._urlEdit.setText(nativestring(url)) def url(self): """ Returns the current url from the edit. :return <str> """ return nativestring(self._urlEdit.text()) x_hint = Property(str, hint, setHint) x_url = Property(str, url, setUrl)
class XCommentEdit(XTextEdit): attachmentRequested = Signal() def __init__(self, parent=None): super(XCommentEdit, self).__init__(parent) # define custom properties self._attachments = {} self._showAttachments = True # create toolbar self._toolbar = QToolBar(self) self._toolbar.setMovable(False) self._toolbar.setFixedHeight(30) self._toolbar.setAutoFillBackground(True) self._toolbar.setFocusProxy(self) self._toolbar.hide() # create toolbar buttons self._attachButton = QToolButton(self) self._attachButton.setIcon(QIcon(resources.find('img/attach.png'))) self._attachButton.setToolTip('Add Attachment') self._attachButton.setAutoRaise(True) self._attachButton.setIconSize(QSize(24, 24)) self._attachButton.setFixedSize(26, 26) self._submitButton = QPushButton(self) self._submitButton.setText('Submit') self._submitButton.setFocusProxy(self) # create attachments widget self._attachmentsEdit = XMultiTagEdit(self) self._attachmentsEdit.setAutoResizeToContents(True) self._attachmentsEdit.setFrameShape(XMultiTagEdit.NoFrame) self._attachmentsEdit.setViewMode(XMultiTagEdit.ListMode) self._attachmentsEdit.setEditable(False) self._attachmentsEdit.setFocusProxy(self) self._attachmentsEdit.hide() # define toolbar layout spacer = QWidget(self) spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self._attachAction = self._toolbar.addWidget(self._attachButton) self._toolbar.addWidget(spacer) self._toolbar.addWidget(self._submitButton) # set standard properties self.setAutoResizeToContents(True) self.setHint('add comment') self.setFocusPolicy(Qt.StrongFocus) self.setRequireShiftForNewLine(True) # create connections self._attachButton.clicked.connect(self.attachmentRequested) self._submitButton.clicked.connect(self.acceptText) self._attachmentsEdit.tagRemoved.connect(self.removeAttachment) self.focusChanged.connect(self.setToolbarVisible) def addAttachment(self, title, attachment): """ Adds an attachment to this comment. :param title | <str> attachment | <variant> """ self._attachments[title] = attachment self.resizeToContents() def attachments(self): """ Returns a list of attachments that have been linked to this widget. :return {<str> title: <variant> attachment, ..} """ return self._attachments.copy() def attachButton(self): """ Returns the attach button from the toolbar for this widget. :return <QToolButton> """ return self._attachButton @Slot() def clear(self): """ Clears out this widget and its attachments. """ # clear the attachment list self._attachments.clear() super(XCommentEdit, self).clear() def isToolbarVisible(self): """ Returns whether or not the toolbar for this comment edit is currently visible to the user. :return <bool> """ return self._toolbar.isVisible() def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.clear() event.accept() else: super(XCommentEdit, self).keyPressEvent(event) @Slot() def pickAttachment(self): """ Prompts the user to select an attachment to add to this edit. """ filename = QFileDialog.getOpenFileName(self.window(), 'Select Attachment', '', 'All Files (*.*)') if type(filename) == tuple: filename = nativestring(filename[0]) filename = nativestring(filename) if filename: self.addAttachment(os.path.basename(filename), filename) def removeAttachment(self, title): """ Removes the attachment from the given title. :param title | <str> :return <variant> | attachment """ attachment = self._attachments.pop(nativestring(title), None) if attachment: self.resizeToContents() return attachment def resizeEvent(self, event): super(XCommentEdit, self).resizeEvent(event) self._toolbar.resize(self.width() - 4, 30) edit = self._attachmentsEdit edit.resize(self.width() - 4, edit.height()) def resizeToContents(self): """ Resizes this toolbar based on the contents of its text. """ if self._toolbar.isVisible(): doc = self.document() h = doc.documentLayout().documentSize().height() offset = 34 # update the attachments edit edit = self._attachmentsEdit if self._attachments: edit.move(2, self.height() - edit.height() - 31) edit.setTags(sorted(self._attachments.keys())) edit.show() offset = 34 + edit.height() else: edit.hide() offset = 34 self.setFixedHeight(h + offset) self._toolbar.move(2, self.height() - 32) else: super(XCommentEdit, self).resizeToContents() def setAttachments(self, attachments): """ Sets the attachments for this widget to the inputed list of attachments. :param attachments | {<str> title: <variant> attachment, ..} """ self._attachments = attachments self.resizeToContents() def setSubmitText(self, text): """ Sets the submission text for this edit. :param text | <str> """ self._submitButton.setText(text) def setShowAttachments(self, state): """ Sets whether or not to show the attachments for this edit. :param state | <bool> """ self._showAttachments = state self._attachAction.setVisible(state) def setToolbarVisible(self, state): """ Sets whether or not the toolbar is visible. :param state | <bool> """ self._toolbar.setVisible(state) self.resizeToContents() def showAttachments(self): """ Returns whether or not to show the attachments for this edit. :return <bool> """ return self._showAttachments def submitButton(self): """ Returns the submit button for this edit. :return <QPushButton> """ return self._submitButton def submitText(self): """ Returns the submission text for this edit. :return <str> """ return self._submitButton.text() def toolbar(self): """ Returns the toolbar widget for this comment edit. :return <QToolBar> """ return self._toolbar x_showAttachments = Property(bool, showAttachments, setShowAttachments) x_submitText = Property(str, submitText, setSubmitText)
class XUrlWidget(QWidget): urlChanged = Signal(str) urlEdited = Signal() def __init__( self, parent ): super(XUrlWidget, self).__init__(parent) # define the interface self._urlEdit = XLineEdit(self) self._urlButton = QToolButton(self) self._urlButton.setAutoRaise(True) self._urlButton.setIcon(QIcon(resources.find('img/web.png'))) self._urlButton.setToolTip('Browse Link') self._urlButton.setFocusPolicy(Qt.NoFocus) self._urlEdit.setHint('http://') layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._urlEdit) layout.addWidget(self._urlButton) self.setLayout(layout) self.setFocusPolicy(Qt.StrongFocus) # create connections self._urlEdit.textChanged.connect(self.urlChanged) self._urlEdit.textEdited.connect(self.urlEdited) self._urlButton.clicked.connect(self.browse) def blockSignals( self, state ): """ Blocks the signals for this widget and its sub-parts. :param state | <bool> """ super(XUrlWidget, self).blockSignals(state) self._urlEdit.blockSignals(state) self._urlButton.blockSignals(state) def browse( self ): """ Brings up a web browser with the address in a Google map. """ webbrowser.open(self.url()) def hint( self ): """ Returns the hint associated with this widget. :return <str> """ return self._urlEdit.hint() def lineEdit( self ): """ Returns the line edit linked with this widget. :return <XLineEdit> """ return self._urlEdit def setFocus(self): """ Sets the focus for this widget on its line edit. """ self._urlEdit.setFocus() @Slot(str) def setHint( self, hint ): """ Sets the hint associated with this widget. :param hint | <str> """ self._urlEdit.setHint(hint) @Slot(str) def setUrl( self, url ): """ Sets the url for this widget to the inputed url. :param url | <str> """ self._urlEdit.setText(nativestring(url)) def url( self ): """ Returns the current url from the edit. :return <str> """ return nativestring(self._urlEdit.text()) x_hint = Property(str, hint, setHint) x_url = Property(str, url, setUrl)
class XZoomSlider(QWidget): zoomAmountChanged = Signal(int) def __init__(self, parent=None): super(XZoomSlider, self).__init__(parent) # define the interface in_icon = projexui.resources.find('img/zoom_in.png') out_icon = projexui.resources.find('img/zoom_out.png') self._zoomInButton = QToolButton(self) self._zoomInButton.setAutoRaise(True) self._zoomInButton.setToolTip('Zoom In') self._zoomInButton.setIcon(QIcon(in_icon)) self._zoomOutButton = QToolButton(self) self._zoomOutButton.setAutoRaise(True) self._zoomOutButton.setToolTip('Zoom Out') self._zoomOutButton.setIcon(QIcon(out_icon)) self._zoomSlider = QSlider(Qt.Horizontal, self) self._zoomSlider.setRange(10, 100) self._zoomSlider.setValue(100) # define the layout layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._zoomOutButton) layout.addWidget(self._zoomSlider) layout.addWidget(self._zoomInButton) self.setLayout(layout) # create connections self._zoomSlider.valueChanged.connect(self.emitZoomAmountChanged) self._zoomInButton.clicked.connect(self.zoomIn) self._zoomOutButton.clicked.connect(self.zoomOut) def emitZoomAmountChanged(self): """ Emits the current zoom amount, provided the signals are not being blocked. """ if not self.signalsBlocked(): self.zoomAmountChanged.emit(self.zoomAmount()) def maximum(self): """ Returns the maximum zoom level for this widget. :return <int> """ return self._zoomSlider.maximum() def minimum(self): """ Returns the minimum zoom level for this widget. :return <int> """ return self._zoomSlider.minimum() @Slot(int) def setMaximum(self, maximum): """ Sets the maximum zoom level for this widget. :param maximum | <int> """ self._zoomSlider.setMaximum(maximum) @Slot(int) def setMinimum(self, minimum): """ Sets the minimum zoom level for this widget. :param minimum | <int> """ self._zoomSlider.setMinimum(minimum) def setZoomStep(self, amount): """ Sets how much a single step for the zoom in/out will be. :param amount | <int> """ self._zoomSlider.setPageStep(amount) @Slot(int) def setZoomAmount(self, amount): """ Sets the zoom amount for this widget to the inputed amount. :param amount | <int> """ self._zoomSlider.setValue(amount) def zoomAmount(self): """ Returns the current zoom amount for this widget. :return <int> """ return self._zoomSlider.value() @Slot() def zoomIn(self): """ Zooms in by a single page step. """ self._zoomSlider.triggerAction(QSlider.SliderPageStepAdd) def zoomInButton(self): """ Returns the zoom in button from the left side of this widget. :return <QToolButton> """ return self._zoomInButton @Slot() def zoomOut(self): """ Zooms out by a single page step. """ self._zoomSlider.triggerAction(QSlider.SliderPageStepSub) def zoomOutButton(self): """ Returns the zoom out button from the right side of this widget. :return <QToolButton> """ return self._zoomOutButton def zoomSlider(self): """ Returns the slider widget of this zoom slider. :return <QSlider> """ return self._zoomSlider def zoomStep(self): """ Returns the amount for a single step when the user clicks the zoom in/ out amount. :return <int> """ return self._zoomSlider.pageStep() x_maximum = Property(int, maximum, setMaximum) x_minimum = Property(int, minimum, setMinimum) z_zoomStep = Property(int, zoomStep, setZoomStep)
class XViewProfileManager(QWidget): currentProfileChanged = Signal(PyObject) optionsMenuRequested = Signal(QPoint) def __init__(self, parent=None): super(XViewProfileManager, self).__init__(parent) # define custom properties self._profiles = [] self._optionsMenuPolicy = Qt.DefaultContextMenu self._viewWidget = None # define the interface self._profileCombo = QComboBox(self) self._optionsButton = QToolButton(self) self._optionsButton.setAutoRaise(True) self._optionsButton.setToolTip('Advanced Options') self._optionsButton.setIcon(QIcon(resources.find('img/advanced.png'))) layout = QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._profileCombo) layout.addWidget(self._optionsButton) self.setLayout(layout) # create connections self._profileCombo.currentIndexChanged.connect( self.handleProfileChange) self._optionsButton.clicked.connect(self.showOptionsMenu) def addProfile(self, profile): """ Adds the inputed profile to the system. :param profile | <XViewProfile> """ if (profile in self._profiles): return self._profiles.append(profile) self._profileCombo.blockSignals(True) self._profileCombo.addItem(profile.name()) self._profileCombo.setCurrentIndex(self._profileCombo.count() - 1) self._profileCombo.blockSignals(False) def currentProfile(self): """ Returns the currently selected profile from the system. :return <XViewProfile> """ index = self._profileCombo.currentIndex() if 0 <= index and index < len(self._profiles): return self._profiles[index] return None def handleProfileChange(self): """ Emits that the current profile has changed. """ # restore the profile settings prof = self.currentProfile() vwidget = self.viewWidget() if vwidget: prof.restore(vwidget) if not self.signalsBlocked(): self.currentProfileChanged.emit(self.currentProfile()) def optionsMenuPolicy(self): """ Returns the option menu policy for this widget. :return <Qt.MenuPolicy> """ return self._optionsMenuPolicy def profiles(self): """ Returns a list of all the profiles for this system. :return [<XViewProfile>, ..] """ return self._profiles def removeProfile(self, profile): """ Adds the inputed profile to the system. :param profile | <XViewProfile> """ if not profile in self._profiles: return index = self._profiles.index(profile) self._profiles.remove(profile) self._profileCombo.blockSignals(True) self._profileCombo.takeItem(index) self._profileCombo.blockSignals(False) def restoreSettings(self, settings): """ Restores settings from the application. :param settings | <QSettings> """ settings.beginGroup(self.objectName()) curr_prof = None curr_name = unwrapVariant(settings.value('current')) profiles = [] for prof_name in settings.childGroups(): settings.beginGroup(prof_name) prof_str = unwrapVariant(settings.value('profile')) profile = XViewProfile.fromString(prof_str) profile.setName(prof_name) if prof_name == curr_name: curr_prof = profile profiles.append(profile) settings.endGroup() self.blockSignals(True) self._profileCombo.blockSignals(True) self.setProfiles(profiles) if curr_prof: self.setCurrentProfile(curr_prof) self._profileCombo.blockSignals(False) self.blockSignals(False) settings.endGroup() def saveSettings(self, settings): """ Saves the settings for this widget to the application :param settings | <QSettings> """ settings.beginGroup(self.objectName()) curr_prof = self.currentProfile() if curr_prof: settings.setValue('current', curr_prof.name()) for profile in self.profiles(): settings.beginGroup(profile.name()) settings.setValue('profile', wrapVariant(profile.toString())) settings.endGroup() settings.endGroup() def setCurrentProfile(self, profile): """ Sets the current profile to the inputed profile. :param profile | <XViewProfile> """ try: index = self._profiles.index(profile) except ValueError: index = -1 self._profileCombo.setCurrentIndex(index) def setOptionsMenuPolicy(self, menuPolicy): """ Sets the options menu policy for this item. :param menuPolicy | <Qt.MenuPolicy> """ self._optionsMenuPolicy = menuPolicy def setProfiles(self, profiles): """ Sets a list of profiles to be the options for the manager. :param profiles | [<XViewProfile>, ..] """ self.blockSignals(True) self._profileCombo.blockSignals(True) self._profiles = profiles[:] self._profileCombo.clear() self._profileCombo.addItems(map(lambda x: x.name(), profiles)) self._profileCombo.blockSignals(False) self.blockSignals(False) def setViewWidget(self, viewWidget): """ Sets the view widget instance linked with this manager. :param viewWidget | <XViewWidget> """ self._viewWidget = viewWidget def showOptionsMenu(self): """ Displays the options menu. If the option menu policy is set to CustomContextMenu, then the optionMenuRequested signal will be emitted, otherwise the default context menu will be displayed. """ point = QPoint(0, self._optionsButton.height()) global_point = self._optionsButton.mapToGlobal(point) # prompt the custom context menu if self.optionsMenuPolicy() == Qt.CustomContextMenu: if not self.signalsBlocked(): self.optionsMenuRequested.emit(global_point) return # use the default context menu menu = XViewProfileManagerMenu(self) menu.exec_(global_point) def viewWidget(self): """ Returns the view widget that is associated with this manager. :return <XViewWidget> """ return self._viewWidget