class XTabBar(QTabBar): resized = qt.Signal() def resizeEvent(self, event): """ Updates the position of the additional buttons when this widget \ resizes. :param event | <QResizeEvet> """ super(XTabBar, self).resizeEvent(event) self.resized.emit()
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 = qt.Signal() accepted = qt.Signal() closed = qt.Signal() rejected = qt.Signal() resetRequested = qt.Signal() shown = qt.Signal() buttonClicked = qt.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 = [] # define controls 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.Button) 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')) painter = QPainter() painter.begin(bitmap) painter.setRenderHint(QPainter.Antialiasing) pen = QPen(QColor('black')) pen.setWidthF(0.75) painter.setPen(pen) painter.setBrush(QColor('black')) painter.drawPath(path) painter.end() 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) @qt.Slot() def accept(self): """ Emits the accepted signal and closes the popup. """ 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 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> """ 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 @qt.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 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, globalPos): """ Returns the anchor point that best fits within the given widget from the inputed global position. :param widget | <QWidget> globalPos | <QPoint> :return <XPopupWidget.Anchor> """ localPos = widget.mapFromGlobal(globalPos) x = localPos.x() y = localPos.y() w = widget.width() h = widget.height() cw = self.width() / 2 ch = self.height() / 2 # by default, try to do a center point, so make sure the center point # is at least 1/2 the width longer from each edge if x < cw and h - y < ch: return XPopupWidget.Anchor.BottomLeft elif x < cw: return XPopupWidget.Anchor.TopLeft elif w - x < cw and h - y < ch: return XPopupWidget.Anchor.BottomRight elif w - x < cw: return XPopupWidget.Anchor.TopRight elif h - y < ch: return XPopupWidget.Anchor.BottomCenter else: return XPopupWidget.Anchor.TopCenter 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() painter = QPainter() painter.begin(self) 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()) painter.end() def popupPadding(self): """ Returns the amount of pixels to pad the popup arrow for this widget. :return <int> """ return self._popupPadding def positionLinkedTo(self): """ Returns the widget that this popup is linked to for positional changes. :return [<QWidget>, ..] """ return self._positionLinkedTo @qt.Slot() def reject(self): """ Emits the accepted signal and closes the popup. """ if not self.signalsBlocked(): self.rejected.emit() if self.autoCloseOnReject(): self.close() 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): """ 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() @qt.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 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.Button) 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 XGanttWidget(QWidget): dateRangeChanged = qt.Signal() Timescale = enum('Week', 'Month', 'Year') def __init__(self, parent=None, _availabilityEnabled=0): super(XGanttWidget, self).__init__(parent) ''' menubar = QMenuBar(self) #menubar.sizeHint(QSize.setHeight(10)) fileMenu = menubar.addMenu('&File') fileMenu.addAction('Create Project') fileMenu.addSeparator() fileMenu.addAction('Save') fileMenu.addSeparator() fileMenu.addAction('Exit') fileMenu.triggered.connect( self.fileMenuActions ) ''' # load the user interface if getattr(sys, 'frozen', None): #print (sys._MEIPASS+"/ui/xganttwidget.ui"); projexui.loadUi(sys._MEIPASS, self, uifile=(sys._MEIPASS + "/ui/xganttwidget.ui")) else: projexui.loadUi(__file__, self) # define custom properties self._backend = None self._dateStart = QDate.currentDate() self._dateEnd = QDate.currentDate().addMonths(12) self._alternatingRowColors = False self._cellWidth = 15 self._cellHeight = 15 self._first = True self._dateFormat = 'M/d/yy' self._timescale = XGanttWidget.Timescale.Year self._scrolling = False # setup the palette colors palette = self.palette() color = palette.color(palette.Base) self._gridPen = QPen(color.darker(135)) self._brush = QBrush(color) self._alternateBrush = QBrush(color.darker(105)) self._currentDayBrush = QBrush(QColor(146, 252, 186)) self._holidayBrush = QBrush(QColor(166, 46, 46)) self._bookedBrush = QBrush(QColor(20, 250, 0)) self._unavailableBrush = QBrush(QColor(75, 75, 75)) self._underbookedBrush = QBrush(QColor(255, 255, 20)) self._overbookedBrush = QBrush(QColor(255, 25, 25)) self._overbookedAmount = {} self._unassignedBrush = QBrush(QColor(25, 25, 255)) weekendColor = color.darker(148) self._availabilityEnabled = _availabilityEnabled self._weekendBrush = QBrush(weekendColor) # setup the columns for the tree if _availabilityEnabled: self.setColumns(['Name']) else: self.setColumns( ['Name', 'Start', 'End', 'Calendar Days', 'Work Days']) header = self.uiGanttTREE.header() header.setFixedHeight(self._cellHeight * 2) header.setResizeMode(0, header.ResizeToContents) header.setDefaultSectionSize(60) headerItem = self.uiGanttTREE.headerItem() headerItem.setSizeHint(0, QSize(10, header.height())) self.uiGanttTREE.setContextMenuPolicy(Qt.CustomContextMenu) # connect signals self.uiGanttTREE.customContextMenuRequested.connect( self.showProjectMenu) # initialize the tree widget self.uiGanttTREE.setShowGrid(False) #enable drag and drop self.uiGanttTREE.setDragDropFilter( self.uiGanttTREE.setDragDropFilter(XGanttWidget.handleDragDrop)) if (sharedDB.currentUser._idPrivileges == 3): self.uiGanttTREE.setEditable(False) for act in sharedDB.mainWindow._fileMenu.actions(): if act.text() == "Save" or act.text() == "Create Project": act.setEnabled(False) else: self.uiGanttTREE.setEditable(True) self.uiGanttTREE.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.uiGanttTREE.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.uiGanttTREE.setVerticalScrollMode(self.uiGanttTREE.ScrollPerPixel) #left half size self.uiGanttTREE.resize(400, 20) # initialize the view widget #self.uiGanttVIEW.setDragMode( self.uiGanttVIEW.RubberBandDrag ) self.uiGanttVIEW.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.uiGanttVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiGanttVIEW.setScene(XGanttScene(self)) self.uiGanttVIEW.installEventFilter(self) #self.uiGanttVIEW.horizontalScrollBar().setValue(50) # create connections self.uiGanttTREE.itemExpanded.connect(self.syncView) self.uiGanttTREE.itemCollapsed.connect(self.syncView) # connect scrollbars tree_bar = self.uiGanttTREE.verticalScrollBar() view_bar = self.uiGanttVIEW.verticalScrollBar() tree_bar.rangeChanged.connect(self.__updateViewRect) tree_bar.valueChanged.connect(self.__scrollView) view_bar.valueChanged.connect(self.__scrollTree) # connect selection self.uiGanttTREE.itemSelectionChanged.connect(self.__selectView) self.uiGanttVIEW.scene().selectionChanged.connect(self.__selectTree) self.uiGanttTREE.itemChanged.connect(self.updateItemData) if self._availabilityEnabled: self._currentDayBrush = None self._holidayBrush = QBrush(QColor(75, 75, 75)) weekendColor = QBrush(QColor(75, 75, 75)) self.uiGanttTREE.setEditable(False) #self._cellHeight = 12 def __del__(self): self.uiGanttVIEW.scene().selectionChanged.disconnect(self.__selectTree) def __scrollTree(self, value): """ Updates the tree view scrolling to the inputed value. :param value | <int> """ if (self._scrolling): return tree_bar = self.uiGanttTREE.verticalScrollBar() self._scrolling = True tree_bar.setValue(value) self._scrolling = False def __scrollView(self, value): """ Updates the gantt view scrolling to the inputed value. :param value | <int> """ if (self._scrolling): return view_bar = self.uiGanttVIEW.verticalScrollBar() self._scrolling = True view_bar.setValue(value) self._scrolling = False def __selectTree(self): """ Matches the tree selection to the views selection. """ self.uiGanttTREE.blockSignals(True) self.uiGanttTREE.clearSelection() for item in self.uiGanttVIEW.scene().selectedItems(): item.treeItem().setSelected(True) sharedDB.sel.select([item.treeItem()._dbEntry]) self.uiGanttTREE.blockSignals(False) def __selectView(self): """ Matches the view selection to the trees selection. """ self.uiGanttVIEW.scene().blockSignals(True) self.uiGanttVIEW.scene().clearSelection() for item in self.uiGanttTREE.selectedItems(): item.viewItem().setSelected(True) sharedDB.sel.select([item._dbEntry]) self.uiGanttVIEW.scene().blockSignals(False) def __updateViewRect(self): """ Updates the view rect to match the current tree value. """ header_h = self._cellHeight * 2 rect = self.uiGanttVIEW.scene().sceneRect() sbar_max = self.uiGanttTREE.verticalScrollBar().maximum() sbar_max += self.uiGanttTREE.viewport().height() + header_h widget_max = self.uiGanttVIEW.height() widget_max -= (self.uiGanttVIEW.horizontalScrollBar().height() + 10) rect.setHeight(max(widget_max, sbar_max)) self.uiGanttVIEW.scene().setSceneRect(rect) def addTopLevelItem(self, item): """ Adds the inputed item to the gantt widget. :param item | <XGanttWidgetItem> """ vitem = item.viewItem() self.treeWidget().addTopLevelItem(item) if not self._availabilityEnabled: self.viewWidget().scene().addItem(vitem) item._viewItem = weakref.ref(vitem) #set scrollbar offset #item.treeWidget._scrollBar = self.uiGanttTREE.verticalScrollBar() item.sync(recursive=True) def alternateBrush(self): """ Returns the alternate brush to be used for the grid view. :return <QBrush> """ return self._alternateBrush def alternatingRowColors(self): """ Returns whether or not this widget should show alternating row colors. :return <bool> """ return self._alternatingRowColors def brush(self): """ Returns the background brush to be used for the grid view. :return <QBrush> """ return self._brush def cellHeight(self): """ Returns the height for the cells in this gantt's views. :return <int> """ return self._cellHeight def cellWidth(self): """ Returns the width for the cells in this gantt's views. :return <int> """ return self._cellWidth def clear(self): """ Clears all the gantt widget items for this widget. """ self.uiGanttTREE.clear() self.uiGanttVIEW.scene().clear() def closeEvent(self, event): if sharedDB.changesToBeSaved and sharedDB.users.currentUser._idPrivileges != 3: quit_msg = "Save before exit?" reply = QtGui.QMessageBox.question(self, 'Message', quit_msg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No, QtGui.QMessageBox.Cancel) if reply == QtGui.QMessageBox.Yes: self.SaveToDatabase() event.accept() elif reply == QtGui.QMessageBox.No: event.accept() else: event.ignore() def columns(self): """ Returns a list of the columns being used in the treewidget of this gantt chart. :return [<str>, ..] """ return self.treeWidget().columns() '''def CreateProject(self): #self._myCreateProjectWidget = CreateProjectWidget() #self._myCreateProjectWidget.show() sharedDB.app.CreateProjectWidget() ''' def dateEnd(self): """ Returns the date end for this date range of this gantt widget. :return <QDate> """ return self._dateEnd def dateFormat(self): """ Returns the date format that will be used when rendering items in the view. :return <str> """ return self._dateFormat def dateStart(self): """ Returns the date start for the date range of this gantt widget. :return <QDate> """ return self._dateStart def emitDateRangeChanged(self): """ Emits the date range changed signal provided signals aren't being blocked. """ if (not self.signalsBlocked()): self.dateRangeChanged.emit() def collapseAllTrees(self): self.uiGanttTREE.blockSignals(True) #for x in range(0,self.treeWidget().topLevelItemCount()): #self.treeWidget().topLevelItem(x).setExpanded(True) self.treeWidget().collapseAll() self.uiGanttTREE.blockSignals(False) self.syncView() def expandAllTrees(self): self.uiGanttTREE.blockSignals(True) #for x in range(0,self.treeWidget().topLevelItemCount()): #self.treeWidget().topLevelItem(x).setExpanded(True) self.treeWidget().expandAll() self.uiGanttTREE.blockSignals(False) self.syncView() def eventFilter(self, object, event): if (event.type() == event.Resize): self.__updateViewRect() return False def frameCurrentDate(self): # Subtract start date from current date prerollDays = self._dateStart.day() - QDate.currentDate().day() #set scroll to multiply difference against cel width view_bar = self.uiGanttVIEW.horizontalScrollBar() self._scrolling = True view_bar.setMaximum(1) view_bar.setMinimum(0) #print view_bar.maximum() view_bar.setValue(1) #view_bar.update() #self.update() #self.uiGanttVIEW.update() self._scrolling = False #self.__scrollView(self._cellWidth * prerollDays) def gridPen(self): """ Returns the pen that this widget uses to draw in the view. :return <QPen> """ return self._gridPen @staticmethod def handleDragDrop(object, event): if (event.type() == QEvent.DragEnter): event.acceptProposedActions() elif (event.type() == QEvent.Drop): print 'dropping' def indexOfTopLevelItem(self, item): """ Returns the index for the inputed item from the tree. :return <int> """ return self.treeWidget().indexOfTopLevelItem(item) def insertTopLevelItem(self, index, item): """ Inserts the inputed item at the given index in the tree. :param index | <int> item | <XGanttWidgetItem> """ self.treeWidget().insertTopLevelItem(index, item) item.sync(recursive=True) def setAlternateBrush(self, brush): """ Sets the alternating brush used for this widget to the inputed brush. :param brush | <QBrush> || <QColor> """ self._alternateBrush = QBrush(brush) def setAlternatingRowColors(self, state): """ Sets the alternating row colors state for this widget. :param state | <bool> """ self._alternatingRowColors = state self.treeWidget().setAlternatingRowColors(state) def setBrush(self, brush): """ Sets the main background brush used for this widget to the inputed brush. :param brush | <QBrush> || <QColor> """ self._brush = QBrush(brush) def setCellHeight(self, cellHeight): """ Sets the height for the cells in this gantt's views. :param cellHeight | <int> """ self._cellHeight = cellHeight def setCellWidth(self, cellWidth): """ Sets the width for the cells in this gantt's views. :param cellWidth | <int> """ self._cellWidth = cellWidth def setColumns(self, columns): """ Sets the columns for this gantt widget's tree to the inputed list of columns. :param columns | {<str>, ..] """ self.treeWidget().setColumns(columns) item = self.treeWidget().headerItem() for i in range(item.columnCount()): item.setTextAlignment(i, Qt.AlignBottom | Qt.AlignHCenter) def setDateEnd(self, dateEnd): """ Sets the end date for the range of this gantt widget. :param dateEnd | <QDate> """ self._dateEnd = dateEnd self.emitDateRangeChanged() def setDateFormat(self, format): """ Sets the date format that will be used when rendering in the views. :return <str> """ return self._dateFormat def setDateStart(self, dateStart): """ Sets the start date for the range of this gantt widget. :param dateStart | <QDate> """ self._dateStart = dateStart self.emitDateRangeChanged() def setGridPen(self, pen): """ Sets the pen used to draw the grid lines for the view. :param pen | <QPen> || <QColor> """ self._gridPen = QPen(pen) def setTimescale(self, timescale): """ Sets the timescale value for this widget to the inputed value. :param timescale | <XGanttWidget.Timescale> """ self._timescale = timescale #def setupUserView(self, privileges, department): #sif department def setWeekendBrush(self, brush): """ Sets the brush to be used when coloring weekend columns. :param brush | <QBrush> || <QColor> """ self._weekendBrush = QBrush(brush) def syncView(self): """ Syncs all the items to the view. """ if (not self.signalsBlocked()): for i in range(self.topLevelItemCount()): item = self.topLevelItem(i) item.syncView(recursive=True) def takeTopLevelItem(self, index): """ Removes the top level item at the inputed index from the widget. :param index | <int> :return <XGanttWidgetItem> || None """ item = self.topLevelItem(index) if (item): self.viewWidget().scene().removeItem(item.viewItem()) self.treeWidget().takeTopLevelItem(index) return item return None def timescale(self): """ Returns the timescale that is being used for this widget. :return <XGanttWidget.Timescale> """ return self._timescale def topLevelItem(self, index): """ Returns the top level item at the inputed index. :return <XGanttWidgetItem> """ return self.treeWidget().topLevelItem(index) def topLevelItemCount(self): """ Returns the number of top level items for this widget. :return <int> """ return self.treeWidget().topLevelItemCount() def treeWidget(self): """ Returns the tree widget for this gantt widget. :return <QTreeWidget> """ return self.uiGanttTREE def updateItemData(self, item, index): """ Updates the item information from the tree. :param item | <XGanttWidgetItem> index | <int> """ value = qt.unwrapVariant(item.data(index, Qt.EditRole)) if type(value) == QDateTime: value = value.date() item.setData(index, Qt.EditRole, qt.wrapVariant(value)) if type(value) == QDate: value = value.toPython() columnName = self.treeWidget().columnOf(index) item.setProperty(columnName, value) item.sync() def updatePhaseVisibility(self, visibility, phaseName=''): if (phaseName != ''): #print ("Changing "+ phaseName + " to : "+ str(visibility)) #iterate through all projects for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) keepVisible = False for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) if (child._dbEntry._phase._name == phaseName): #projectWidgetItem.setHidden(not visibility) child.setHidden(not visibility) if (visibility): keepVisible = True elif (visibility == False and not child.isHidden() and keepVisible == False): keepVisible = True #print child._name #print ("item: " + str(x)) #iterate through all phases if (keepVisible): if (projectWidgetItem.isHidden()): projectWidgetItem.setHidden(False) elif (not visibility): if (not projectWidgetItem.isHidden()): #self.syncView() projectWidgetItem.setHidden(True) #if phase matches, change visibility for phase in sharedDB.myPhases.values(): if (phase._name == phaseName): phase._visible = visibility else: #iterate through all projects #iterate through all phases #change visibility if visibility == False: self.collapseAllTrees() for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) child.setHidden(not visibility) #self.syncView() projectWidgetItem.setHidden(not visibility) for phase in sharedDB.myPhases.values(): phase._visible = visibility #print ("Changing all phases to: "+ str(visibility)) self.syncView() def updateUserVisibility(self, visibility, username=''): if (username != ''): #print ("Changing "+ phaseName + " to : "+ str(visibility)) #iterate through all projects for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) keepProjectVisible = False for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) if (child._dbEntry.type() == "phaseassignment"): print "keeping " + username + " visible." if self.CheckUserAssignmentForUser( child._dbEntry._userAssignments.values(), username): child.setHidden(not visibility) if (visibility): keepProjectVisible = True if (visibility == False and not child.isHidden() and keepProjectVisible == False): keepProjectVisible = True if (keepProjectVisible): if (projectWidgetItem.isHidden()): projectWidgetItem.setHidden(False) elif (not visibility): if (not projectWidgetItem.isHidden()): #self.syncView() for c in range(projectWidgetItem.childCount()): projectWidgetItem.child(c).setHidden(True) projectWidgetItem.setHidden(True) #if phase matches, change visibility for user in sharedDB.myUsers.values(): if (user.name() == username): user._calendarVisibility = visibility else: #iterate through all projects #iterate through all phases #change visibility if visibility == False: self.collapseAllTrees() for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) child.setHidden(not visibility) #self.syncView() projectWidgetItem.setHidden(not visibility) for phase in sharedDB.myPhases.values(): phase._visible = visibility for user in sharedDB.myUsers.values(): user._calendarVisibility = visibility #print ("Changing all phases to: "+ str(visibility)) self.syncView() def CheckUserAssignmentForUser(self, userAssignments, username): for ua in userAssignments: if ua._hours > 1 and ua.idUsers() is not None: if (sharedDB.myUsers[str(ua.idUsers())].name() == username): return True return False def viewWidget(self): """ Returns the view widget for this gantt widget. :return <QGraphicsView> """ return self.uiGanttVIEW def weekendBrush(self): """ Returns the weekend brush to be used for coloring in weekends. :return <QBrush> """ return self._weekendBrush def bookedBrush(self): """ Returns the booked brush to be used for coloring in booked days. :return <QBrush> """ return self._bookedBrush def unavailableBrush(self): """ Returns the unavailable brush to be used for coloring in unavailable days. :return <QBrush> """ return self._unavailableBrush def underbookedBrush(self): """ Returns the underbookedBrush brush to be used for coloring in underbooked days. :return <QBrush> """ return self._underbookedBrush def overbookedBrush(self): """ Returns the overbookedBrush brush to be used for coloring in overbooked days. :return <QBrush> """ return self._overbookedBrush def unassignedBrush(self): """ Returns the unassignedBrush brush to be used for coloring in unassigned days. :return <QBrush> """ return self._unassignedBrush def showProjectMenu(self, pos): """ Displays the header menu for this tree widget. :param pos | <QPoint> || None """ ''' header = self.header() index = header.logicalIndexAt(pos) self._headerIndex = index # show a pre-set menu if self._headerMenu: menu = self._headerMenu else: menu = self.createHeaderMenu(index) ''' # determine the point to show the menu from #if pos is not None: # point = header.mapToGlobal(pos) #else: index = self.uiGanttTREE.indexAt(pos) typ = "None" if index.isValid(): dbentry = self.uiGanttTREE.itemFromIndex(index)._dbEntry if hasattr(dbentry, '_type'): typ = dbentry._type point = QCursor.pos() #self.headerMenuAboutToShow.emit(menu, index) menu = QtGui.QMenu() if typ == "phaseassignment": statusMenu = menu.addMenu("TEST") elif typ == "project": statusAction = menu.addAction("Open in Project View") statusAction.setData(dbentry.id()) menu.addSeparator() if sharedDB.currentUser._idPrivileges < 2: addPhaseMenu = menu.addMenu("Add Phase") phases = sharedDB.myPhases.values() phases.sort(key=operator.attrgetter('_name')) middleChar = phases[len(phases) / 2]._name[0] AMMenu = addPhaseMenu.addMenu('A - ' + middleChar) NZMenu = addPhaseMenu.addMenu( chr(ord(middleChar) + 1) + ' - Z') for x in range(0, len(phases)): phase = phases[x] if phase._name == "DUE": continue #col = self.column(column) if x < len(phases) / 2 or phase._name[0] == middleChar: action = AMMenu.addAction(phase._name) action.setData("addphase_" + str(phase.id()) + "_" + str(dbentry.id())) else: action = NZMenu.addAction(phase._name) action.setData("addphase_" + str(phase.id()) + "_" + str(dbentry.id())) ''' for phase in sharedDB.myPhases.values(): if phase._name != "DUE": addPhaseAction = addPhaseMenu.addAction(phase._name) addPhaseAction.setData("addphase_"+str(phase.id())+"_"+str(dbentry.id())) ''' if sharedDB.currentUser._idPrivileges == 1: archiveAction = menu.addAction("Archive Project") archiveAction.setData(dbentry.id()) else: if sharedDB.currentUser._idPrivileges < 2: menu.addAction("Create Project") menu.triggered.connect(self.mActions) menu.exec_(point) def mActions(self, action): act = action.text() if act == "Open in Project View": self.loadinprojectview(sharedDB.myProjects[str( action.data().toPyObject())]) #print sharedDB.myProjects[str(projectId)]._name elif act == "Archive Project": sharedDB.myProjects[str(action.data().toPyObject())].setArchived(1) elif act == "Create Project": if not hasattr(sharedDB, 'myCreateProjectWidget'): sharedDB.myCreateProjectWidget = createprojectwidget.CreateProjectWidget( sharedDB.mainWindow) sharedDB.myCreateProjectWidget.setDefaults() sharedDB.myCreateProjectWidget.dockWidget.show() elif "addphase" in str(action.data().toPyObject()): phaseId = str(action.data().toPyObject()).split("_")[1] proj = sharedDB.myProjects[str( action.data().toPyObject()).split("_")[2]] phase = sharedDB.phaseAssignments.PhaseAssignments( _idphases=phaseId, _startdate=proj._startdate, _enddate=proj._startdate, _updated=0) proj.AddPhase(phase) #iterate through shots for for image in proj._images.values(): currentTask = sharedDB.tasks.Tasks( _idphaseassignments=phase._idphaseassignments, _idprojects=proj._idprojects, _idshots=image._idshots, _idphases=phase._idphases, _new=1) currentTask.Save() image._tasks[str(currentTask.id())] = (currentTask) currentTask.Save() for seq in proj._sequences.values(): for shot in seq._shots.values(): currentTask = sharedDB.tasks.Tasks( _idphaseassignments=phase._idphaseassignments, _idprojects=proj._idprojects, _idshots=shot._idshots, _idphases=phase._idphases, _new=1) currentTask.Save() shot._tasks[str(currentTask.id())] = (currentTask) def loadinprojectview(self, project): #print "Loading Project"+self.cellWidget(row,column)._phaseassignment._name sharedDB.mainWindow.centralTabbedWidget.setCurrentIndex(0) sharedDB.myProjectViewWidget._currentProject = project sharedDB.myProjectViewWidget.LoadProjectValues() sharedDB.myProjectViewWidget.projectPartWidget.setCurrentIndex(0)
class XComboBox(QComboBox): """ ~~>[img:widgets/xcombobox.png] The XComboBox class is a simple extension to the standard QComboBox that provides a couple enhancement features, namely the ability to add a hint to the line edit and supporting multi-selection via checkable items. == Example == |>>> from projexui.widgets.xcombobox import XComboBox |>>> import projexui | |>>> # create the combobox |>>> combo = projexui.testWidget(XComboBox) | |>>> # set the hint |>>> combo.setHint('select type') | |>>> # create items, make checkable |>>> combo.addItems(['A', 'B', 'C']) |>>> combo.setCheckable(True) | |>>> # set the checked items |>>> combo.setCheckedItems(['C']) |>>> combo.setCheckedIndexes([0, 2]) | |>>> # retrieve checked items |>>> combo.checkedItems() |['A', 'C'] |>>> combo.checkedIndexes() |[0, 2] | |>>> # connect to signals |>>> def printChecked(): print checked.checkedItems() |>>> combo.checkedIndexesChanged.connect(printChecked) | |>>> # modify selection and see the output """ __designer_icon__ = projexui.resources.find('img/ui/combobox.png') checkedIndexesChanged = qt.Signal(list) checkedItemsChanged = qt.Signal(list) def __init__(self, parent=None): super(XComboBox, self).__init__(parent) # define custom properties self._checkable = False self._hint = '' self._separator = ',' # setup the checkable popup widget self._checkablePopup = None # set default properties self.setLineEdit(XLineEdit(self)) def adjustCheckState(self): """ Updates when new items are added to the system. """ if (self.isCheckable()): self.updateCheckState() def checkablePopup(self): """ Returns the popup if this widget is checkable. :return <QListView> || None """ if (not self._checkablePopup and self.isCheckable()): popup = QListView(self) popup.setSelectionMode(QListView.NoSelection) popup.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) popup.setWindowFlags(Qt.Popup) popup.installEventFilter(self) popup.doubleClicked.connect(self.checkModelIndex) self._checkablePopup = popup return self._checkablePopup def checkModelIndex(self, modelIndex): """ Sets the current index as the checked index. :param modelIndex | <QModelIndex> """ self.checkablePopup().hide() if (not self.isCheckable()): return self.setCheckedIndexes([modelIndex.row()]) def currentText(self): """ Returns the current text for this combobox, including the hint option \ if no text is set. """ lineEdit = self.lineEdit() if (lineEdit): return lineEdit.currentText() return super(XComboBox, self).currentText() def checkedIndexes(self): """ Returns a list of checked indexes for this combobox. :return [<int>, ..] """ if (not self.isCheckable()): return [] model = self.model() return [i for i in range(self.count()) if model.item(i).checkState()] def checkedItems(self): """ Returns the checked items for this combobox. :return [<str>, ..] """ if (not self.isCheckable()): return [] return [str(self.itemText(i)) for i in self.checkedIndexes()] def eventFilter(self, object, event): """ Filters events for the popup widget. :param object | <QObject> event | <QEvent> """ # popup the editor when clicking in the line edit for a checkable state if object == self.lineEdit() and self.isEnabled(): if not self.isCheckable(): return super(XComboBox, self).eventFilter(object, event) # show the popup when the user clicks on it elif event.type() == event.MouseButtonPress: self.showPopup() # eat the wheel event when the user is scrolling elif event.type() == event.Wheel: return True # make sure we're looking for the checkable popup elif object == self._checkablePopup: if event.type() == event.KeyPress and \ event.key() in (Qt.Key_Escape, Qt.Key_Return, Qt.Key_Enter): object.close() elif event.type() == event.MouseButtonPress: if not object.geometry().contains(event.pos()): object.close() return super(XComboBox, self).eventFilter(object, event) def hint(self): """ Returns the hint for this combobox. :return <str> """ return self._hint def hintColor(self): """ Returns the hint color for this combo box provided its line edit is an XLineEdit instance. :return <QColor> """ lineEdit = self.lineEdit() if isinstance(lineEdit, XLineEdit): return lineEdit.hintColor() return QColor() def isCheckable(self): """ Returns whether or not this combobox has checkable options. :return <bool> """ return self._checkable def items(self): """ Returns the labels for the different items in this combo box. :return [<str>, ..] """ return [self.itemText(i) for i in range(self.count())] def separator(self): """ Returns the separator that will be used for joining together the options when in checked mode. By default, this will be a comma. :return <str> """ return self._separator def setCheckedIndexes(self, indexes): """ Sets a list of checked indexes for this combobox. :param indexes | [<int>, ..] """ if (not self.isCheckable()): return model = self.model() for i in range(self.count()): if (not self.itemText(i)): continue item = model.item(i) if (i in indexes): state = Qt.Checked else: state = Qt.Unchecked item.setCheckState(state) def setCheckedItems(self, items): """ Returns the checked items for this combobox. :return items | [<str>, ..] """ if (not self.isCheckable()): return model = self.model() for i in range(self.count()): item_text = self.itemText(i) if (not item_text): continue if (str(item_text) in items): state = Qt.Checked else: state = Qt.Unchecked model.item(i).setCheckState(state) def setCheckable(self, state): """ Sets whether or not this combobox stores checkable items. :param state | <bool> """ self._checkable = state # need to be editable to be checkable edit = self.lineEdit() if state: self.setEditable(True) edit.setReadOnly(True) # create connections model = self.model() model.rowsInserted.connect(self.adjustCheckState) model.dataChanged.connect(self.updateCheckedText) elif edit: edit.setReadOnly(False) self.updateCheckState() self.updateCheckedText() def setEditable(self, state): """ Sets whether or not this combobox will be editable, updating its \ line edit to an XLineEdit if necessary. :param state | <bool> """ super(XComboBox, self).setEditable(state) if (state): edit = self.lineEdit() if (edit and isinstance(edit, XLineEdit)): return elif (edit): edit.setParent(None) edit.deleteLater() edit = XLineEdit(self) edit.setHint(self.hint()) self.setLineEdit(edit) def setLineEdit(self, edit): """ Sets the line edit for this widget. :warning If the inputed edit is NOT an instance of XLineEdit, \ this method will destroy the edit and create a new \ XLineEdit instance. :param edit | <XLineEdit> """ if (edit and not isinstance(edit, XLineEdit)): edit.setParent(None) edit.deleteLater() edit = XLineEdit(self) edit.installEventFilter(self) super(XComboBox, self).setLineEdit(edit) def setHint(self, hint): """ Sets the hint for this line edit that will be displayed when in \ editable mode. :param hint | <str> """ self._hint = hint lineEdit = self.lineEdit() if isinstance(lineEdit, XLineEdit): lineEdit.setHint(hint) def setHintColor(self, color): """ Sets the hint color for this combo box provided its line edit is an XLineEdit instance. :param color | <QColor> """ lineEdit = self.lineEdit() if isinstance(lineEdit, XLineEdit): lineEdit.setHintColor(color) @qt.Slot(str) def setSeparator(self, separator): """ Sets the separator that will be used when joining the checked items for this combo in the display. :param separator | <str> """ self._separator = str(separator) self.updateCheckedText() def showPopup(self): """ Displays a custom popup widget for this system if a checkable state \ is setup. """ if not self.isCheckable(): return super(XComboBox, self).showPopup() if not self.isVisible(): return # update the checkable widget popup point = self.mapToGlobal(QPoint(0, self.height() - 1)) popup = self.checkablePopup() popup.setModel(self.model()) popup.move(point) popup.setFixedWidth(self.width()) height = (self.count() * 19) + 2 if (height > 400): height = 400 popup.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) else: popup.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) popup.setFixedHeight(height) popup.show() popup.raise_() def updateCheckState(self): """ Updates the items to reflect the current check state system. """ checkable = self.isCheckable() model = self.model() flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled for i in range(self.count()): item = model.item(i) if (not (checkable and item.text())): item.setCheckable(False) item.setFlags(flags) # only allow checking for items with text else: item.setCheckable(True) item.setFlags(flags | Qt.ItemIsUserCheckable) def updateCheckedText(self): """ Updates the text in the editor to reflect the latest state. """ if (not self.isCheckable()): return self.lineEdit().setText(self.separator().join(self.checkedItems())) def toggleModelIndex(self, modelIndex): """ Toggles the index's check state. :param modelIndex | <QModelIndex> """ if (not self.isCheckable()): return item = self.model().item(modelIndex.row()) if (item.checkState() == Qt.Checked): state = Qt.Unchecked else: state = Qt.Checked item.setCheckState(state) # define qt properties x_hint = qt.Property(str, hint, setHint) x_checkable = qt.Property(bool, isCheckable, setCheckable) x_separator = qt.Property(str, separator, setSeparator)
class XNodeWidget(QGraphicsView): """ Defines the main widget for creating node graph views. """ __designer_icon__ = projexui.resources.find('img/ui/node.png') zoomAmountChanged = qt.Signal(int) def __init__(self, parent, sceneClass=None): # initialize the super class super(XNodeWidget, self).__init__(parent) # set the scene if (not sceneClass): sceneClass = XNodeScene self._cleanupOnClose = True self.setScene(sceneClass(self)) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) self.setContextMenuPolicy(Qt.CustomContextMenu) self.setBaseSize(QSize(300, 250)) def __dir__(self): out = set(self.__dict__.keys()) out.update(dir(self.scene())) return list(out) def __getattr__(self, key): return getattr(self.scene(), key) def _runLayoutTest(self, layoutName): """ Runs a layout test for this widget for the inputed layout plugin name. :param layoutName | <str> :return <bool> | success """ layout = XNodeLayout.plugin(layoutName) if not layout: return False layout.runTest(self.scene()) return True @qt.Slot() def autoLayout(self): """ Auto-lays out the whole scene. """ self.scene().autoLayout() @qt.Slot() def autoLayoutSelected(self): """ Auto-lays out the selected items. """ self.scene().autoLayoutSelected() def centerOn(self, *args): """ Updates the center on method to ensure the viewport is updated. :param *args | <variant> """ super(XNodeWidget, self).centerOn(*args) for con in self.connections(): con.setPath(con.rebuild()) con.update() def centerOnAnimated(self, centerOn, animate=0): """ Animates the centering options over a given number of seconds. :param centerOn | <QRectF> | <QPointF> | <XNode> animate | <float> | seconds """ if isinstance(centerOn, XNode): center = centerOn.sceneRect().center() elif isinstance(centerOn, QRectF): center = centerOn.center() elif isinstance(centerOn, QPointF): center = centerOn else: return anim = XObjectAnimation(self, 'centerOn', self) anim.setStartValue(self.viewportRect().center()) anim.setEndValue(center) anim.setDuration(1000 * animate) anim.start() anim.finished.connect(anim.deleteLater) def centerOnItems(self, items=None): """ Centers on the given items, if no items are supplied, then all items will be centered on. :param items | [<QGraphicsItem>, ..] """ if not items: rect = self.scene().visibleItemsBoundingRect() if not rect.width(): rect = self.scene().sceneRect() self.centerOn(rect.center()) else: self.centerOn(self.scene().calculateBoundingRect(items).center()) def centerOnSelection(self): """ Centers on the selected items. :sa centerOnItems """ self.centerOnItems(self.scene().selectedItems()) def cleanupOnClose(self): """ Sets whether or not this widget should clean up its scene before closing. :return <bool> """ return self._cleanupOnClose def closeEvent(self, event): """ Cleans up the scene before closing. :param event | <QEvent> """ if (self.cleanupOnClose()): scene = self.scene() scene.cleanup() self.setScene(None) super(XNodeWidget, self).closeEvent(event) @qt.Slot() def disableViewMode(self): """ Sets the node widget into selection mode which allows the user to select vs. pan and zoom. """ self.scene().setViewMode(False) @qt.Slot() def enableViewMode(self): """ Sets the node widget into view mode which allows the user to pan and zoom vs. select. """ self.scene().setViewMode(True) def findNodeByRegex(self, nodeRegex): """ Returns the first node that matches the inputed regular expression. :param nodeRegex | <str> :return <XNode> || None """ return self.scene().findNodeByRegex(nodeRegex) def findNode(self, nodeName): """ Returns the node for the given node name. :param nodeName | <str> :return <XNode> || None """ return self.scene().findNode(nodeName) def isolationMode(self): """ Returns whether or not this widget is in isolation mode. :return <bool> """ return self.scene().isolationMode() def setCleanupOnClose(self, state): """ Sets whether or not the scene should be cleaned up before closing. :param state | <bool> """ self._cleanupOnClose = state @qt.Slot(bool) def setIsolationMode(self, state): """ Sets whether or not the widget is in isolation mode. :param state | <bool> """ self.scene().setIsolationMode(state) @qt.Slot(int) def setZoomAmount(self, amount): """ Sets the zoom amount for this widget to the inputed amount. :param amount | <int> """ self.scene().setZoomAmount(amount) def viewportRect(self): """ Returns the QRectF that represents the visible viewport rect for the current view. :return <QRectF> """ w = self.width() h = self.height() vbar = self.verticalScrollBar() hbar = self.horizontalScrollBar() if vbar.isVisible(): w -= vbar.width() if hbar.isVisible(): h -= hbar.height() top_l = self.mapToScene(QPoint(0, 0)) bot_r = self.mapToScene(QPoint(w, h)) return QRectF(top_l.x(), top_l.y(), bot_r.x() - top_l.x(), bot_r.y() - top_l.y()) def zoomAmount(self): """ Returns the zoom amount for this widget to the inputed amount. :param amount | <int> """ return self.scene().zoomAmount() @qt.Slot() def zoomExtents(self): """ Fits all the nodes in the view. """ rect = self.scene().visibleItemsBoundingRect() vrect = self.viewportRect() if rect.width(): if rect.width() < vrect.width() and rect.height() < vrect.height(): self.centerOn(rect.center()) else: self.fitInView(rect, Qt.KeepAspectRatio) if not self.signalsBlocked(): self.zoomAmountChanged.emit(self.zoomAmount()) @qt.Slot() def zoomIn(self): """ Zooms in for this widget by the scene's zoom step amount. """ self.scene().zoomIn() @qt.Slot() def zoomOut(self): """ Zooms out for this widget by the scene's zoom step amount. """ self.scene().zoomOut() x_isolationMode = qt.Property(bool, isolationMode, setIsolationMode) x_cleanupOnClose = qt.Property(bool, cleanupOnClose, setCleanupOnClose)
class XToolBar(QToolBar): collapseToggled = qt.Signal(bool) def __init__( self, *args ): super(XToolBar, self).__init__( *args ) # set custom properties self._collapseButton = None self._collapsed = True self._collapsedSize = 14 self._precollapseSize = None # set standard options self.layout().setSpacing(0) self.layout().setContentsMargins(1, 1, 1, 1) self.setMovable(False) self.clear() self.setOrientation(Qt.Horizontal) self.setCollapsed(False) def clear( self ): """ Clears out this toolbar from the system. """ # preserve the collapse button super(XToolBar, self).clear() self._collapseButton = QToolButton(self) self._collapseButton.setAutoRaise(True) self._collapseButton.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding ) self.addWidget(self._collapseButton) self.refreshButton() # create connection self._collapseButton.clicked.connect( self.toggleCollapsed ) def count( self ): """ Returns the number of actions linked with this toolbar. :return <int> """ return len(self.actions()) def collapseButton( self ): """ Returns the collapsing button for this toolbar. :return <QToolButton> """ return self._collapseButton def isCollapsed( self ): """ Returns whether or not this toolbar is in a collapsed state. :return <bool> """ return self._collapsed def refreshButton( self ): """ Refreshes the button for this toolbar. """ collapsed = self.isCollapsed() btn = self._collapseButton if ( not btn ): return btn.setMaximumSize(MAX_SIZE, MAX_SIZE) # set up a vertical scrollbar if ( self.orientation() == Qt.Vertical ): btn.setMaximumHeight(12) else: btn.setMaximumWidth(12) icon = '' # collapse/expand a vertical toolbar if ( self.orientation() == Qt.Vertical ): if ( collapsed ): self.setFixedWidth(self._collapsedSize) btn.setMaximumHeight(MAX_SIZE) btn.setArrowType(Qt.RightArrow) else: self.setMaximumWidth(1000) self._precollapseSize = None btn.setMaximumHeight(12) btn.setArrowType(Qt.LeftArrow) else: if ( collapsed ): self.setFixedHeight(self._collapsedSize) btn.setMaximumWidth(MAX_SIZE) btn.setArrowType(Qt.DownArrow) else: self.setMaximumHeight(1000) self._precollapseSize = None btn.setMaximumWidth(12) btn.setArrowType(Qt.UpArrow) for index in range(1, self.layout().count()): item = self.layout().itemAt(index) if ( not item.widget() ): continue if ( collapsed ): item.widget().setMaximumSize(0, 0) else: item.widget().setMaximumSize(MAX_SIZE, MAX_SIZE) def setCollapsed( self, state ): """ Sets whether or not this toolbar is in a collapsed state. :return <bool> changed """ if ( state == self._collapsed ): return False self._collapsed = state self.refreshButton() if ( not self.signalsBlocked() ): self.collapseToggled.emit(state) return True def setOrientation( self, orientation ): """ Sets the orientation for this toolbar to the inputed value, and \ updates the contents margins and collapse button based on the vaule. :param orientation | <Qt.Orientation> """ super(XToolBar, self).setOrientation(orientation) self.refreshButton() def toggleCollapsed( self ): """ Toggles the collapsed state for this toolbar. :return <bool> changed """ return self.setCollapsed(not self.isCollapsed())
class XFilepathEdit(QWidget): """ The XFilepathEdit class provides a common interface to prompt the user to select a filepath from the filesystem. It can be configured to load directories, point to a save file path location, or to an open file path location. It can also be setup to color changed based on the validity of the existance of the filepath. == Example == |>>> from projexui.widgets.xfilepathedit import XFilepathEdit |>>> import projexui | |>>> # create the edit |>>> edit = projexui.testWidget(XFilepathEdit) | |>>> # set the filepath |>>> edit.setFilepath('/path/to/file') | |>>> # prompt the user to select the filepath |>>> edit.pickFilepath() | |>>> # enable the coloring validation |>>> edit.setValidated(True) """ __designer_icon__ = projexui.resources.find('img/file.png') Mode = enum('OpenFile', 'SaveFile', 'Path', 'OpenFiles') filepathChanged = qt.Signal(str) def __init__(self, parent=None): super(XFilepathEdit, self).__init__(parent) # define custom properties self._validated = False self._validForeground = QColor(0, 120, 0) self._validBackground = QColor(0, 120, 0, 100) self._invalidForeground = QColor(255, 0, 0) self._invalidBackground = QColor(255, 0, 0, 100) self._normalizePath = False self._filepathMode = XFilepathEdit.Mode.OpenFile self._filepathEdit = XLineEdit(self) self._filepathButton = QToolButton(self) self._filepathTypes = 'All Files (*.*)' # set default properties ico = projexui.resources.find('img/folder.png') self._filepathEdit.setReadOnly(False) self._filepathButton.setText('...') self._filepathButton.setFixedSize(25, 23) self._filepathButton.setAutoRaise(True) self._filepathButton.setIcon(QIcon(ico)) self._filepathEdit.setContextMenuPolicy(Qt.CustomContextMenu) self.setWindowTitle('Load File') self.setAcceptDrops(True) # define the layout layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._filepathEdit) layout.addWidget(self._filepathButton) self.setLayout(layout) # create connections self._filepathEdit.installEventFilter(self) self._filepathButton.clicked.connect(self.pickFilepath) self._filepathEdit.textChanged.connect(self.emitFilepathChanged) self._filepathEdit.textChanged.connect(self.validateFilepath) self._filepathEdit.customContextMenuRequested.connect(self.showMenu) def autoRaise(self): """ Returns whether or not the tool button will auto raise. :return <bool> """ return self._filepathButton.autoRaise() @qt.Slot() def clearFilepath(self): """ Clears the filepath contents for this path. """ self.setFilepath('') @qt.Slot() def copyFilepath(self): """ Copies the current filepath contents to the current clipboard. """ clipboard = QApplication.instance().clipboard() clipboard.setText(self.filepath()) clipboard.setText(self.filepath(), clipboard.Selection) def dragEnterEvent(self, event): """ Processes drag enter events. :param event | <QDragEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dragMoveEvent(self, event): """ Processes drag move events. :param event | <QDragEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dropEvent(self, event): """ Processes drop event. :param event | <QDropEvent> """ if (event.mimeData().hasUrls()): url = event.mimeData().urls()[0] filepath = url.toLocalFile() if (filepath): self.setFilepath(filepath) def emitFilepathChanged(self): """ Emits the filepathChanged signal for this widget if the signals are \ not being blocked. """ if (not self.signalsBlocked()): self.filepathChanged.emit(self.filepath()) def eventFilter(self, object, event): """ Overloads the eventFilter to look for click events on the line edit. :param object | <QObject> event | <QEvent> """ if ( object == self._filepathEdit and \ self._filepathEdit.isReadOnly() and \ event.type() == event.MouseButtonPress and \ event.button() == Qt.LeftButton ): self.pickFilepath() return False def filepath(self, validated=False): """ Returns the filepath for this widget. If the validated flag is set \ then this method will only return if the file or folder actually \ exists for this path. In the case of a SaveFile, only the base folder \ needs to exist on the system, in other modes the actual filepath must \ exist. If not validated, the text will return whatever is currently \ entered. :return <str> """ paths = self.filepaths() if not paths: return '' if not validated or self.isValid(): return paths[0] return '' def filepaths(self): """ Returns a list of the filepaths for this edit. :return [<str>, ..] """ return str(self._filepathEdit.text()).split(os.path.pathsep) def filepathMode(self): """ Returns the filepath mode for this widget. :return <XFilepathEdit.Mode> """ return self._filepathMode def filepathModeText(self): """ Returns the text representation for this filepath mode. :return <str> """ return XFilepathEdit.Mode[self._filepathMode] def filepathTypes(self): """ Returns the filepath types that will be used for this widget. :return <str> """ return self._filepathTypes def hint(self): """ Returns the hint for this filepath. :return <str> """ return self._filepathEdit.hint() def icon(self): """ Returns the icon that is used for this filepath widget. :return <QIcon> """ return self._filepathButton.icon() def invalidBackground(self): """ Returns the invalid background color for this widget. :return <QColor> """ return self._invalidBackground def invalidForeground(self): """ Returns the invalid foreground color for this widget. :return <QColor> """ return self._invalidForeground def isValid(self): """ Returns whether or not the filepath exists on the system. \ In the case of a SaveFile, only the base folder \ needs to exist on the system, in other modes the actual filepath must \ exist. :return <bool> """ check = str(self._filepathEdit.text()).split(os.path.pathsep)[0] if (self.filepathMode() == XFilepathEdit.Mode.SaveFile): check = os.path.dirname(check) return os.path.exists(check) def isValidated(self): """ Set whether or not to validate the filepath as the user is working \ with it. :return <bool> """ return self._validated def isReadOnly(self): """ Returns if the widget is read only for text editing or not. :return <bool> """ return self._filepathEdit.isReadOnly() def normalizePath(self): """ Returns whether or not the path should be normalized for the current operating system. When off, it will be defaulted to forward slashes (/). :return <bool> """ return self._normalizePath def pickFilepath(self): """ Prompts the user to select a filepath from the system based on the \ current filepath mode. """ mode = self.filepathMode() filepath = '' filepaths = [] curr_dir = str(self._filepathEdit.text()) if (not curr_dir): curr_dir = QDir.currentPath() if mode == XFilepathEdit.Mode.SaveFile: filepath = QFileDialog.getSaveFileName(self, self.windowTitle(), curr_dir, self.filepathTypes()) elif mode == XFilepathEdit.Mode.OpenFile: filepath = QFileDialog.getOpenFileName(self, self.windowTitle(), curr_dir, self.filepathTypes()) elif mode == XFilepathEdit.Mode.OpenFiles: filepaths = QFileDialog.getOpenFileNames(self, self.windowTitle(), curr_dir, self.filepathTypes()) else: filepath = QFileDialog.getExistingDirectory( self, self.windowTitle(), curr_dir) if filepath: if type(filepath) == tuple: filepath = filepath[0] self.setFilepath(str(filepath)) elif filepaths: self.setFilepaths(map(str, filepaths)) def setAutoRaise(self, state): """ Sets whether or not the tool button will auto raise. :param state | <bool> """ self._filepathButton.setAutoRaise(state) @qt.Slot(int) def setFilepathMode(self, mode): """ Sets the filepath mode for this widget to the inputed mode. :param mode | <XFilepathEdit.Mode> """ self._filepathMode = mode @qt.Slot(str) def setFilepathModeText(self, text): """ Sets the filepath mode for this widget based on the inputed text. :param text | <str> :return <bool> | success """ try: self.setFilepathMode(XFilepathEdit.Mode[str(text)]) return True except KeyError: return False @qt.Slot(str) def setFilepathTypes(self, filepathTypes): """ Sets the filepath type string that will be used when looking up \ filepaths. :param filepathTypes | <str> """ self._filepathTypes = filepathTypes @qt.Slot(str) def setFilepath(self, filepath): """ Sets the filepath text for this widget to the inputed path. :param filepath | <str> """ if self.normalizePath(): filepath = os.path.normpath(str(filepath)) else: filepath = os.path.normpath(str(filepath)).replace('\\', '/') self._filepathEdit.setText(filepath) def setFilepaths(self, filepaths): """ Sets the list of the filepaths for this widget to the inputed paths. :param filepaths | [<str>, ..] """ self.setFilepath(os.path.pathsep.join(filepaths)) def setHint(self, hint): """ Sets the hint for this filepath. :param hint | <str> """ if self.normalizePath(): filepath = os.path.normpath(str(hint)) else: filepath = os.path.normpath(str(hint)).replace('\\', '/') self._filepathEdit.setHint(hint) def setIcon(self, icon): """ Sets the icon that will be used for this widget's tool button. :param icon | <QIcon> || <str> """ self._filepathButton.setIcon(QIcon(icon)) def setInvalidBackground(self, bg): """ Sets the invalid background color for this widget to the inputed widget. :param bg | <QColor> """ self._invalidBackground = QColor(bg) def setInvalidForeground(self, fg): """ Sets the invalid foreground color for this widget to the inputed widget. :param fg | <QColor> """ self._invalidForeground = QColor(fg) def setNormalizePath(self, state): """ Sets whether or not the path should be normalized for the current operating system. When off, it will be defaulted to forward slashes (/). :param state | <bool> """ self._normalizePath = state @qt.Slot(bool) def setReadOnly(self, state): """ Sets whether or not this filepath widget is readonly in the text edit. :param state | <bool> """ self._filepathEdit.setReadOnly(state) @qt.Slot(bool) def setValidated(self, state): """ Set whether or not to validate the path as the user edits it. :param state | <bool> """ self._validated = state palette = self.palette() # reset the palette to default, revalidate self._filepathEdit.setPalette(palette) self.validate() def setValidBackground(self, bg): """ Sets the valid background color for this widget to the inputed color. :param bg | <QColor> """ self._validBackground = QColor(bg) def setValidForeground(self, fg): """ Sets the valid foreground color for this widget to the inputed color. :param fg | <QColor> """ self._validForeground = QColor(fg) def showMenu(self, pos): """ Popups a menu for this widget. """ menu = QMenu(self) menu.setAttribute(Qt.WA_DeleteOnClose) menu.addAction('Clear').triggered.connect(self.clearFilepath) menu.addSeparator() menu.addAction('Copy Filepath').triggered.connect(self.copyFilepath) menu.exec_(self.mapToGlobal(pos)) def validBackground(self): """ Returns the valid background color for this widget. :return <QColor> """ return self._validBackground def validForeground(self): """ Returns the valid foreground color for this widget. :return <QColor> """ return self._validForeground def validateFilepath(self): """ Alters the color scheme based on the validation settings. """ if (not self.isValidated()): return valid = self.isValid() if (not valid): fg = self.invalidForeground() bg = self.invalidBackground() else: fg = self.validForeground() bg = self.validBackground() palette = self.palette() palette.setColor(palette.Base, bg) palette.setColor(palette.Text, fg) self._filepathEdit.setPalette(palette) # map Qt properties x_autoRaise = qt.Property(bool, autoRaise, setAutoRaise) x_filepathTypes = qt.Property(str, filepathTypes, setFilepathTypes) x_filepath = qt.Property(str, filepath, setFilepath) x_readOnly = qt.Property(bool, isReadOnly, setReadOnly) x_validated = qt.Property(bool, isValidated, setValidated) x_hint = qt.Property(str, hint, setHint) x_icon = qt.Property('QIcon', icon, setIcon) x_normalizePath = qt.Property(bool, normalizePath, setNormalizePath) x_invalidForeground = qt.Property('QColor', invalidForeground, setInvalidForeground) x_invalidBackground = qt.Property('QColor', invalidBackground, setInvalidBackground) x_validForeground = qt.Property('QColor', validForeground, setValidForeground) x_validBackground = qt.Property('QColor', validBackground, setValidBackground) x_filepathModeText = qt.Property(str, filepathModeText, setFilepathModeText)
class XView(QWidget): activated = qt.Signal() currentStateChanged = qt.Signal() windowTitleChanged = qt.Signal(str) sizeConstraintChanged = qt.Signal() initialized = qt.Signal() visibleStateChanged = qt.Signal(bool) _registry = {} _globals = {} _viewName = '' _viewGroup = 'Default' _viewIcon = resources.find('img/view/view.png') _viewSingleton = False _popupSingleton = True _popupInstance = None _currentViewRef = None _instances = None _instanceSingleton = None _dispatcher = None _dispatch = {} __designer_icon__ = resources.find('img/ui/view.png') SignalPolicy = enum('BlockIfNotCurrent', 'BlockIfNotInGroup', 'BlockIfNotVisible', 'BlockIfNotInitialized', 'NeverBlock') def __init__(self, parent, autoKillThreads=True): super(XView, self).__init__( parent ) if not self._viewSingleton: self.setAttribute(Qt.WA_DeleteOnClose) # define custom properties self._initialized = False self._destroyThreadsOnClose = True self._viewingGroup = 0 self._signalPolicy = XView.SignalPolicy.BlockIfNotInitialized | \ XView.SignalPolicy.BlockIfNotVisible | \ XView.SignalPolicy.BlockIfNotInGroup self._visibleState = False # storing this state for knowing if a # widget WILL be visible once Qt finishes # processing for purpose of signal # validation. # setup default properties self.setFocusPolicy( Qt.StrongFocus ) self.setWindowTitle( self.viewName() ) self.registerInstance(self) if autoKillThreads: QApplication.instance().aboutToQuit.connect(self.killChildThreads) def canClose( self ): return True def closeEvent( self, event ): if self.isViewSingleton(): self.setParent(None) # make sure to destroy any threads running on close if self.testAttribute(Qt.WA_DeleteOnClose) and \ self.destroyThreadsOnClose(): self.killChildThreads() # clear out any progress loaders XLoaderWidget.stopAll(self) # remove any registered instances if self.testAttribute(Qt.WA_DeleteOnClose): self.unregisterInstance(self) super(XView, self).closeEvent(event) def dispatchConnect( self, signal, slot ): """ Connect the slot for this view to the given signal that gets emitted by the XView.dispatch() instance. :param signal | <str> slot | <callable> """ XView.dispatch().connect(signal, slot) def dispatchEmit( self, signal, *args ): """ Emits the given signal via the XView dispatch instance with the given arguments. :param signal | <str> args | <tuple> """ XView.setGlobal('emitGroup', self.viewingGroup()) XView.dispatch().emit(signal, *args) def destroyThreadsOnClose( self ): """ Marks whether or not any child threads should be destroyed when \ this view is closed. :return <bool> """ return self._destroyThreadsOnClose def duplicate( self, parent ): """ Duplicates this current view for another. Subclass this method to provide any additional duplication options. :param parent | <QWidget> :return <XView> | instance of this class """ # only return a single singleton instance if ( self.isViewSingleton() ): return self output = type(self).createInstance(parent) # save/restore the current settings xdata = ElementTree.Element('data') self.saveXml(xdata) new_name = output.objectName() output.setObjectName(self.objectName()) output.restoreXml(xdata) output.setObjectName(new_name) return output def killChildThreads(self): """ Kills all child threads for this view. """ threads = self.findChildren(QThread) for thread in threads: thread.finished.connect(thread.deleteLater) thread.quit() thread.wait(100) def hideEvent(self, event): """ Sets the visible state for this widget. If it is the first time this widget will be visible, the initialized signal will be emitted. :param state | <bool> """ super(XView, self).hideEvent(event) # record the visible state for this widget to be separate of Qt's # system to know if this view WILL be visible or not once the # system is done processing. This will affect how signals are # validated as part of the visible slot delegation self._visibleState = False if not self.signalsBlocked(): self.visibleStateChanged.emit(False) def initialize(self, force=False): """ Initializes the view if it is visible or being loaded. """ if force or (self.isVisible() and \ not self.isInitialized() and \ not self.signalsBlocked()): self._initialized = True self.initialized.emit() def isCurrent( self ): return self == self.currentView() def isInitialized(self): """ Returns whether or not this view has been initialized. A view will be initialized the first time it becomes visible to the user. You can use this to delay loading of information until it is needed by listening for the initialized signal. :return <bool> """ return self._initialized @deprecatedmethod('XView', 'Use restoreXml instead.') def restoreSettings( self, settings ): """ Restores the settings for this view from the inputed QSettings. :param settings | <QSettings> """ pass def restoreXml( self, xml ): """ Restores the settings for this view from the inputed XML node. :param xml | <xml.etree.ElementTree.Element> """ pass @deprecatedmethod('XView', 'Use saveXml instead.') def saveSettings( self, settings ): """ Saves the current settings for this view to the inputed QSettings. :param settings | <QSettings> """ pass def saveXml( self, xml ): """ Saves the settings for this view to the inputed XML node. :param xml | <xml.etree.ElementTree.Element> """ pass def settingsName( self ): """ Returns the default settings name for this view. :return <str> """ return 'Views/%s' % self.objectName() def setCurrent( self, state = True ): """ Marks this view as the current source based on the inputed flag. \ This method will return True if the currency changes. :return <bool> """ if ( state ): changed = self.setCurrentView(self) elif ( self.currentView() == self ): changed = self.setCurrentView(None) if ( changed and not self.signalsBlocked() ): self.currentStateChanged.emit() if state: self.activated.emit() return changed def setDestroyThreadsOnClose( self, state ): """ Marks whether or not any child threads should be destroyed when \ this view is closed. :param state | <bool> """ self._destroyThreadsOnClose = state def setFixedHeight( self, height ): """ Sets the maximum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setFixedHeight(height) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setFixedWidth( self, width ): """ Sets the maximum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setFixedWidth(width) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMaximumHeight( self, height ): """ Sets the maximum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setMaximumHeight(height) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMaximumSize( self, *args ): """ Sets the maximum size value to the inputed size and emits the \ sizeConstraintChanged signal. :param *args | <tuple> """ super(XView, self).setMaximumSize(*args) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMaximumWidth( self, width ): """ Sets the maximum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setMaximumWidth(width) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMinimumHeight( self, height ): """ Sets the minimum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setMinimumHeight(height) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMinimumSize( self, *args ): """ Sets the minimum size value to the inputed size and emits the \ sizeConstraintChanged signal. :param *args | <tuple> """ super(XView, self).setMinimumSize(*args) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMinimumWidth( self, width ): """ Sets the minimum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setMinimumWidth(width) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setSignalPolicy( self, policy ): """ Sets the signal delegation policy for this instance to the given policy. By default, signals will be delegates for groups or by currency if they are not in a group. This will not directly affect signal propogation, only the result of the validateSignal method, so if you want to test against this, then you will need to check in your slot. :param policy | <XView.SignalPolicy> """ self._signalPolicy = policy def setViewingGroup( self, grp ): """ Sets the viewing group that this view is associated with. :param grp | <int> """ self._viewingGroup = grp def setWindowTitle( self, title ): """ Sets the window title for this view, and emits the windowTitleChanged \ signal if the signals are not blocked. Setting this title will update \ the tab title for the view within the widget. :param title | <str> """ super(XView, self).setWindowTitle(title) if ( not self.signalsBlocked() ): self.windowTitleChanged.emit(title) def showActiveState( self, state ): """ Shows this view in the active state based on the inputed state settings. :param state | <bool> """ self.setAutoFillBackground(True) palette = self.window().palette() clr = palette.color(palette.Window) avg = (clr.red() + clr.green() + clr.blue()) / 3 if ( avg < 180 and state ): clr = clr.lighter(105) elif ( not state ): clr = clr.darker(105) palette.setColor(palette.Window, clr) self.setPalette(palette) def showEvent(self, event): """ Sets the visible state for this widget. If it is the first time this widget will be visible, the initialized signal will be emitted. :param state | <bool> """ super(XView, self).showEvent(event) # record the visible state for this widget to be separate of Qt's # system to know if this view WILL be visible or not once the # system is done processing. This will affect how signals are # validated as part of the visible slot delegation self._visibleState = True if not self.isInitialized(): self.initialize() # after the initial time the view is loaded, the visibleStateChanged # signal will be emitted elif not self.signalsBlocked(): self.visibleStateChanged.emit(True) def signalPolicy( self ): """ Returns the signal policy for this instance. :return <XView.SignalPolicy> """ return self._signalPolicy def rootWidget( self ): widget = self while ( widget.parent() ): widget = widget.parent() return widget def validateSignal( self, policy = None ): """ Validates that this view is part of the group that was emitting the signal. Views that are not in any viewing group will accept all signals. :param policy | <XView.SignalPolicy> || None :return <bool> """ # validate whether or not to process a signal if policy is None: policy = self.signalPolicy() group_check = XView.getGlobal('emitGroup') == self.viewingGroup() current_check = self.isCurrent() # always delegate signals if they are not set to block, # or if the method is called directly (not from a signal) if not self.sender() or policy & XView.SignalPolicy.NeverBlock: return True # block delegation of the signal if the view is not initialized elif policy & XView.SignalPolicy.BlockIfNotInitialized and \ not self.isInitialized(): return False # block delegation if the view is not visible elif policy & XView.SignalPolicy.BlockIfNotVisible and \ not self._visibleState: return False # block delegation if the view is not part of a group elif self.viewingGroup() and \ policy & XView.SignalPolicy.BlockIfNotInGroup: return group_check # look for only currency releated connections elif policy & XView.SignalPolicy.BlockIfNotCurrent: return current_check else: return True def viewingGroup( self ): """ Returns the viewing group that this view is assigned to. :return <int> """ return self._viewingGroup @classmethod def currentView( cls ): # look for last focused view if ( cls._currentViewRef ): inst = cls._currentViewRef() if ( inst ): return inst cls._currentViewRef = None return None @classmethod def createInstance( cls, parent ): # assign the singleton instance if ( cls._instanceSingleton ): instance = cls._instanceSingleton() # make sure the instance is still in use if ( not instance ): cls._instanceSingleton = None else: instance.setParent(parent) return instance # determine if we need to store a singleton inst = cls(parent) inst.setObjectName( cls.uniqueName() ) if ( cls.isViewSingleton() ): cls._instanceSingleton = weakref.ref(inst) return inst @classmethod def destroyInstance( cls, inst ): if ( cls.isViewSingleton() ): inst.close() return inst.close() inst.setParent(None) inst.deleteLater() @classmethod @deprecatedmethod('XView', 'Use the XView.dispatch() syntax now.') def dispatcher( cls ): if ( not cls._dispatcher ): cls._dispatcher = cls.MetaDispatcher(cls) return cls._dispatcher @classmethod def instances( cls ): """ Returns all generated instances of a particular view type. :return [<XView>, ..] """ if ( not cls._instances ): return [] # purge the instance list insts = [] refs = [] for ref in cls._instances: inst = ref() if ( not inst ): continue insts.append(inst) refs.append(ref) cls._instances = refs return insts @classmethod def isViewSingleton( cls ): return cls._viewSingleton @classmethod def isPopupSingleton( cls ): return cls._popupSingleton @classmethod def popup( cls, parent = None ): """ Pops up this view as a new dialog. If the forceDialog flag is set to \ False, then it will try to activate the first instance of the view \ within an existing viewwidget context before creating a new dialog. :param parent | <QWidget> || None """ # popup the singleton view for this class if ( cls.isViewSingleton() ): inst = cls.currentView() if ( not inst ): inst = cls.createInstance(parent) inst.setWindowFlags(Qt.Dialog) inst.show() # popup the popupSingleton for this class inst = cls.popupInstance(parent) inst.show() @classmethod def popupInstance( cls, parent ): if ( cls._popupInstance ): return cls._popupInstance inst = cls.createInstance(parent) inst.setWindowFlags(Qt.Dialog) if cls.isPopupSingleton(): inst.setAttribute(Qt.WA_DeleteOnClose, False) cls._popupInstance = inst return inst @classmethod def registerInstance( cls, instance ): if ( not cls._instances ): cls._instances = [] cls._instances.append(weakref.ref(instance)) @classmethod def registerToWindow( cls, window ): """ Registers this view to the window to update additional menu items, \ actions, and toolbars. :param window | <QWidget> """ pass @classmethod def restoreGlobalSettings( cls, settings ): """ Restores the global settings for the inputed view class type. :param cls | <subclass of XView> settings | <QSettings> """ pass @classmethod def saveGlobalSettings( cls, settings ): """ Saves the global settings for the inputed view class type. :param cls | <subclass of XView> settings | <QSettings> """ pass @classmethod def setViewGroup( cls, grp ): cls._viewGroup = grp @classmethod def setCurrentView( cls, view ): current = cls.currentView() if ( current == view ): return False elif ( current ): current.showActiveState(False) if ( view ): view.showActiveState(True) cls._currentViewRef = weakref.ref(view) return True @classmethod def setViewIcon( cls, icon ): cls._viewIcon = icon @classmethod def setViewName( cls, name ): cls._viewName = name @classmethod def setViewSingleton( cls, state ): cls._viewSingleton = state @classmethod def setPopupSingleton( cls, state ): cls._popupSingleton = state @classmethod def uniqueName( cls ): names = map(lambda x: str(x.objectName()), cls.instances()) index = 1 base = cls.viewName() name = '%s%02i' % (base, index) while ( name in names ): index += 1 name = '%s%02i' % (base, index) return name @classmethod def unregisterInstance( cls, instance ): if ( not cls._instances ): return if ( cls._currentViewRef and instance == cls._currentViewRef() ): cls._currentViewRef = None refs = [] for ref in cls._instances: inst = ref() if ( not inst or inst == instance ): continue refs.append(ref) cls._instances = refs @classmethod def unregisterToWindow( cls, window ): """ Registers this view to the window to update additional menu items, \ actions, and toolbars. :param window | <QWidget> """ pass @classmethod def viewGroup( cls ): return cls._viewGroup @classmethod def viewIcon( cls ): return cls._viewIcon @classmethod def viewName( cls ): return cls._viewName @classmethod def viewTypeName( cls ): """ Returns the unique name for this view type by joining its group with \ its name. :return <str> """ return '%s.%s' % (cls.viewGroup(), cls.viewName()) #-------------------------------------------------------------------------- @staticmethod def dispatch( location = 'Central' ): """ Returns the instance of the global view dispatching system. All views \ will route their signals through the central hub so no single view \ necessarily depends on another. :return <XViewDispatch> """ dispatch = XView._dispatch.get(str(location)) if ( not dispatch ): dispatch = XViewDispatch(QApplication.instance()) XView._dispatch[str(location)] = dispatch return dispatch @staticmethod def getGlobal( key, default = None ): """ Returns the global value for the inputed key. :param key | <str> default | <variant> :return <variant> """ return XView._globals.get(key, default) @staticmethod def registeredView( viewName, location = 'Central' ): """ Returns the view that is registered to the inputed location for the \ given name. :param viewName | <str> location | <str> :return <subclass of XView> || None """ for viewType in XView._registry.get(str(location), []): if ( viewType.viewName() == viewName ): return viewType return None @staticmethod def registeredViews( location = 'Central' ): """ Returns all the views types that have bene registered to a particular \ location. :param location | <str> :return [<subclass of XView>, ..] """ return XView._registry.get(str(location), []) @staticmethod def registerView( viewType, location = 'Central' ): """ Registers the inputed view type to the given location. The location \ is just a way to group and organize potential view plugins for a \ particular widget, and is determined per application. This eases \ use when building a plugin based system. It has no relevance to the \ XView class itself where you register a view. :param viewType | <subclass of XView> """ # update the dispatch signals if ( '__xview_signals__' in viewType.__dict__ ): XView.dispatch(location).registerSignals(viewType.__xview_signals__) location = str(location) XView._registry.setdefault(location, []) XView._registry[str(location)].append(viewType) @staticmethod def setGlobal( key, value ): """ Shares a global value across all views by setting the key in the \ globals dictionary to the inputed value. :param key | <str> value | <variant> """ XView._globals[key] = value @staticmethod def updateCurrentView( oldWidget, newWidget ): """ Updates the current view for each view class type based on the new \ focused widget. :param oldWidget | <QWidget> newWidget | <QWidget> """ widget = newWidget while widget: if isinstance(widget, XView): widget.setCurrent() break widget = widget.parent()
class XIconButton(QPushButton): """ """ __designer_icon__ = projexui.resources.find('img/ui/icon.png') filepathChanged = qt.Signal(str) def __init__(self, parent=None): super(XIconButton, self).__init__(parent) # define custom properties self._filepath = '' self._fileTypes = 'PNG Files (*.png);;All Files (*.*)' # set default properties self.setFixedWidth(64) self.setFixedHeight(64) self.setAcceptDrops(True) # create connections self.clicked.connect(self.pickFilepath) def filepath(self): """ Returns the filepath for this button. :return <str> """ return self._filepath def fileTypes(self): """ Returns the file types that will be used to filter this button. :return <str> """ return self._fileTypes def dragEnterEvent(self, event): """ Handles a drag enter event. :param event | <QEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dragMoveEvent(self, event): """ Handles a drag move event. :param event | <QEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dropEvent(self, event): """ Handles a drop event. """ url = event.mimeData().urls()[0] url_path = str(url.toString()) # download an icon from the web if (not url_path.startswith('file:')): filename = os.path.basename(url_path) temp_path = os.path.join(str(QDir.tempPath()), filename) try: urllib.urlretrieve(url_path, temp_path) except IOError: return self.setFilepath(temp_path) else: self.setFilepath(url_path.replace('file://', '')) def pickFilepath(self): """ Picks the image file to use for this icon path. """ filepath = QFileDialog.getOpenFileName(self, 'Select Image File', QDir.currentPath(), self.fileTypes()) if type(filepath) == tuple: filepath = str(filepath[0]) if (filepath): self.setFilepath(filepath) def setFilepath(self, filepath): """ Sets the filepath for this button to the inputed path. :param filepath | <str> """ self._filepath = str(filepath) self.setIcon(QIcon(filepath)) if (not self.signalsBlocked()): self.filepathChanged.emit(filepath) def setFileTypes(self, fileTypes): """ Sets the filetypes for this button to the inputed types. :param fileTypes | <str> """ self._fileTypes = fileTypes @staticmethod def buildIcon(icon): """ Builds an icon from the inputed information. :param icon | <variant> """ if icon is None: return QIcon() if type(icon) == buffer: try: icon = QIcon(projexui.generatePixmap(icon)) except: icon = QIcon() else: try: icon = QIcon(icon) except: icon = QIcon() return icon x_filepath = qt.Property(str, filepath, setFilepath) x_fileTypes = qt.Property(str, fileTypes, setFileTypes)
class XLineEdit(QLineEdit): """ Creates a new QLineEdit that allows the user to define a grayed out text hint that will be drawn when there is no text assigned to the widget. """ __designer_icon__ = projexui.resources.find('img/ui/lineedit.png') textEntered = qt.Signal(str) InputFormat = enum('Normal', 'CamelHump', 'Underscore', 'Dash', 'ClassName', 'NoSpaces', 'Capitalize', 'Uppercase', 'Lowercase', 'Pretty') def __init__(self, *args): super(XLineEdit, self).__init__(*args) palette = self.palette() hint_clr = palette.color(palette.Disabled, palette.Text) # set the hint property self._hint = '' self._spacer = '_' self._hintColor = hint_clr self._cornerRadius = 0 self._inputFormat = XLineEdit.InputFormat.Normal self._selectAllOnFocus = False self._focusedIn = False self._useHintValue = False self._icon = QIcon() self._iconSize = QSize(14, 14) self._buttons = {} self.textChanged.connect(self.adjustText) self.returnPressed.connect(self.emitTextEntered) def adjustText(self): """ Updates the text based on the current format options. """ pos = self.cursorPosition() self.blockSignals(True) super(XLineEdit, self).setText(self.formatText(self.text())) self.setCursorPosition(pos) self.blockSignals(False) def addButton(self, button, alignment=None): """ Adds a button the edit. All the buttons will be layed out at the \ end of the widget. :param button | <QToolButton> alignment | <Qt.Alignment> :return <bool> | success """ if alignment == None: if button.pos().x() < self.pos().x(): alignment = Qt.AlignLeft else: alignment = Qt.AlignRight all_buttons = self.buttons() if button in all_buttons: return False # move the button to this edit button.setAutoRaise(True) button.setParent(self) button.setIconSize(self.iconSize()) button.setCursor(Qt.ArrowCursor) button.setFixedSize(QSize(self.height() - 2, self.height() - 2)) self._buttons.setdefault(alignment, []) self._buttons[alignment].append(button) self.adjustButtons() return True def adjustButtons(self): """ Adjusts the placement of the buttons for this line edit. """ y = 1 for btn in self.buttons(): btn.setIconSize(self.iconSize()) btn.setFixedSize(QSize(self.height() - 2, self.height() - 2)) # adjust the location for the left buttons left_buttons = self._buttons.get(Qt.AlignLeft, []) x = (self.cornerRadius() / 2.0) + 2 for btn in left_buttons: btn.move(x, y) x += btn.width() # adjust the location for the right buttons right_buttons = self._buttons.get(Qt.AlignRight, []) w = self.width() bwidth = sum([btn.width() for btn in right_buttons]) bwidth += (self.cornerRadius() / 2.0) + 1 for btn in right_buttons: btn.move(w - bwidth, y) bwidth -= btn.width() self.adjustTextMargins() def adjustTextMargins(self): """ Adjusts the margins for the text based on the contents to be displayed. """ left_buttons = self._buttons.get(Qt.AlignLeft, []) if left_buttons: bwidth = left_buttons[-1].pos().x() + left_buttons[-1].width() - 4 else: bwidth = 0 + (max(8, self.cornerRadius()) - 8) self.setTextMargins(bwidth, 0, 0, 0) def adjustStyleSheet(self): """ Adjusts the stylesheet for this widget based on whether it has a \ corner radius and/or icon. """ radius = self.cornerRadius() icon = self.icon() if not self.objectName(): self.setStyleSheet('') elif not (radius or icon): self.setStyleSheet('') else: palette = self.palette() options = {} options['corner_radius'] = radius options['padding'] = 5 options['objectName'] = self.objectName() if icon and not icon.isNull(): options['padding'] += self.iconSize().width() + 2 self.setStyleSheet(LINEEDIT_STYLE % options) def buttons(self): """ Returns all the buttons linked to this edit. :return [<QToolButton>, ..] """ all_buttons = [] for buttons in self._buttons.values(): all_buttons += buttons return all_buttons def cornerRadius(self): """ Returns the rounding radius for this widget's corner, allowing a \ developer to round the edges for a line edit on the fly. :return <int> """ return self._cornerRadius def currentText(self): """ Returns the text that is available currently, \ if the user has set standard text, then that \ is returned, otherwise the hint is returned. :return <str> """ text = str(self.text()) if (text): return text return self._hint def emitTextEntered(self): """ Emits the text entered signal for this line edit, provided the signals are not being blocked. """ if not self.signalsBlocked(): self.textEntered.emit(self.text()) def focusInEvent(self, event): """ Updates the focus in state for this edit. :param event | <QFocusEvent> """ super(XLineEdit, self).focusInEvent(event) self._focusedIn = True def focusOutEvent(self, event): """ Updates the focus in state for this edit. :param event | <QFocusEvent> """ super(XLineEdit, self).focusOutEvent(event) self._focusedIn = False def formatText(self, text): """ Formats the inputed text based on the input format assigned to this line edit. :param text | <str> :return <str> | frormatted text """ format = self.inputFormat() if format == XLineEdit.InputFormat.Normal: return text text = projex.text.toUtf8(text) if format == XLineEdit.InputFormat.CamelHump: return projex.text.camelHump(text) elif format == XLineEdit.InputFormat.Pretty: return projex.text.pretty(text) elif format == XLineEdit.InputFormat.Underscore: return projex.text.underscore(text) elif format == XLineEdit.InputFormat.Dash: return projex.text.dashed(text) elif format == XLineEdit.InputFormat.ClassName: return projex.text.classname(text) elif format == XLineEdit.InputFormat.NoSpaces: return projex.text.joinWords(text, self.spacer()) elif format == XLineEdit.InputFormat.Capitalize: return text.capitalize() elif format == XLineEdit.InputFormat.Uppercase: return text.upper() elif format == XLineEdit.InputFormat.Lowercase: return text.lower() return text def hint(self): """ Returns the hint value for this line edit. :return <str> """ return self._hint def hintColor(self): """ Returns the hint color for this text item. :return <QColor> """ return self._hintColor def icon(self): """ Returns the icon instance that is being used for this widget. :return <QIcon> || None """ return self._icon def iconSize(self): """ Returns the icon size that will be used for this widget. :return <QSize> """ return self._iconSize def inputFormat(self): """ Returns the input format for this widget. :return <int> """ return self._inputFormat def inputFormatText(self): """ Returns the input format as a text value for this widget. :return <str> """ return XLineEdit.InputFormat[self.inputFormat()] def mousePressEvent(self, event): """ Selects all the text if the property is set after this widget first gains focus. :param event | <QMouseEvent> """ super(XLineEdit, self).mousePressEvent(event) if self._focusedIn and self.selectAllOnFocus(): self.selectAll() self._focusedIn = False def paintEvent(self, event): """ Overloads the paint event to paint additional \ hint information if no text is set on the \ editor. :param event | <QPaintEvent> """ super(XLineEdit, self).paintEvent(event) # paint the hint text if not text is set if self.text() and not (self.icon() and not self.icon().isNull()): return # paint the hint text painter = QPainter(self) painter.setPen(self.hintColor()) icon = self.icon() left, top, right, bottom = self.getTextMargins() w = self.width() h = self.height() - 2 w -= (right + left) h -= (bottom + top) if icon and not icon.isNull(): size = icon.actualSize(self.iconSize()) x = 5 + left y = (self.height() - size.height()) / 2.0 painter.drawPixmap(x, y, icon.pixmap(size.width(), size.height())) w -= size.width() - 2 else: x = 6 + left w -= sum([btn.width() for btn in self.buttons()]) y = 2 + top # create the elided hint if not self.text() and self.hint(): rect = self.cursorRect() metrics = QFontMetrics(self.font()) hint = metrics.elidedText(self.hint(), Qt.ElideRight, w) align = self.alignment() if align & Qt.AlignHCenter: x = 0 else: x = rect.center().x() painter.drawText(x, y, w, h, align, hint) def resizeEvent(self, event): """ Overloads the resize event to handle updating of buttons. :param event | <QResizeEvent> """ super(XLineEdit, self).resizeEvent(event) self.adjustButtons() def selectAllOnFocus(self): """ Returns whether or not this edit will select all its contents on focus in. :return <bool> """ return self._selectAllOnFocus def setCornerRadius(self, radius): """ Sets the corner radius for this widget tot he inputed radius. :param radius | <int> """ self._cornerRadius = radius self.adjustStyleSheet() @qt.Slot(str) def setHint(self, hint): """ Sets the hint text to the inputed value. :param hint | <str> """ self._hint = self.formatText(hint) self.repaint() def setHintColor(self, clr): """ Sets the color for the hint for this edit. :param clr | <QColor> """ self._hintColor = clr def setIcon(self, icon): """ Sets the icon that will be used for this widget to the inputed icon. :param icon | <QIcon> || None """ self._icon = QIcon(icon) self.adjustStyleSheet() def setIconSize(self, size): """ Sets the icon size that will be used for this edit. :param size | <QSize> """ self._iconSize = size self.adjustTextMargins() def setInputFormat(self, inputFormat): """ Sets the input format for this text. :param inputFormat | <int> """ self._inputFormat = inputFormat def setInputFormatText(self, text): """ Sets the input format text for this widget to the given value. :param text | <str> """ try: self._inputFormat = XLineEdit.InputFormat[str(text)] except KeyError: pass def setObjectName(self, objectName): """ Updates the style sheet for this line edit when the name changes. :param objectName | <str> """ super(XLineEdit, self).setObjectName(objectName) self.adjustStyleSheet() def setSelectAllOnFocus(self, state): """ Returns whether or not this edit will select all its contents on focus in. :param state | <bool> """ self._selectAllOnFocus = state def setSpacer(self, spacer): """ Sets the spacer that will be used for this line edit when replacing NoSpaces input formats. :param spacer | <str> """ self._spacer = spacer def setUseHintValue(self, state): """ This method sets whether or not the value for this line edit should use the hint value if no text is found (within the projexui.xwidgetvalue plugin system). When set to True, the value returned will first look at the text of the widget, and if it is blank, will then return the hint value. If it is False, only the text value will be returned. :param state | <bool> """ self._useHintValue = state def setText(self, text): """ Sets the text for this widget to the inputed text, converting it based \ on the current input format if necessary. :param text | <str> """ if text is None: text = '' super(XLineEdit, self).setText(self.formatText(text)) def setVisible(self, state): """ Sets the visible state for this line edit. :param state | <bool> """ super(XLineEdit, self).setVisible(state) self.adjustStyleSheet() self.adjustTextMargins() def spacer(self): """ Returns the spacer that is used to replace spaces when the NoSpaces input format is used. :return <str> """ return self._spacer def useHintValue(self): """ This method returns whether or not the value for this line edit should use the hint value if no text is found (within the projexui.xwidgetvalue plugin system). When set to True, the value returned will first look at the text of the widget, and if it is blank, will then return the hint value. If it is False, only the text value will be returned. :return <bool> """ return self._useHintValue # create Qt properties x_hint = qt.Property(str, hint, setHint) x_icon = qt.Property('QIcon', icon, setIcon) x_iconSize = qt.Property(QSize, iconSize, setIconSize) x_hintColor = qt.Property('QColor', hintColor, setHintColor) x_cornerRadius = qt.Property(int, cornerRadius, setCornerRadius) x_inputFormatText = qt.Property(str, inputFormatText, setInputFormatText) x_spacer = qt.Property(str, spacer, setSpacer) x_selectAllOnFocus = qt.Property(bool, selectAllOnFocus, setSelectAllOnFocus) x_useHintValue = qt.Property(bool, useHintValue, setUseHintValue) # hack for qt setX_icon = setIcon
class XViewProfileManager(QWidget): currentProfileChanged = qt.Signal(qt.PyObject) optionsMenuRequested = qt.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 = qt.unwrapVariant(settings.value('current')) profiles = [] for prof_name in settings.childGroups(): settings.beginGroup(prof_name) prof_str = qt.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', qt.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 XViewProfileToolBar(XToolBar): profileCreated = qt.Signal(qt.PyObject) profileChanged = qt.Signal(qt.PyObject) profileRemoved = qt.Signal(qt.PyObject) currentProfileChanged = qt.Signal(qt.PyObject) def __init__(self, parent): super(XViewProfileToolBar, self).__init__(parent) # create custom properties self._editingEnabled = True self._viewWidget = None self._profileGroup = QActionGroup(self) # set the default options self.setIconSize(QSize(48, 48)) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.setContextMenuPolicy(Qt.CustomContextMenu) # create connections self.actionTriggered.connect(self.handleActionTrigger) self.customContextMenuRequested.connect(self.showProfileMenu) def addProfile(self, profile): """ Adds the inputed profile as an action to the toolbar. :param profile | <projexui.widgets.xviewwidget.XViewProfile> """ act = XViewProfileAction(profile, self) self._profileGroup.addAction(act) self.addAction(act) return act def currentProfile(self): """ Returns the current profile for this toolbar. :return <projexui.widgets.xviewwidget.XViewProfile> || None """ act = self._profileGroup.checkedAction() if (act): return act.profile() return None def createProfile(self, profile=None, clearLayout=True): """ Prompts the user to create a new profile. """ if (profile): prof = profile elif (not self.viewWidget() or clearLayout): prof = XViewProfile() else: prof = self.viewWidget().saveProfile() blocked = self.signalsBlocked() self.blockSignals(False) changed = self.editProfile(prof) self.blockSignals(blocked) if (not changed): return act = self.addProfile(prof) act.setChecked(True) # update the interface if (self.viewWidget() and (profile or clearLayout)): self.viewWidget().restoreProfile(prof) if (not self.signalsBlocked()): self.profileCreated.emit(prof) @qt.Slot(qt.PyObject) def editProfile(self, profile): """ Prompts the user to edit the given profile. :param profile | <projexui.widgets.xviewwidget.XViewProfile> """ mod = XViewProfileDialog.edit(self, profile) if (not mod): return False # update the action interface for act in self._profileGroup.actions(): if (act.profile() == profile): act.setProfile(profile) break # signal the change if (not self.signalsBlocked()): self.profileChanged.emit(profile) return True def exportProfiles(self, filename=None): """ Exports this toolbar to the given filename. :param filename | <str> || None """ if (not filename): filename = QFileDialog.getSaveFileName(self, 'Export Toolbar', '', 'Toolbar Files (*.xtool)') if (not filename): return False profile_xml = self.toXml() projex.text.xmlindent(profile_xml) profile_string = ElementTree.tostring(profile_xml) f = open(str(filename), 'w') f.write(profile_string) f.close() return True def handleActionTrigger(self, action): """ Handles when an action has been triggered. If the inputed action is a XViewProfileAction, then the currentProfileChanged signal will emit. :param action | <QAction> """ # trigger a particular profile if (isinstance(action, XViewProfileAction)): if (not self.signalsBlocked()): self.currentProfileChanged.emit(action.profile()) if (self._viewWidget): self._viewWidget.restoreProfile(action.profile()) def importProfiles(self, filename=None): """ Imports the profiles from the given filename. :param filename | <str> || None """ if (not filename): filename = QFileDialog.getOpenFileName(self, 'Import Toolbar', '', 'Toolbar Files (*.xtool)') if type(filename) == tuple: filename = str(filename[0]) if (not (filename and os.path.exists(filename))): return False f = open(str(filename), 'r') profile_string = f.read() f.close() self.loadString(profile_string) # load the default toolbar action = self._profileGroup.checkedAction() if (action): self.handleActionTrigger(action) def isEditingEnabled(self): """ Sets whether or not the create is enabled for this toolbar. :return <bool> """ return self._editingEnabled def isEmpty(self): """ Returns whether or not this toolbar is empty. :return <bool> """ return len(self._profileGroup.actions()) == 0 def loadString(self, profilestr): """ Loads the information for this toolbar from the inputed string. :param profilestr | <str> """ try: xtoolbar = ElementTree.fromstring(str(profilestr)) except ExpatError, e: return self.clear() curr = xtoolbar.get('current') for xprofile in xtoolbar: prof = XViewProfile.fromXml(xprofile) act = self.addProfile(prof) if (prof.name() == curr): act.setChecked(True)
class XOrbQueryContainer(QWidget): """ """ entriesUpdated = qt.Signal() enterCompoundRequested = qt.Signal(object, object) exitCompoundRequested = qt.Signal() def __init__(self, parent=None): super(XOrbQueryContainer, self).__init__(parent) # load the user interface projexui.loadUi(__file__, self) # define custom properties self._queryWidget = parent self._entryWidget = QWidget(self) self._currentJoiner = QueryCompound.Op.And layout = QVBoxLayout() layout.addStretch(1) layout.setSpacing(3) self._entryWidget.setLayout(layout) self.uiQueryAREA.setWidget(self._entryWidget) # set default properties self.setContextMenuPolicy(Qt.CustomContextMenu) # create connections (use old-style syntax or PySide errors) self.connect(self.uiBackBTN, SIGNAL('clicked()'), self.exitCompound) self.entriesUpdated.connect(self.refreshEntries) def addEntry(self, query=None, entry=None): if query is None: query = Query() layout = self._entryWidget.layout() index = layout.count() - 1 if entry: index = layout.indexOf(entry) + 1 widget = XOrbQueryEntryWidget(self, self.tableType()) layout.insertWidget(index, widget) widget.setQuery(query) if not self.signalsBlocked(): self.entriesUpdated.emit() return widget def checkedEntries(self): """ Returns the widgets that are checked for this widget. :return [<XOrbQueryEntryWidget>, ..] """ return [entry for entry in self.entries() if entry.isChecked()] def clear(self): """ Clears out the widgets for this query builder. """ layout = self._entryWidget.layout() for i in range(layout.count() - 1): widget = layout.itemAt(i).widget() widget.close() def createCompoundFromChecked(self): """ Creates a new compound query from the checked entry list. :return <orb.QueryCompound> """ checked_entries = self.checkedEntries() if len(checked_entries) <= 1: return QueryCompound() self.setUpdatesEnabled(False) joiner = self.currentJoiner() query = Query() for entry in checked_entries: if joiner == QueryCompound.Op.And: query &= entry.query() else: query |= entry.query() # clear out the existing containers first = checked_entries[0] first.setQuery(query) first.setChecked(False) layout = self._entryWidget.layout() for i in range(len(checked_entries) - 1, 0, -1): w = checked_entries[i] layout.takeAt(layout.indexOf(w)) w.close() self.refreshEntries() self.setUpdatesEnabled(True) if not self.signalsBlocked(): self.enterCompound(first, query) def currentJoiner(self): return self._currentJoiner def enterCompound(self, entry, query): # enter an existing compound if QueryCompound.typecheck(query): self.enterCompoundRequested.emit(entry, query) # create a new compound from the checked entries else: self.createCompoundFromChecked() def entries(self): """ Returns the entry widgets for this widget. :return [<XOrbQueryEntryWidget>, ..] """ layout = self._entryWidget.layout() output = [] for i in range(layout.count() - 1): widget = layout.itemAt(i).widget() if not isinstance(widget, XOrbQueryEntryWidget): continue output.append(widget) return output def exitCompound(self): self.exitCompoundRequested.emit() def isNull(self): """ Returns whether or not any widgets have been defined for this container yet. :return <bool> """ return self._entryWidget.layout().count() <= 1 def moveDown(self, entry): """ Moves the current query down one entry. """ if not entry: return entries = self.entries() next = entries[entries.index(entry) + 1] entry_q = entry.query() next_q = next.query() next.setQuery(entry_q) entry.setQuery(next_q) def moveUp(self, entry): """ Moves the current query down up one entry. """ if not entry: return entries = self.entries() next = entries[entries.index(entry) - 1] entry_q = entry.query() next_q = next.query() next.setQuery(entry_q) entry.setQuery(next_q) def pluginFactory(self): """ Returns the plugin factory for this widget. You can define a custom factory for handling specific columns or column types based on your table type. :return <XOrbQueryPluginFactory> """ return self._queryWidget.pluginFactory() def query(self): """ Returns the query that is defined by this current panel. :return <orb.Query> """ joiner = self.currentJoiner() query = Query() for entry in self.entries(): if joiner == QueryCompound.Op.And: query &= entry.query() else: query |= entry.query() query.setName(self.uiNameTXT.text()) return query def queryWidget(self): """ Returns the query widget linked with this container. :return <XOrbQueryWidget> """ return self._queryWidget def removeEntry(self, entry): if not entry: return layout = self._entryWidget.layout() if layout.count() == 2: print 'clearing query' entry.setQuery(Query()) return layout.takeAt(layout.indexOf(entry)) entry.close() if not self.signalsBlocked(): self.entriesUpdated.emit() def refreshEntries(self): layout = self._entryWidget.layout() for i in range(layout.count() - 1): widget = layout.itemAt(i).widget() widget.setFirst(i == 0) widget.setLast(i == (layout.count() - 2)) def setCurrentJoiner(self, joiner): self._currentJoiner = joiner layout = self._entryWidget.layout() for i in range(layout.count() - 1): widget = layout.itemAt(i).widget() widget.setJoiner(joiner) def setQuery(self, query): """ Sets the query for this wigdet to the inputed query instance. :param query | <orb.Query> || <orb.QueryCompound> """ if not self.isNull() and hash(query) == hash(self.query()): return # add entries table = self.tableType() self.setUpdatesEnabled(False) self.blockSignals(True) self.clear() if query is None or table is None: self.setEnabled(False) self.setUpdatesEnabled(True) self.blockSignals(False) return else: self.setEnabled(True) # load the queries for this item if QueryCompound.typecheck(query): queries = query.queries() self.setCurrentJoiner(query.operatorType()) else: queries = [query] self.uiNameTXT.setText(query.name()) layout = self._entryWidget.layout() for index, query in enumerate(queries): widget = self.addEntry(query) widget.setFirst(index == 0) widget.setLast(index == (len(queries) - 1)) widget.setJoiner(self.currentJoiner()) self.setUpdatesEnabled(True) self.blockSignals(False) def setShowBack(self, state): # check to see if we're working on the current query self.uiBackBTN.setVisible(state) self.uiNameTXT.setVisible(state) def tableType(self): """ Returns the table type instance for this widget. :return <subclass of orb.Table> """ return self._queryWidget.tableType()
class XTabWidget(QTabWidget): addRequested = qt.Signal(QPoint) optionsRequested = qt.Signal(QPoint) def __init__(self, *args): super(XTabWidget, self).__init__(*args) # create the tab bar self.setTabBar(XTabBar(self)) # create custom properties self._showAddButton = True self._showOptionsButton = True # create the add button self._addButton = QPushButton(self) self._addButton.setIcon(QIcon(resources.find('img/tab/add.png'))) self._addButton.setFixedSize(18, 18) self._addButton.setIconSize(QSize(10, 10)) # create the option button self._optionsButton = QPushButton(self) self._optionsButton.setFixedSize(22, 18) self._optionsButton.setIcon(QIcon(resources.find('img/tab/gear.png'))) self._optionsButton.setIconSize(QSize(10, 10)) # create connection self.connect(self.tabBar(), SIGNAL('currentChanged(int)'), self.adjustButtons) self.connect(self.tabBar(), SIGNAL('resized()'), self.adjustButtons) self.connect(self._optionsButton, SIGNAL('clicked()'), self.emitOptionsRequested) self.connect(self._addButton, SIGNAL('clicked()'), self.emitAddRequested) def __nonzero__(self): """ At somepoint, QTabWidget's nonzero became linked to whether it had children vs. whether it was none. This returns the original functionality. """ return self is not None def adjustButtons(self): """ Updates the position of the buttons based on the current geometry. """ tabbar = self.tabBar() tabbar.adjustSize() w = self.width() - self._optionsButton.width() - 2 self._optionsButton.move(w, 0) if self.count(): if self.currentIndex() == self.count() - 1: self._addButton.move(tabbar.width() - 2, -1) self._addButton.setFixedHeight(tabbar.height() + 2) else: self._addButton.move(tabbar.width() - 4, 1) self._addButton.setFixedHeight(tabbar.height()) else: self._addButton.move(tabbar.width() + 2, 1) self._addButton.stackUnder(self.currentWidget()) def addButton(self): """ Returns the add button linked with this tab widget. :return <QPushButton> """ return self._addButton def emitAddRequested(self, point=None): """ Emitted when the option menu button is clicked provided the signals \ are not being blocked for this widget. :param point | <QPoint> """ if self.signalsBlocked(): return if not point: point = QCursor.pos() self.addRequested.emit(point) def emitOptionsRequested(self, point=None): """ Emitted when the option menu button is clicked provided the signals \ are not being blocked for this widget. :param point | <QPoint> """ if self.signalsBlocked(): return if not point: point = QCursor.pos() self.optionsRequested.emit(point) def optionsButton(self): """ Returns the options button linked with this tab widget. :return <QPushButton> """ return self._optionsButton def paintEvent(self, event): if not self.count(): return super(XTabWidget, self).paintEvent(event) def resizeEvent(self, event): """ Updates the position of the additional buttons when this widget \ resizes. :param event | <QResizeEvet> """ super(XTabWidget, self).resizeEvent(event) self.adjustButtons() def setShowAddButton(self, state): """ Sets whether or not the add button is visible. :param state | <bool> """ self._showAddButton = state self._addButton.setVisible(state) def setShowOptionsButton(self, state): """ Sets whether or not the option button is visible. :param state | <bool> """ self._showOptionsButton = state self._optionsButton.setVisible(state) def showAddButton(self): """ Returns whether or not the add button is visible. :return <bool> """ return self._showAddButton def showOptionsButton(self): """ Returns whether or not the option button should be visible. :return <bool> """ return self._showOptionsButton x_showAddButton = qt.Property(bool, showAddButton, setShowAddButton) x_showOptionsButton = qt.Property(bool, showOptionsButton, setShowOptionsButton)
class XLoggerWidget(QTextEdit): __designer_icon__ = resources.find('img/log/info.png') LoggingMap = { logging.DEBUG: ('debug', resources.find('img/log/bug.png')), logging.INFO: ('info', resources.find('img/log/info.png')), logging.WARN: ('warning', resources.find('img/log/warning.png')), logging.ERROR: ('error', resources.find('img/log/error.png')), logging.CRITICAL: ('critical', resources.find('img/log/critical.png')), } messageLogged = qt.Signal(int, unicode) pythonMessageLogged = qt.Signal(int, unicode) """ Defines the main logger widget class. """ def __init__(self, parent): super(XLoggerWidget, self).__init__(parent) # set standard properties self.setReadOnly(True) self.setLineWrapMode(XLoggerWidget.NoWrap) # define custom properties self._logger = None self._clearOnClose = True self._handler = XLoggerHandler(self, True) self._currentMode = 'standard' self._blankCache = '' self._mutex = QMutex() # determine whether or not to use the light or dark configuration palette = self.palette() base = palette.color(palette.Base) avg = (base.red() + base.green() + base.blue()) / 3.0 if (avg < 160): colorSet = XLoggerColorSet.darkScheme() else: colorSet = XLoggerColorSet.lightScheme() self._colorSet = colorSet palette.setColor(palette.Text, colorSet.color('Standard')) palette.setColor(palette.Base, colorSet.color('Background')) self.setPalette(palette) # setup the levels self._loggingEnabled = { 'debug': True, 'info': True, 'warning': True, 'error': True, 'critical': True, 'fatal': True, } # create connections self.pythonMessageLogged.connect(self.log) def clearOnClose(self): """ Returns whether or not this widget should clear the link to its \ logger when it closes. :return <bool> """ return self._clearOnClose def closeEvent(self, event): """ Clear the handler from the logger when this widget closes. :param event | <QCloseEvent> """ if (self.clearOnClose()): self.setLogger(None) super(XLoggerWidget, self).closeEvent(event) def color(self, key): """ Returns the color value for the given key for this console. :param key | <unicode> :return <QColor> """ return self._colorSet.color(str(key).capitalize()) def colorSet(self): """ Returns the colors used for this console. :return <XLoggerColorSet> """ return self._colorSet def critical(self, msg): """ Logs a critical message to the console. :param msg | <unicode> """ self.log('critical', msg) def currentMode(self): """ Returns the current mode that the console is in for coloring. :return <unicode> """ return self._currentMode def debug(self, msg): """ Inserts a debug message to the current system. :param msg | <unicode> """ self.log('debug', msg) def error(self, msg): """ Inserts an error message to the current system. :param msg | <unicode> """ self.log('error', msg) def fatal(self, msg): """ Logs a fatal message to the system. :param msg | <unicode> """ self.log('fatal', msg) def handler(self): """ Returns the logging handler that is linked to this widget. :return <XLoggerHandler> """ return self._handler def information(self, msg): """ Inserts an information message to the current system. :param msg | <unicode> """ self.log('info', msg) def isLoggingEnabled(self, level): """ Returns whether or not logging is enabled for the given level. :param level | <int> """ if (type(level) == int): level = self.LoggingMap.get(level, ('info', ''))[0] return self._loggingEnabled.get(level, True) def log(self, level, msg): """ Logs the inputed message with the given level. :param level | <int> | logging level value msg | <unicode> :return <bool> success """ locker = QMutexLocker(self._mutex) if (not self.isLoggingEnabled(level)): return False msg = self._blankCache + projex.text.encoded(msg) if msg.endswith('\n'): self._blankCache = '\n' msg = msg[:-1] else: self._blankCache = '' self.setCurrentMode(level) self.insertPlainText(msg) if not self.signalsBlocked(): self.messageLogged.emit(level, msg) self.scrollToEnd() return True def logger(self): """ Returns the logger instance that this widget will monitor. :return <logging.Logger> """ return self._logger def scrollToEnd(self): """ Scrolls to the end for this console edit. """ vsbar = self.verticalScrollBar() vsbar.setValue(vsbar.maximum()) hbar = self.horizontalScrollBar() hbar.setValue(0) def setClearOnClose(self, state): """ Sets whether or not this widget should clear the logger link on close. :param state | <bool> """ self._clearOnClose = state def setColor(self, key, value): """ Sets the color value for the inputed color. :param key | <unicode> value | <QColor> """ key = str(key).capitalize() self._colorSet.setColor(key, value) # update the palette information if (key == 'Background'): palette = self.palette() palette.setColor(palette.Base, value) self.setPalette(palette) def setColorSet(self, colorSet): """ Sets the colors for this console to the inputed collection. :param colors | <XLoggerColorSet> """ self._colorSet = colorSet # update the palette information palette = self.palette() palette.setColor(palette.Text, colorSet.color('Standard')) palette.setColor(palette.Base, colorSet.color('Background')) self.setPalette(palette) def setCurrentMode(self, mode): """ Sets the current color mode for this console to the inputed value. :param mode | <unicode> """ if (type(mode) == int): mode = self.LoggingMap.get(mode, ('standard', ''))[0] if (mode == self._currentMode): return self._currentMode = mode color = self.color(mode) if (not color.isValid()): return format = QTextCharFormat() format.setForeground(color) self.setCurrentCharFormat(format) def setLoggingEnabled(self, level, state): """ Sets whether or not this widget should log the inputed level amount. :param level | <int> state | <bool> """ if (type(level) == int): level = self.LoggingMap.get(level, ('standard', ''))[0] self._loggingEnabled[level] = state def setLogger(self, logger): """ Sets the logger instance that this widget will monitor. :param logger | <logging.Logger> """ if (self._logger == logger): return if (self._logger): self._logger.removeHandler(self._handler) self._logger = logger if (logger): logger.addHandler(self._handler) def setShowDetails(self, state): """ Sets whether or not the level should be logged with the message in the \ output. :param state | <bool> """ self._handler.setShowDetails(state) def setShowLevel(self, state): """ Sets whether or not the level should be logged with the message in the \ output. :param state | <bool> """ self._handler.setShowLevel(state) def showDetails(self): """ Returns whether or not to show details for this logger. :return <bool> """ return self._handler.showDetails() def showLevel(self): """ Returns whether or not this logger should output the level when a \ message is printed. :return <bool> """ return self._handler.showLevel() def warning(self, msg): """ Logs a warning message to the system. :param msg | <unicode> """ self.log('warning', msg)
class XColorButton(QPushButton): """ The XColorButton class is a simple extension to the standard QPushButton that will control color settings. When teh user clicks on the button, the QColorDialog will be displayed, prompting the user to select a new color. Colors are stored internally can can be accessed by etter and setter methods, as well as the colorChanged signal. As the color is modified, either through code or by a user, the background color for the button will automatically update to match. == Example == |>>> from projexui.widgets.xcolorbutton import XColorButton |>>> import projexui | |>>> # create the widget |>>> btn = projexui.testWidget(XColorButton) | |>>> # click around, change the color |>>> from PyQt4.QtGui import QColor |>>> print btn.color().red(), btn.color().green(), btn.color().blue() |255 170 0 |>>> btn.setColor(QColor('red')) | |>>> # create connections |>>> def printColor(clr): print clr.red(), clr.green(), clr.blue() |>>> btn.colorChanged.connect(printColor) | |>>> # prompt the user to select a color for that button |>>> btn.pickColor() """ colorChanged = qt.Signal(QColor) def __init__(self, parent): super(XColorButton, self).__init__(parent) # initialize the color color = QColor('black') self._color = color palette = self.palette() palette.setColor(palette.Button, color) self.setPalette(palette) # create connections self.clicked.connect(self.pickColor) def color(self): """ Returns the color value for this button. :return <QColor> """ return self._color def pickColor(self): """ Prompts the user to select a color for this button. """ color = QColorDialog.getColor(self.color(), self) if (color.isValid()): self.setColor(color) def setColor(self, color): """ Sets the color value for this button to the given color. :param color | <QColor> """ self._color = color palette = self.palette() palette.setColor(palette.Button, color) self.setPalette(palette) if (not self.signalsBlocked()): self.colorChanged.emit(color) x_color = qt.Property(QColor, color, setColor)
class XLocationWidget(QWidget): locationChanged = qt.Signal(str) locationEdited = qt.Signal() def __init__(self, parent): super(XLocationWidget, self).__init__(parent) # define the interface self._locationEdit = XLineEdit(self) self._locationButton = QToolButton(self) self._urlTemplate = 'http://maps.google.com/maps?%(params)s' self._urlQueryKey = 'q' self._locationButton.setAutoRaise(True) self._locationButton.setIcon(QIcon(resources.find('img/map.png'))) self._locationEdit.setHint('no location set') layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._locationEdit) layout.addWidget(self._locationButton) self.setLayout(layout) # create connections self._locationEdit.textChanged.connect(self.locationChanged) self._locationEdit.textEdited.connect(self.locationEdited) self._locationButton.clicked.connect(self.browseMaps) def blockSignals(self, state): """ Blocks the signals for this widget and its sub-parts. :param state | <bool> """ super(XLocationWidget, self).blockSignals(state) self._locationEdit.blockSignals(state) self._locationButton.blockSignals(state) def browseMaps(self): """ Brings up a web browser with the address in a Google map. """ url = self.urlTemplate() params = urllib.urlencode({self.urlQueryKey(): self.location()}) url = url % {'params': params} webbrowser.open(url) def hint(self): """ Returns the hint associated with this widget. :return <str> """ return self._locationEdit.hint() def lineEdit(self): """ Returns the line edit linked with this widget. :return <XLineEdit> """ return self._locationEdit def location(self): """ Returns the current location from the edit. :return <str> """ return str(self._locationEdit.text()) @qt.Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._locationEdit.setHint(hint) @qt.Slot(str) def setLocation(self, location): """ Sets the location for this widget to the inputed location. :param location | <str> """ self._locationEdit.setText(str(location)) def setUrlQueryKey(self, key): """ Sets the key for the URL to the inputed key. :param key | <str> """ self._urlQueryKey = str(key) def setUrlTemplate(self, templ): """ Sets the URL path template that will be used when looking up locations on the web. :param templ | <str> """ self._urlQueryTemplate = str(templ) def urlQueryKey(self): """ Returns the query key that will be used for this location. :return <str> """ return self._urlQueryKey def urlTemplate(self): """ Returns the url template that will be used when mapping this location. :return <str> """ return self._urlTemplate x_hint = qt.Property(str, hint, setHint) x_location = qt.Property(str, location, setLocation) x_urlQueryKey = qt.Property(str, urlQueryKey, setUrlQueryKey) x_urlTemplate = qt.Property(str, urlTemplate, setUrlTemplate)
class XUrlWidget(QWidget): urlChanged = qt.Signal(str) urlEdited = qt.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() @qt.Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._urlEdit.setHint(hint) @qt.Slot(str) def setUrl(self, url): """ Sets the url for this widget to the inputed url. :param url | <str> """ self._urlEdit.setText(str(url)) def url(self): """ Returns the current url from the edit. :return <str> """ return str(self._urlEdit.text()) x_hint = qt.Property(str, hint, setHint) x_url = qt.Property(str, url, setUrl)
class XChart(QFrame): """ """ middleClicked = qt.Signal(QPoint) def __init__(self, parent=None): super(XChart, self).__init__(parent) # load the interface projexui.loadUi(__file__, self) # define custom properties self._renderer = None self._chartTitle = '' self._axes = [] self._datasets = [] self._horizontalAxis = None self._verticalAxis = None self._showDatasetToolbar = True self._showTypeButton = True self._dataChanged = False self._showGrid = True self._showRows = True self._showColumns = True self._showXAxis = True self._showYAxis = True # set default properties self.uiChartVIEW.setScene(XChartScene(self)) self.uiXAxisVIEW.setScene(XChartScene(self)) self.uiYAxisVIEW.setScene(XChartScene(self)) self.uiChartVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiXAxisVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiYAxisVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiDatasetTBAR.setFixedHeight(32) self.uiXAxisVIEW.hide() self.uiYAxisVIEW.hide() self.setAutoFillBackground(True) self.setBackgroundRole(QPalette.Base) self.setStyleSheet(STYLE) self.uiChartVIEW.setMouseTracking(True) self.uiDatasetTBAR.setStyleSheet(TOOLBAR_STYLE) # load renderers for renderer in XChartRenderer.plugins(): act = QAction('%s Chart' % renderer, self) ico = 'img/chart/%s.png' % renderer.lower() act.setIcon(QIcon(resources.find(ico))) self.uiTypeBTN.addAction(act) # assign the default renderer if not self.uiTypeBTN.defaultAction(): self._renderer = XChartRenderer.plugin(renderer) self.uiTypeBTN.setDefaultAction(act) # create connections chart_hbar = self.uiChartVIEW.horizontalScrollBar() chart_vbar = self.uiChartVIEW.verticalScrollBar() x_bar = self.uiXAxisVIEW.horizontalScrollBar() y_bar = self.uiYAxisVIEW.verticalScrollBar() chart_hbar.valueChanged.connect(self.syncScrollbars) chart_hbar.rangeChanged.connect(self.syncScrollbars) y_bar.valueChanged.connect(self.syncScrollbars) y_bar.rangeChanged.connect(self.syncScrollbars) self.uiTypeBTN.triggered.connect(self.assignRenderer) def _addDatasetAction(self, dataset): """ Adds an action for the inputed dataset to the toolbar :param dataset | <XChartDataset> """ # create the toolbar action action = QAction(dataset.name(), self) action.setIcon(XColorIcon(dataset.color())) action.setCheckable(True) action.setChecked(True) action.setData(qt.wrapVariant(dataset)) action.toggled.connect(self.toggleDataset) self.uiDatasetTBAR.addAction(action) def _drawBackground(self, scene, painter, rect): """ Draws the backgroud for a particular scene within the charts. :param scene | <XChartScene> painter | <QPainter> rect | <QRectF> """ rect = scene.sceneRect() if scene == self.uiChartVIEW.scene(): self.renderer().drawGrid(painter, rect, self.showGrid(), self.showColumns(), self.showRows()) elif scene == self.uiXAxisVIEW.scene(): self.renderer().drawAxis(painter, rect, self.horizontalAxis()) elif scene == self.uiYAxisVIEW.scene(): self.renderer().drawAxis(painter, rect, self.verticalAxis()) def addAxis(self, axis): """ Adds a new axis for this chart. Axis can define X & Y data including notch and value processing, as well as define individual lines within the chart that any chart items can reference or use. :param axis | <projexui.widgets.xchart.XChartAxis> """ self._axes.append(axis) def addDataset(self, dataset): """ Adds the given data set to this chart widget. :param dataSet | <XChartDataset> """ self._datasets.append(dataset) self._dataChanged = True self._addDatasetAction(dataset) def addToolbarWidget(self, widget): """ Adds a new widget to the toolbar layout for the chart. :param widget | <QWidget> """ self.uiToolbarHBOX.addWidget(widget) def assignRenderer(self, action): """ Assigns the renderer for this chart to the current selected renderer. """ name = str(action.text()).split(' ')[0] self._renderer = XChartRenderer.plugin(name) self.uiTypeBTN.setDefaultAction(action) self.recalculate() def axes(self): """ Returns all the axes that have been defined for this chart. :return [<projexui.widgets.xchart.XChartAxis>, ..] """ out = self._axes[:] if self._horizontalAxis: out.append(self._horizontalAxis) if self._verticalAxis: out.append(self._verticalAxis) return out def axis(self, name): """ Looks up an axis for this chart by the given name. :return <projexui.widgets.xchart.XChartAxis> || None """ for axis in self.axes(): if axis.name() == name: return axis return None def clear(self): """ Clears out all the dataset information from the chart. """ self.clearAxes() self.clearDatasets() def clearAxes(self): self._axes = [] self._verticalAxis = None self._horizontalAxis = None def clearDatasets(self): self._datasets = [] for act in self.uiDatasetTBAR.actions(): act.deleteLater() self.uiChartVIEW.scene().clear() def chartTitle(self): """ Returns the title for this plot. :return <str> """ return self._title def compareValues(self, a, b): """ Compares two values based on their values for this axis. :param a | <variant> b | <variant> """ values = self.values() try: return cmp(values.index(a), values.index(b)) except ValueError: return cmp(a, b) def datasets(self, visible=True): """ Returns a list of the data sets that are assigned with this chart widget. :param visible | <bool> :return [<XChartDataSet>, ..] """ if visible is not None: return filter(lambda x: x.isVisible(), self._datasets) return self._datasets[:] def showEvent(self, event): super(XChart, self).showEvent(event) self.recalculate() def findRenderer(self, name): """ Returns the renderer based on the inputed name. :return <str> """ return XChartRenderer.plugin(name) def horizontalAxis(self): """ Returns the axis that is assigned to the horizontal direction for this chart. :return <XChartAxis> """ return self._horizontalAxis def insertToolbarWidget(self, index, widget): """ Inserts a new widget to the toolbar layout for the chart. :param index | <int> widget | <QWidget> """ self.uiToolbarHBOX.insertWidget(index, widget) def pointAt(self, **axis_values): """ Returns the point on the chart where the inputed values are located. :return <QPointF> """ scene_point = self.renderer().pointAt(self.axes(), axis_values) chart_point = self.uiChartVIEW.mapFromScene(scene_point) return self.uiChartVIEW.mapToParent(chart_point) def mousePressEvent(self, event): if event.button() == Qt.MidButton: self.middleClicked.emit(event.pos()) super(XChart, self).mousePressEvent(event) def recalculate(self): """ Recalculates the information for this chart. """ if not (self.isVisible() and self.renderer()): return # update dynamic range if self._dataChanged: for axis in self.axes(): if axis.useDynamicRange(): axis.calculateRange(self.values(axis.name())) self._dataChanged = False # recalculate the main grid xaxis = self.horizontalAxis() yaxis = self.verticalAxis() renderer = self.renderer() xvisible = xaxis is not None and self.showXAxis( ) and renderer.showXAxis() yvisible = yaxis is not None and self.showYAxis( ) and renderer.showYAxis() self.uiXAxisVIEW.setVisible(xvisible) self.uiYAxisVIEW.setVisible(yvisible) # calculate the main view view = self.uiChartVIEW chart_scene = view.scene() chart_scene.setSceneRect(0, 0, view.width() - 2, view.height() - 2) rect = renderer.calculate(chart_scene, xaxis, yaxis) # recalculate the xaxis if xaxis and self.showXAxis() and renderer.showXAxis(): view = self.uiXAxisVIEW scene = view.scene() scene.setSceneRect(0, 0, rect.width(), view.height()) scene.invalidate() # render the yaxis if yaxis and self.showYAxis() and renderer.showYAxis(): view = self.uiYAxisVIEW scene = view.scene() scene.setSceneRect(0, 0, view.width(), rect.height()) scene.invalidate() # recalculate the items renderer.calculateDatasets(chart_scene, self.axes(), self.datasets()) chart_scene.invalidate() def removeAxis(self, axis): """ Removes an axis from this chart either by direct reference or by name. :param axis | <projexui.widgets.XChartAxis> || <str> """ if not isinstance(axis, XChartAxis): axis = self.axis(str(axis)) try: self._axes.remove(axis) except ValueError: pass def renderer(self): """ Returns the current renderer associated with this plot. :return <projexui.widgets.xchart.XChartRenderer> """ return self._renderer def renderers(self): """ Returns the renderer instances associated with this chart. :return [<XChartRenderer>, ..] """ return map(XChartRenderer.plugin, XChartRenderer.plugins()) def resizeEvent(self, event): """ Recalculates the chart information when the widget resizes. :param event | <QResizeEvent> """ super(XChart, self).resizeEvent(event) if self.isVisible(): self.recalculate() def restoreXml(self, xchart): """ Restores the xml information for this chart. :param xparent | <xml.etree.ElementTree.Element> """ if xchart is None: return self.setRenderer(xchart.get('renderer', 'Bar')) def saveXml(self, xchart): """ Saves the xml information for this chart to the inputed xml. :param xchart | <xml.etree.ElementTree.Element> """ if xchart is None: return xchart.set('renderer', self.renderer().name()) def setRenderer(self, renderer): """ Sets the current renderer associated with this plot. :param renderer | <XChartRenderer> || <str> :return <bool> | success """ if not isinstance(renderer, XChartRenderer): renderer = XChartRenderer.plugin(str(renderer)) if renderer is None: return False self._renderer = renderer for act in self.uiTypeBTN.actions(): if act.text() == '%s Chart' % renderer.name(): self.uiTypeBTN.setDefaultAction(act) break return True def setAxes(self, axes): """ Sets the axes for this chart to the inputed list of axes. :param axes | [<projexui.widgets.xchart.XChartAxis>, ..] """ self._axes = axes def setDatasets(self, datasets): """ Sets the dataset list for this chart to the inputed data. :param datasets | [<XChartDataset>, ..] """ self.clearDatasets() self._datasets = datasets for dataset in datasets: self._addDatasetAction(dataset) self._dataChanged = True self.recalculate() def setChartTitle(self, title): """ Sets the title for the plot to the inputed title. :param title | <str> """ self._chartTitle = title def setHorizontalAxis(self, axis): """ Sets the horizontal axis for this chart. :param axis | <XChartAxis> """ self._horizontalAxis = axis if axis: axis.setOrientation(Qt.Horizontal) self.uiXAxisVIEW.setFixedHeight(axis.minimumLabelHeight() + 5) self.uiXAxisVIEW.setVisible(axis is not None) def setVerticalAxis(self, axis): """ Sets the vertical axis for this chart. :param axis | <XChartAxis> """ self._verticalAxis = axis if axis: axis.setOrientation(Qt.Vertical) self.uiYAxisVIEW.setFixedWidth(axis.minimumLabelWidth() + 15) self.uiYAxisVIEW.setVisible(axis is not None) def setShowColumns(self, state): """ Sets the whether or not this renderer should draw the grid. :param state | <bool> """ self._showColumns = state def setShowDatasetToolbar(self, state): """ Sets whether or not the dataset toolbar is visible. :param state | <bool> """ self._showDatasetToolbar = state if not state: self.uiDatasetTBAR.hide() else: self.uiDatasetTBAR.show() def setShowGrid(self, state): """ Sets the whether or not this renderer should draw the grid. :param state | <bool> """ self._showGrid = state def setShowRows(self, state): """ Sets the whether or not this renderer should draw the grid. :param state | <bool> """ self._showRows = state def setShowTypeButton(self, state): """ Sets whether or not the type button is visible. :param state | <bool> """ self._showTypeButton = state if not state: self.uiTypeBTN.hide() else: self.uiTypeBTN.show() def setShowXAxis(self, state): """ Sets the whether or not this renderer should draw the x-axis. :param state | <bool> """ self._showXAxis = state def setShowYAxis(self, state): """ Sets the whether or not this renderer should draw the y-axis. :param state | <bool> """ self._showYAxis = state def showColumns(self): """ Returns whether or not this renderer should draw the grid. :return <bool> """ return self._showColumns and self.showXAxis() def showDatasetToolbar(self): """ Returns whether or not the dataset toolbar is visible. :return <bool> """ return self._showDatasetToolbar def showGrid(self): """ Returns whether or not this renderer should draw the grid. :return <bool> """ return self._showGrid def showRows(self): """ Returns whether or not this renderer should draw the grid. :return <bool> """ return self._showRows and self.showYAxis() def showTypeButton(self): """ Returns whether or not the type button is visible. :return <bool> """ return self._showTypeButton def showXAxis(self): """ Returns whether or not this renderer should draw the x-axis. :return <bool> """ return self._showXAxis def showYAxis(self): """ Returns whether or not this renderer should draw the y-axis. :return <bool> """ return self._showYAxis def sizeHint(self): """ Returns the size hint for this chart. """ return QSize(300, 300) def syncScrollbars(self): """ Synchronizes the various scrollbars within this chart. """ chart_hbar = self.uiChartVIEW.horizontalScrollBar() chart_vbar = self.uiChartVIEW.verticalScrollBar() x_hbar = self.uiXAxisVIEW.horizontalScrollBar() x_vbar = self.uiXAxisVIEW.verticalScrollBar() y_hbar = self.uiYAxisVIEW.horizontalScrollBar() y_vbar = self.uiYAxisVIEW.verticalScrollBar() x_hbar.setRange(chart_hbar.minimum(), chart_hbar.maximum()) x_hbar.setValue(chart_hbar.value()) x_vbar.setValue(0) chart_vbar.setRange(y_vbar.minimum(), y_vbar.maximum()) chart_vbar.setValue(y_vbar.value()) y_hbar.setValue(4) def toggleDataset(self, state, dataset=None): """ Toggles the dataset based on the given action or dataset. :param state | <bool> dataset | <XChartDataset> """ if dataset is None and self.sender(): dataset = qt.unwrapVariant(self.sender().data()) dataset.setVisible(state) self._dataChanged = True self.recalculate() def valueAt(self, point): """ Returns the value within the chart for the given point. :param point | <QPoint> :return {<str> axis name: <variant> value, ..} """ chart_point = self.uiChartVIEW.mapFromParent(point) scene_point = self.uiChartVIEW.mapToScene(chart_point) return self.renderer().valueAt(self.axes(), scene_point) def values(self, axis): """ Returns the values of the given axis from all the datasets within this chart. :param axis | <str> :return [<variant>, ..] """ output = [] for dataset in self.datasets(): output += dataset.values(axis) return output def verticalAxis(self): """ Returns the axis that is used for the vertical view for this graph. :return <XChartAxis> """ return self._verticalAxis x_showColumns = qt.Property(bool, showColumns, setShowColumns) x_showRows = qt.Property(bool, showRows, setShowRows) x_showGrid = qt.Property(bool, showGrid, setShowGrid) x_showYAxis = qt.Property(bool, showYAxis, setShowYAxis) x_showXAxis = qt.Property(bool, showXAxis, setShowXAxis) x_showDatasetToolbar = qt.Property(bool, showDatasetToolbar, setShowDatasetToolbar) x_showTypeButton = qt.Property(bool, showTypeButton, setShowTypeButton)