class XMenu(QMenu): def __init__(self, parent=None): super(XMenu, self).__init__(parent) # define custom parameters self._acceptedAction = None self._showTitle = True self._advancedMap = {} self._customData = {} self._titleHeight = 24 self._toolTipAction = None self._toolTipTimer = QTimer(self) self._toolTipTimer.setInterval(1000) self._toolTipTimer.setSingleShot(True) # set default parameters self.setContentsMargins(0, self._titleHeight, 0, 0) self.setShowTitle(False) # create connections self.hovered.connect(self.startActionToolTip) self.aboutToShow.connect(self.clearAcceptedAction) self._toolTipTimer.timeout.connect(self.showActionToolTip) def acceptAdvanced(self): self._acceptedAction = self.sender().defaultAction() self.close() def acceptedAction(self): return self._acceptedAction def addMenu(self, submenu): """ Adds a new submenu to this menu. Overloads the base QMenu addMenu \ method so that it will return an XMenu instance vs. a QMenu when \ creating a submenu by passing in a string. :param submenu | <str> || <QMenu> :return <QMenu> """ # create a new submenu based on a string input if not isinstance(submenu, QMenu): title = nativestring(submenu) submenu = XMenu(self) submenu.setTitle(title) submenu.setShowTitle(self.showTitle()) super(XMenu, self).addMenu(submenu) else: super(XMenu, self).addMenu(submenu) submenu.menuAction().setData(wrapVariant('menu')) return submenu def addSearchAction(self): """ Adds a search action that will allow the user to search through the actions and sub-actions within in this menu. :return <XSearchAction> """ action = XSearchAction(self) self.addAction(action) return action def addSection(self, section): """ Adds a section to this menu. A section will create a label for the menu to separate sections of the menu out. :param section | <str> """ label = QLabel(section, self) label.setMinimumHeight(self.titleHeight()) # setup font font = label.font() font.setBold(True) # setup palette palette = label.palette() palette.setColor(palette.WindowText, palette.color(palette.Mid)) # setup label label.setFont(font) label.setAutoFillBackground(True) label.setPalette(palette) # create the widget action action = QWidgetAction(self) action.setDefaultWidget(label) self.addAction(action) return action def adjustMinimumWidth( self ): """ Updates the minimum width for this menu based on the font metrics \ for its title (if its shown). This method is called automatically \ when the menu is shown. """ if not self.showTitle(): return metrics = QFontMetrics(self.font()) width = metrics.width(self.title()) + 20 if self.minimumWidth() < width: self.setMinimumWidth(width) def clearAdvancedActions( self ): """ Clears out the advanced action map. """ self._advancedMap.clear() margins = list(self.getContentsMargins()) margins[2] = 0 self.setContentsMargins(*margins) def clearAcceptedAction(self): self._acceptedAction = None def customData( self, key, default = None ): """ Returns data that has been stored on this menu. :param key | <str> default | <variant> :return <variant> """ key = nativestring(key) menu = self while (not key in menu._customData and \ isinstance(menu.parent(), XMenu)): menu = menu.parent() return menu._customData.get(nativestring(key), default) def paintEvent( self, event ): """ Overloads the paint event for this menu to draw its title based on its \ show title property. :param event | <QPaintEvent> """ super(XMenu, self).paintEvent(event) if self.showTitle(): with XPainter(self) as painter: palette = self.palette() painter.setBrush(palette.color(palette.Button)) painter.setPen(Qt.NoPen) painter.drawRect(1, 1, self.width() - 2, 22) painter.setBrush(Qt.NoBrush) painter.setPen(palette.color(palette.ButtonText)) painter.drawText(1, 1, self.width() - 2, 22, Qt.AlignCenter, self.title()) def rebuildButtons(self): """ Rebuilds the buttons for the advanced actions. """ for btn in self.findChildren(XAdvancedButton): btn.close() btn.setParent(None) btn.deleteLater() for standard, advanced in self._advancedMap.items(): rect = self.actionGeometry(standard) btn = XAdvancedButton(self) btn.setFixedWidth(22) btn.setFixedHeight(rect.height()) btn.setDefaultAction(advanced) btn.setAutoRaise(True) btn.move(rect.right() + 1, rect.top()) btn.show() if btn.icon().isNull(): btn.setIcon(QIcon(resources.find('img/advanced.png'))) btn.clicked.connect(self.acceptAdvanced) def setAdvancedAction(self, standardAction, advancedAction): """ Links an advanced action with the inputed standard action. This will \ create a tool button alongside the inputed standard action when the \ menu is displayed. If the user selects the advanced action, then the \ advancedAction.triggered signal will be emitted. :param standardAction | <QAction> advancedAction | <QAction> || None """ if advancedAction: self._advancedMap[standardAction] = advancedAction margins = list(self.getContentsMargins()) margins[2] = 22 self.setContentsMargins(*margins) elif standardAction in self._advancedMap: self._advancedMap.pop(standardAction) if not self._advancedMap: margins = list(self.getContentsMargins()) margins[2] = 22 self.setContentsMargins(*margins) def setCustomData( self, key, value ): """ Sets custom data for the developer on this menu instance. :param key | <str> value | <variant> """ self._customData[nativestring(key)] = value def setShowTitle( self, state ): """ Sets whether or not the title for this menu should be displayed in the \ popup. :param state | <bool> """ self._showTitle = state margins = list(self.getContentsMargins()) if state: margins[1] = self.titleHeight() else: margins[1] = 0 self.setContentsMargins(*margins) def showEvent(self, event): """ Overloads the set visible method to update the advanced action buttons \ to match their corresponding standard action location. :param state | <bool> """ super(XMenu, self).showEvent(event) self.adjustSize() self.adjustMinimumWidth() self.rebuildButtons() def setTitleHeight(self, height): """ Sets the height for the title of this menu bar and sections. :param height | <int> """ self._titleHeight = height def showActionToolTip(self): """ Shows the tool tip of the action that is currently being hovered over. :param action | <QAction> """ if ( not self.isVisible() ): return geom = self.actionGeometry(self._toolTipAction) pos = self.mapToGlobal(QPoint(geom.left(), geom.top())) pos.setY(pos.y() + geom.height()) tip = nativestring(self._toolTipAction.toolTip()).strip().strip('.') text = nativestring(self._toolTipAction.text()).strip().strip('.') # don't waste time showing the user what they already see if ( tip == text ): return QToolTip.showText(pos, self._toolTipAction.toolTip()) def showTitle( self ): """ Returns whether or not this menu should show the title in the popup. :return <bool> """ return self._showTitle def startActionToolTip( self, action ): """ Starts the timer to hover over an action for the current tool tip. :param action | <QAction> """ self._toolTipTimer.stop() QToolTip.hideText() if not action.toolTip(): return self._toolTipAction = action self._toolTipTimer.start() def titleHeight(self): """ Returns the height for the title of this menu bar and sections. :return <int> """ return self._titleHeight def updateCustomData( self, data ): """ Updates the custom data dictionary with the inputed data. :param data | <dict> """ if ( not data ): return self._customData.update(data) @staticmethod def fromString( parent, xmlstring, actions = None ): """ Loads the xml string as xml data and then calls the fromXml method. :param parent | <QWidget> xmlstring | <str> actions | {<str> name: <QAction>, .. } || None :return <XMenu> || None """ try: xdata = ElementTree.fromstring(xmlstring) except ExpatError, e: logger.exception(e) return None return XMenu.fromXml(parent, xdata, actions)
class XDockToolbar(QWidget): Position = enum('North', 'South', 'East', 'West') actionTriggered = Signal(object) actionMiddleTriggered = Signal(object) actionMenuRequested = Signal(object, QPoint) currentActionChanged = Signal(object) actionHovered = Signal(object) def __init__(self, parent=None): super(XDockToolbar, self).__init__(parent) # defines the position for this widget self._currentAction = -1 self._selectedAction = None self._padding = 8 self._position = XDockToolbar.Position.South self._minimumPixmapSize = QSize(16, 16) self._maximumPixmapSize = QSize(48, 48) self._hoverTimer = QTimer(self) self._hoverTimer.setSingleShot(True) self._hoverTimer.setInterval(1000) self._actionHeld = False self._easingCurve = QEasingCurve(QEasingCurve.InOutQuad) self._duration = 200 self._animating = False # install an event filter to update the location for this toolbar layout = QBoxLayout(QBoxLayout.LeftToRight) layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(0) layout.addStretch(1) layout.addStretch(1) self.setLayout(layout) self.setContentsMargins(2, 2, 2, 2) self.setMouseTracking(True) parent.window().installEventFilter(self) parent.window().statusBar().installEventFilter(self) self._hoverTimer.timeout.connect(self.emitActionHovered) def __markAnimatingFinished(self): self._animating = False def actionAt(self, pos): """ Returns the action at the given position. :param pos | <QPoint> :return <QAction> || None """ child = self.childAt(pos) if child: return child.action() return None def actionHeld(self): """ Returns whether or not the action will be held instead of closed on leaving. :return <bool> """ return self._actionHeld def actionLabels(self): """ Returns the labels for this widget. :return <XDockActionLabel> """ l = self.layout() return [l.itemAt(i).widget() for i in range(1, l.count() - 1)] def addAction(self, action): """ Adds the inputed action to this toolbar. :param action | <QAction> """ super(XDockToolbar, self).addAction(action) label = XDockActionLabel(action, self.minimumPixmapSize(), self) label.setPosition(self.position()) layout = self.layout() layout.insertWidget(layout.count() - 1, label) def clear(self): """ Clears out all the actions and items from this toolbar. """ # clear the actions from this widget for act in self.actions(): act.setParent(None) act.deleteLater() # clear the labels from this widget for lbl in self.actionLabels(): lbl.close() lbl.deleteLater() def currentAction(self): """ Returns the currently hovered/active action. :return <QAction> || None """ return self._currentAction def duration(self): """ Returns the duration value for the animation of the icons. :return <int> """ return self._duration def easingCurve(self): """ Returns the easing curve that will be used for the animation of animated icons for this dock bar. :return <QEasingCurve> """ return self._easingCurve def emitActionHovered(self): """ Emits a signal when an action is hovered. """ if not self.signalsBlocked(): self.actionHovered.emit(self.currentAction()) def eventFilter(self, object, event): """ Filters the parent objects events to rebuild this toolbar when the widget resizes. :param object | <QObject> event | <QEvent> """ if event.type() in (event.Move, event.Resize): if self.isVisible(): self.rebuild() elif object.isVisible(): self.setVisible(True) return False def holdAction(self): """ Returns whether or not the action should be held instead of clearing on leave. :return <bool> """ self._actionHeld = True def labelForAction(self, action): """ Returns the label that contains the inputed action. :return <XDockActionLabel> || None """ for label in self.actionLabels(): if label.action() == action: return label return None def leaveEvent(self, event): """ Clears the current action for this widget. :param event | <QEvent> """ super(XDockToolbar, self).leaveEvent(event) if not self.actionHeld(): self.setCurrentAction(None) def maximumPixmapSize(self): """ Returns the maximum pixmap size for this toolbar. :return <int> """ return self._maximumPixmapSize def minimumPixmapSize(self): """ Returns the minimum pixmap size that will be displayed to the user for the dock widget. :return <int> """ return self._minimumPixmapSize def mouseMoveEvent(self, event): """ Updates the labels for this dock toolbar. :param event | <XDockToolbar> """ # update the current label self.setCurrentAction(self.actionAt(event.pos())) def padding(self): """ Returns the padding value for this toolbar. :return <int> """ return self._padding def paintEvent(self, event): """ Paints the background for the dock toolbar. :param event | <QPaintEvent> """ x = 1 y = 1 w = self.width() h = self.height() clr_a = QColor(220, 220, 220) clr_b = QColor(190, 190, 190) grad = QLinearGradient() grad.setColorAt(0.0, clr_a) grad.setColorAt(0.6, clr_a) grad.setColorAt(1.0, clr_b) # adjust the coloring for the horizontal toolbar if self.position() & (self.Position.North | self.Position.South): h = self.minimumPixmapSize().height() + 6 if self.position() == self.Position.South: y = self.height() - h grad.setStart(0, y) grad.setFinalStop(0, self.height()) else: grad.setStart(0, 0) grad.setFinalStart(0, h) # adjust the coloring for the vertical toolbar if self.position() & (self.Position.East | self.Position.West): w = self.minimumPixmapSize().width() + 6 if self.position() == self.Position.West: x = self.width() - w grad.setStart(x, 0) grad.setFinalStop(self.width(), 0) else: grad.setStart(0, 0) grad.setFinalStop(w, 0) with XPainter(self) as painter: painter.fillRect(x, y, w, h, grad) # show the active action action = self.selectedAction() if action is not None and \ not self.currentAction() and \ not self._animating: for lbl in self.actionLabels(): if lbl.action() != action: continue geom = lbl.geometry() size = lbl.pixmapSize() if self.position() == self.Position.North: x = geom.left() y = 0 w = geom.width() h = size.height() + geom.top() + 2 elif self.position() == self.Position.East: x = 0 y = geom.top() w = size.width() + geom.left() + 2 h = geom.height() painter.setPen(QColor(140, 140, 40)) painter.setBrush(QColor(160, 160, 160)) painter.drawRect(x, y, w, h) break def position(self): """ Returns the position for this docktoolbar. :return <XDockToolbar.Position> """ return self._position def rebuild(self): """ Rebuilds the widget based on the position and current size/location of its parent. """ if not self.isVisible(): return self.raise_() max_size = self.maximumPixmapSize() min_size = self.minimumPixmapSize() widget = self.window() rect = widget.rect() rect.setBottom(rect.bottom() - widget.statusBar().height()) rect.setTop(widget.menuBar().height()) offset = self.padding() # align this widget to the north if self.position() == XDockToolbar.Position.North: self.move(rect.left(), rect.top()) self.resize(rect.width(), min_size.height() + offset) # align this widget to the east elif self.position() == XDockToolbar.Position.East: self.move(rect.left(), rect.top()) self.resize(min_size.width() + offset, rect.height()) # align this widget to the south elif self.position() == XDockToolbar.Position.South: self.move(rect.left(), rect.top() - min_size.height() - offset) self.resize(rect.width(), min_size.height() + offset) # align this widget to the west else: self.move(rect.right() - min_size.width() - offset, rect.top()) self.resize(min_size.width() + offset, rect.height()) def resizeToMinimum(self): """ Resizes the dock toolbar to the minimum sizes. """ offset = self.padding() min_size = self.minimumPixmapSize() if self.position() in (XDockToolbar.Position.East, XDockToolbar.Position.West): self.resize(min_size.width() + offset, self.height()) elif self.position() in (XDockToolbar.Position.North, XDockToolbar.Position.South): self.resize(self.width(), min_size.height() + offset) def selectedAction(self): """ Returns the action that was last selected. :return <QAction> """ return self._selectedAction def setActionHeld(self, state): """ Sets whether or not this action should be held before clearing on leaving. :param state | <bool> """ self._actionHeld = state def setCurrentAction(self, action): """ Sets the current action for this widget that highlights the size for this toolbar. :param action | <QAction> """ if action == self._currentAction: return self._currentAction = action self.currentActionChanged.emit(action) labels = self.actionLabels() anim_grp = QParallelAnimationGroup(self) max_size = self.maximumPixmapSize() min_size = self.minimumPixmapSize() if action: label = self.labelForAction(action) index = labels.index(label) # create the highlight effect palette = self.palette() effect = QGraphicsDropShadowEffect(label) effect.setXOffset(0) effect.setYOffset(0) effect.setBlurRadius(20) effect.setColor(QColor(40, 40, 40)) label.setGraphicsEffect(effect) offset = self.padding() if self.position() in (XDockToolbar.Position.East, XDockToolbar.Position.West): self.resize(max_size.width() + offset, self.height()) elif self.position() in (XDockToolbar.Position.North, XDockToolbar.Position.South): self.resize(self.width(), max_size.height() + offset) w = max_size.width() h = max_size.height() dw = (max_size.width() - min_size.width()) / 3 dh = (max_size.height() - min_size.height()) / 3 for i in range(4): before = index - i after = index + i if 0 <= before and before < len(labels): anim = XObjectAnimation(labels[before], 'setPixmapSize', anim_grp) anim.setEasingCurve(self.easingCurve()) anim.setStartValue(labels[before].pixmapSize()) anim.setEndValue(QSize(w, h)) anim.setDuration(self.duration()) anim_grp.addAnimation(anim) if i: labels[before].setGraphicsEffect(None) if after != before and 0 <= after and after < len(labels): anim = XObjectAnimation(labels[after], 'setPixmapSize', anim_grp) anim.setEasingCurve(self.easingCurve()) anim.setStartValue(labels[after].pixmapSize()) anim.setEndValue(QSize(w, h)) anim.setDuration(self.duration()) anim_grp.addAnimation(anim) if i: labels[after].setGraphicsEffect(None) w -= dw h -= dh else: offset = self.padding() for label in self.actionLabels(): # clear the graphics effect label.setGraphicsEffect(None) # create the animation anim = XObjectAnimation(label, 'setPixmapSize', self) anim.setEasingCurve(self.easingCurve()) anim.setStartValue(label.pixmapSize()) anim.setEndValue(min_size) anim.setDuration(self.duration()) anim_grp.addAnimation(anim) anim_grp.finished.connect(self.resizeToMinimum) anim_grp.start() self._animating = True anim_grp.finished.connect(anim_grp.deleteLater) anim_grp.finished.connect(self.__markAnimatingFinished) if self._currentAction: self._hoverTimer.start() else: self._hoverTimer.stop() def setDuration(self, duration): """ Sets the duration value for the animation of the icon. :param duration | <int> """ self._duration = duration def setEasingCurve(self, curve): """ Sets the easing curve for this toolbar to the inputed curve. :param curve | <QEasingCurve> """ self._easingCurve = QEasingCurve(curve) def setMaximumPixmapSize(self, size): """ Sets the maximum pixmap size for this toolbar. :param size | <int> """ self._maximumPixmapSize = size position = self.position() self._position = None self.setPosition(position) def setMinimumPixmapSize(self, size): """ Sets the minimum pixmap size that will be displayed to the user for the dock widget. :param size | <int> """ self._minimumPixmapSize = size position = self.position() self._position = None self.setPosition(position) def setPadding(self, padding): """ Sets the padding amount for this toolbar. :param padding | <int> """ self._padding = padding def setPosition(self, position): """ Sets the position for this widget and its parent. :param position | <XDockToolbar.Position> """ if position == self._position: return self._position = position widget = self.window() layout = self.layout() offset = self.padding() min_size = self.minimumPixmapSize() # set the layout to north if position == XDockToolbar.Position.North: self.move(0, 0) widget.setContentsMargins(0, min_size.height() + offset, 0, 0) layout.setDirection(QBoxLayout.LeftToRight) # set the layout to east elif position == XDockToolbar.Position.East: self.move(0, 0) widget.setContentsMargins(min_size.width() + offset, 0, 0, 0) layout.setDirection(QBoxLayout.TopToBottom) # set the layout to the south elif position == XDockToolbar.Position.South: widget.setContentsMargins(0, 0, 0, min_size.height() + offset) layout.setDirection(QBoxLayout.LeftToRight) # set the layout to the west else: widget.setContentsMargins(0, 0, min_size.width() + offset, 0) layout.setDirection(QBoxLayout.TopToBottom) # update the label alignments for label in self.actionLabels(): label.setPosition(position) # rebuilds the widget self.rebuild() self.update() def setSelectedAction(self, action): """ Sets the selected action instance for this toolbar. :param action | <QAction> """ self._hoverTimer.stop() self._selectedAction = action def setVisible(self, state): """ Sets whether or not this toolbar is visible. If shown, it will rebuild. :param state | <bool> """ super(XDockToolbar, self).setVisible(state) if state: self.rebuild() self.setCurrentAction(None) def unholdAction(self): """ Unholds the action from being blocked on the leave event. """ self._actionHeld = False point = self.mapFromGlobal(QCursor.pos()) self.setCurrentAction(self.actionAt(point)) def visualRect(self, action): """ Returns the visual rect for the inputed action, or a blank QRect if no matching action was found. :param action | <QAction> :return <QRect> """ for widget in self.actionLabels(): if widget.action() == action: return widget.geometry() return QRect()
class XOrbRecordBox(XComboBox): __designer_group__ = 'ProjexUI - ORB' """ Defines a combo box that contains records from the ORB system. """ loadRequested = Signal(object) loadingStarted = Signal() loadingFinished = Signal() currentRecordChanged = Signal(object) currentRecordEdited = Signal(object) initialized = Signal() def __init__(self, parent=None): # needs to be defined before the base class is initialized or the # event filter won't work self._treePopupWidget = None super(XOrbRecordBox, self).__init__( parent ) # define custom properties self._currentRecord = None # only used while loading self._changedRecord = -1 self._tableTypeName = '' self._tableLookupIndex = '' self._baseHints = ('', '') self._batchSize = 100 self._tableType = None self._order = None self._query = None self._iconMapper = None self._labelMapper = nstr self._required = True self._loaded = False self._showTreePopup = False self._autoInitialize = False self._threadEnabled = True self._specifiedColumns = None self._specifiedColumnsOnly = False # define an editing timer self._editedTimer = QTimer(self) self._editedTimer.setSingleShot(True) self._editedTimer.setInterval(500) # create threading options self._worker = None self._workerThread = None # create connections edit = self.lineEdit() if edit: edit.textEntered.connect(self.assignCurrentRecord) edit.editingFinished.connect(self.emitCurrentRecordEdited) edit.returnPressed.connect(self.emitCurrentRecordEdited) self.currentIndexChanged.connect(self.emitCurrentRecordChanged) self.currentIndexChanged.connect(self.startEditTimer) self._editedTimer.timeout.connect(self.emitCurrentRecordEdited) QApplication.instance().aboutToQuit.connect(self._cleanupWorker) def _cleanupWorker(self): if not self._workerThread: return thread = self._workerThread worker = self._worker self._workerThread = None self._worker = None worker.deleteLater() thread.finished.connect(thread.deleteLater) thread.quit() thread.wait() def addRecord(self, record): """ Adds the given record to the system. :param record | <str> """ label_mapper = self.labelMapper() icon_mapper = self.iconMapper() self.addItem(label_mapper(record)) self.setItemData(self.count() - 1, wrapVariant(record), Qt.UserRole) # load icon if icon_mapper: self.setItemIcon(self.count() - 1, icon_mapper(record)) if self.showTreePopup(): XOrbRecordItem(self.treePopupWidget(), record) def addRecords(self, records): """ Adds the given record to the system. :param records | [<orb.Table>, ..] """ label_mapper = self.labelMapper() icon_mapper = self.iconMapper() # create the items to display tree = None if self.showTreePopup(): tree = self.treePopupWidget() tree.blockSignals(True) tree.setUpdatesEnabled(False) # add the items to the list start = self.count() self.addItems(map(label_mapper, records)) # update the item information for i, record in enumerate(records): index = start + i self.setItemData(index, wrapVariant(record), Qt.UserRole) if icon_mapper: self.setItemIcon(index, icon_mapper(record)) if tree: XOrbRecordItem(tree, record) if tree: tree.blockSignals(False) tree.setUpdatesEnabled(True) def addRecordsFromThread(self, records): """ Adds the given record to the system. :param records | [<orb.Table>, ..] """ label_mapper = self.labelMapper() icon_mapper = self.iconMapper() tree = None if self.showTreePopup(): tree = self.treePopupWidget() # add the items to the list start = self.count() # update the item information blocked = self.signalsBlocked() self.blockSignals(True) for i, record in enumerate(records): index = start + i self.addItem(label_mapper(record)) self.setItemData(index, wrapVariant(record), Qt.UserRole) if icon_mapper: self.setItemIcon(index, icon_mapper(record)) if record == self._currentRecord: self.setCurrentIndex(self.count() - 1) if tree: XOrbRecordItem(tree, record) self.blockSignals(blocked) def acceptRecord(self, item): """ Closes the tree popup and sets the current record. :param record | <orb.Table> """ record = item.record() self.treePopupWidget().close() self.setCurrentRecord(record) def assignCurrentRecord(self, text): """ Assigns the current record from the inputed text. :param text | <str> """ if self.showTreePopup(): item = self._treePopupWidget.currentItem() if item: self._currentRecord = item.record() else: self._currentRecord = None return # look up the record for the given text if text: index = self.findText(text) elif self.isRequired(): index = 0 else: index = -1 # determine new record to look for record = self.recordAt(index) if record == self._currentRecord: return # set the current index and record for any changes self._currentRecord = record self.setCurrentIndex(index) def autoInitialize(self): """ Returns whether or not this record box should auto-initialize its records. :return <bool> """ return self._autoInitialize def batchSize(self): """ Returns the batch size to use when processing this record box's list of entries. :return <int> """ return self._batchSize def checkedRecords( self ): """ Returns a list of the checked records from this combo box. :return [<orb.Table>, ..] """ indexes = self.checkedIndexes() return map(self.recordAt, indexes) def currentRecord( self ): """ Returns the record found at the current index for this combo box. :rerturn <orb.Table> || None """ if self._currentRecord is None and self.isRequired(): self._currentRecord = self.recordAt(self.currentIndex()) return self._currentRecord def dragEnterEvent(self, event): """ Listens for query's being dragged and dropped onto this tree. :param event | <QDragEnterEvent> """ data = event.mimeData() if data.hasFormat('application/x-orb-table') and \ data.hasFormat('application/x-orb-query'): tableName = self.tableTypeName() if nstr(data.data('application/x-orb-table')) == tableName: event.acceptProposedAction() return elif data.hasFormat('application/x-orb-records'): event.acceptProposedAction() return super(XOrbRecordBox, self).dragEnterEvent(event) def dragMoveEvent(self, event): """ Listens for query's being dragged and dropped onto this tree. :param event | <QDragEnterEvent> """ data = event.mimeData() if data.hasFormat('application/x-orb-table') and \ data.hasFormat('application/x-orb-query'): tableName = self.tableTypeName() if nstr(data.data('application/x-orb-table')) == tableName: event.acceptProposedAction() return elif data.hasFormat('application/x-orb-records'): event.acceptProposedAction() return super(XOrbRecordBox, self).dragMoveEvent(event) def dropEvent(self, event): """ Listens for query's being dragged and dropped onto this tree. :param event | <QDropEvent> """ # overload the current filtering options data = event.mimeData() if data.hasFormat('application/x-orb-table') and \ data.hasFormat('application/x-orb-query'): tableName = self.tableTypeName() if nstr(data.data('application/x-orb-table')) == tableName: data = nstr(data.data('application/x-orb-query')) query = Q.fromXmlString(data) self.setQuery(query) return elif self.tableType() and data.hasFormat('application/x-orb-records'): from projexui.widgets.xorbtreewidget import XOrbTreeWidget records = XOrbTreeWidget.dataRestoreRecords(data) for record in records: if isinstance(record, self.tableType()): self.setCurrentRecord(record) return super(XOrbRecordBox, self).dropEvent(event) def emitCurrentRecordChanged(self): """ Emits the current record changed signal for this combobox, provided \ the signals aren't blocked. """ record = unwrapVariant(self.itemData(self.currentIndex(), Qt.UserRole)) if not Table.recordcheck(record): record = None self._currentRecord = record if not self.signalsBlocked(): self._changedRecord = record self.currentRecordChanged.emit(record) def emitCurrentRecordEdited(self): """ Emits the current record edited signal for this combobox, provided the signals aren't blocked and the record has changed since the last time. """ if self._changedRecord == -1: return if self.signalsBlocked(): return record = self._changedRecord self._changedRecord = -1 self.currentRecordEdited.emit(record) def eventFilter(self, object, event): """ Filters events for the popup tree widget. :param object | <QObject> event | <QEvent> :retuen <bool> | consumed """ edit = self.lineEdit() if not (object and object == self._treePopupWidget): return super(XOrbRecordBox, self).eventFilter(object, event) elif event.type() == event.Show: object.resizeToContents() object.horizontalScrollBar().setValue(0) elif edit and event.type() == event.KeyPress: # accept lookup if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab, Qt.Key_Backtab): item = object.currentItem() text = edit.text() if not text: record = None item = None elif isinstance(item, XOrbRecordItem): record = item.record() if record and item.isSelected() and not item.isHidden(): self.hidePopup() self.setCurrentRecord(record) event.accept() return True else: self.setCurrentRecord(None) self.hidePopup() edit.setText(text) edit.keyPressEvent(event) event.accept() return True # cancel lookup elif event.key() == Qt.Key_Escape: text = edit.text() self.setCurrentRecord(None) edit.setText(text) self.hidePopup() event.accept() return True # update the search info else: edit.keyPressEvent(event) elif edit and event.type() == event.KeyRelease: edit.keyReleaseEvent(event) elif edit and event.type() == event.MouseButtonPress: local_pos = object.mapFromGlobal(event.globalPos()) in_widget = object.rect().contains(local_pos) if not in_widget: text = edit.text() self.setCurrentRecord(None) edit.setText(text) self.hidePopup() event.accept() return True return super(XOrbRecordBox, self).eventFilter(object, event) def focusNextChild(self, event): edit = self.lineEdit() if not self.isLoading() and edit: self.assignCurrentRecord(edit.text()) return super(XOrbRecordBox, self).focusNextChild(event) def focusNextPrevChild(self, event): edit = self.lineEdit() if not self.isLoading() and edit: self.assignCurrentRecord(edit.text()) return super(XOrbRecordBox, self).focusNextPrevChild(event) def focusInEvent(self, event): """ When this widget loses focus, try to emit the record changed event signal. """ self._changedRecord = -1 super(XOrbRecordBox, self).focusInEvent(event) def focusOutEvent(self, event): """ When this widget loses focus, try to emit the record changed event signal. """ edit = self.lineEdit() if not self.isLoading() and edit: self.assignCurrentRecord(edit.text()) super(XOrbRecordBox, self).focusOutEvent(event) def hidePopup(self): """ Overloads the hide popup method to handle when the user hides the popup widget. """ if self._treePopupWidget and self.showTreePopup(): self._treePopupWidget.close() super(XOrbRecordBox, self).hidePopup() def iconMapper( self ): """ Returns the icon mapping method to be used for this combobox. :return <method> || None """ return self._iconMapper def isLoading(self): """ Returns whether or not this combobox is loading records. :return <bool> """ try: return self._worker.isRunning() except AttributeError: return False def isRequired( self ): """ Returns whether or not this combo box requires the user to pick a selection. :return <bool> """ return self._required def isThreadEnabled(self): """ Returns whether or not threading is enabled for this combo box. :return <bool> """ return self._threadEnabled def labelMapper( self ): """ Returns the label mapping method to be used for this combobox. :return <method> || None """ return self._labelMapper @Slot(object) def lookupRecords(self, record): """ Lookups records based on the inputed record. This will use the tableLookupIndex property to determine the Orb Index method to use to look up records. That index method should take the inputed record as an argument, and return a list of records. :param record | <orb.Table> """ table_type = self.tableType() if not table_type: return index = getattr(table_type, self.tableLookupIndex(), None) if not index: return self.setRecords(index(record)) def markLoadingStarted(self): """ Marks this widget as loading records. """ if self.isThreadEnabled(): XLoaderWidget.start(self) if self.showTreePopup(): tree = self.treePopupWidget() tree.setCursor(Qt.WaitCursor) tree.clear() tree.setUpdatesEnabled(False) tree.blockSignals(True) self._baseHints = (self.hint(), tree.hint()) tree.setHint('Loading records...') self.setHint('Loading records...') else: self._baseHints = (self.hint(), '') self.setHint('Loading records...') self.setCursor(Qt.WaitCursor) self.blockSignals(True) self.setUpdatesEnabled(False) # prepare to load self.clear() use_dummy = not self.isRequired() or self.isCheckable() if use_dummy: self.addItem('') self.loadingStarted.emit() def markLoadingFinished(self): """ Marks this widget as finished loading records. """ XLoaderWidget.stop(self, force=True) hint, tree_hint = self._baseHints self.setHint(hint) # set the tree widget if self.showTreePopup(): tree = self.treePopupWidget() tree.setHint(tree_hint) tree.unsetCursor() tree.setUpdatesEnabled(True) tree.blockSignals(False) self.unsetCursor() self.blockSignals(False) self.setUpdatesEnabled(True) self.loadingFinished.emit() def order(self): """ Returns the ordering for this widget. :return [(<str> column, <str> asc|desc, ..] || None """ return self._order def query( self ): """ Returns the query used when querying the database for the records. :return <Query> || None """ return self._query def records( self ): """ Returns the record list that ist linked with this combo box. :return [<orb.Table>, ..] """ records = [] for i in range(self.count()): record = self.recordAt(i) if record: records.append(record) return records def recordAt(self, index): """ Returns the record at the inputed index. :return <orb.Table> || None """ return unwrapVariant(self.itemData(index, Qt.UserRole)) def refresh(self, records): """ Refreshs the current user interface to match the latest settings. """ self._loaded = True if self.isLoading(): return # load the information if RecordSet.typecheck(records): table = records.table() self.setTableType(table) if self.order(): records.setOrder(self.order()) # load specific data for this record box if self.specifiedColumnsOnly(): records.setColumns(map(lambda x: x.name(), self.specifiedColumns())) # load the records asynchronously if self.isThreadEnabled() and table: try: thread_enabled = table.getDatabase().isThreadEnabled() except AttributeError: thread_enabled = False if thread_enabled: # ensure we have a worker thread running self.worker() # assign ordering based on tree table if self.showTreePopup(): tree = self.treePopupWidget() if tree.isSortingEnabled(): col = tree.sortColumn() colname = tree.headerItem().text(col) column = table.schema().column(colname) if column: if tree.sortOrder() == Qt.AscendingOrder: sort_order = 'asc' else: sort_order = 'desc' records.setOrder([(column.name(), sort_order)]) self.loadRequested.emit(records) return # load the records synchronously self.loadingStarted.emit() curr_record = self.currentRecord() self.blockSignals(True) self.setUpdatesEnabled(False) self.clear() use_dummy = not self.isRequired() or self.isCheckable() if use_dummy: self.addItem('') self.addRecords(records) self.setUpdatesEnabled(True) self.blockSignals(False) self.setCurrentRecord(curr_record) self.loadingFinished.emit() def setAutoInitialize(self, state): """ Sets whether or not this combo box should auto initialize itself when it is shown. :param state | <bool> """ self._autoInitialize = state def setBatchSize(self, size): """ Sets the batch size of records to look up for this record box. :param size | <int> """ self._batchSize = size try: self._worker.setBatchSize(size) except AttributeError: pass def setCheckedRecords( self, records ): """ Sets the checked off records to the list of inputed records. :param records | [<orb.Table>, ..] """ QApplication.sendPostedEvents(self, -1) indexes = [] for i in range(self.count()): record = self.recordAt(i) if record is not None and record in records: indexes.append(i) self.setCheckedIndexes(indexes) def setCurrentRecord(self, record, autoAdd=False): """ Sets the index for this combobox to the inputed record instance. :param record <orb.Table> :return <bool> success """ if record is not None and not Table.recordcheck(record): return False # don't reassign the current record # clear the record if record is None: self._currentRecord = None blocked = self.signalsBlocked() self.blockSignals(True) self.setCurrentIndex(-1) self.blockSignals(blocked) if not blocked: self.currentRecordChanged.emit(None) return True elif record == self.currentRecord(): return False self._currentRecord = record found = False blocked = self.signalsBlocked() self.blockSignals(True) for i in range(self.count()): stored = unwrapVariant(self.itemData(i, Qt.UserRole)) if stored == record: self.setCurrentIndex(i) found = True break if not found and autoAdd: self.addRecord(record) self.setCurrentIndex(self.count() - 1) self.blockSignals(blocked) if not blocked: self.currentRecordChanged.emit(record) return False def setIconMapper( self, mapper ): """ Sets the icon mapping method for this combobox to the inputed mapper. \ The inputed mapper method should take a orb.Table instance as input \ and return a QIcon as output. :param mapper | <method> || None """ self._iconMapper = mapper def setLabelMapper( self, mapper ): """ Sets the label mapping method for this combobox to the inputed mapper.\ The inputed mapper method should take a orb.Table instance as input \ and return a string as output. :param mapper | <method> """ self._labelMapper = mapper def setOrder(self, order): """ Sets the order for this combo box to the inputed order. This will be used in conjunction with the query when loading records to the combobox. :param order | [(<str> column, <str> asc|desc), ..] || None """ self._order = order def setQuery(self, query, autoRefresh=True): """ Sets the query for this record box for generating records. :param query | <Query> || None """ self._query = query tableType = self.tableType() if not tableType: return False if autoRefresh: self.refresh(tableType.select(where = query)) return True def setRecords(self, records): """ Sets the records on this combobox to the inputed record list. :param records | [<orb.Table>, ..] """ self.refresh(records) def setRequired( self, state ): """ Sets the required state for this combo box. If the column is not required, a blank record will be included with the choices. :param state | <bool> """ self._required = state def setShowTreePopup(self, state): """ Sets whether or not to use an ORB tree widget in the popup for this record box. :param state | <bool> """ self._showTreePopup = state def setSpecifiedColumns(self, columns): """ Sets the specified columns for this combobox widget. :param columns | [<orb.Column>, ..] || [<str>, ..] || None """ self._specifiedColumns = columns self._specifiedColumnsOnly = columns is not None def setSpecifiedColumnsOnly(self, state): """ Sets whether or not only specified columns should be loaded for this record box. :param state | <bool> """ self._specifiedColumnsOnly = state def setTableLookupIndex(self, index): """ Sets the name of the index method that will be used to lookup records for this combo box. :param index | <str> """ self._tableLookupIndex = nstr(index) def setTableType( self, tableType ): """ Sets the table type for this record box to the inputed table type. :param tableType | <orb.Table> """ self._tableType = tableType if tableType: self._tableTypeName = tableType.schema().name() else: self._tableTypeName = '' def setTableTypeName(self, name): """ Sets the table type name for this record box to the inputed name. :param name | <str> """ self._tableTypeName = nstr(name) self._tableType = None def setThreadEnabled(self, state): """ Sets whether or not threading should be enabled for this widget. Actual threading will be determined by both this property, and whether or not the active ORB backend supports threading. :param state | <bool> """ self._threadEnabled = state def setVisible(self, state): """ Sets the visibility for this record box. :param state | <bool> """ super(XOrbRecordBox, self).setVisible(state) if state and not self._loaded: if self.autoInitialize(): table = self.tableType() if not table: return self.setRecords(table.select(where=self.query())) else: self.initialized.emit() def showPopup(self): """ Overloads the popup method from QComboBox to display an ORB tree widget when necessary. :sa setShowTreePopup """ if not self.showTreePopup(): return super(XOrbRecordBox, self).showPopup() tree = self.treePopupWidget() if tree and not tree.isVisible(): tree.move(self.mapToGlobal(QPoint(0, self.height()))) tree.resize(self.width(), 250) tree.resizeToContents() tree.filterItems('') tree.setFilteredColumns(range(tree.columnCount())) tree.show() def showTreePopup(self): """ Sets whether or not to use an ORB tree widget in the popup for this record box. :return <bool> """ return self._showTreePopup def specifiedColumns(self): """ Returns the list of columns that are specified based on the column view for this widget. :return [<orb.Column>, ..] """ columns = [] table = self.tableType() tree = self.treePopupWidget() schema = table.schema() if self._specifiedColumns is not None: colnames = self._specifiedColumns else: colnames = tree.columns() for colname in colnames: if isinstance(colname, Column): columns.append(colname) else: col = schema.column(colname) if col and not col.isProxy(): columns.append(col) return columns def specifiedColumnsOnly(self): """ Returns whether or not only specified columns should be loaded for this record box. :return <int> """ return self._specifiedColumnsOnly def startEditTimer(self): self._editedTimer.start() def tableLookupIndex(self): """ Returns the name of the index method that will be used to lookup records for this combo box. :return <str> """ return self._tableLookupIndex def tableType( self ): """ Returns the table type for this instance. :return <subclass of orb.Table> || None """ if not self._tableType: if self._tableTypeName: self._tableType = Orb.instance().model(nstr(self._tableTypeName)) return self._tableType def tableTypeName(self): """ Returns the table type name that is set for this combo box. :return <str> """ return self._tableTypeName def treePopupWidget(self): """ Returns the popup widget for this record box when it is supposed to be an ORB tree widget. :return <XTreeWidget> """ edit = self.lineEdit() if not self._treePopupWidget: # create the treewidget tree = XTreeWidget(self) tree.setWindowFlags(Qt.Popup) tree.setFocusPolicy(Qt.StrongFocus) tree.installEventFilter(self) tree.setAlternatingRowColors(True) tree.setShowGridColumns(False) tree.setRootIsDecorated(False) tree.setVerticalScrollMode(tree.ScrollPerPixel) # create connections tree.itemClicked.connect(self.acceptRecord) if edit: edit.textEdited.connect(tree.filterItems) edit.textEdited.connect(self.showPopup) self._treePopupWidget = tree return self._treePopupWidget def worker(self): """ Returns the worker object for loading records for this record box. :return <XOrbLookupWorker> """ if self._worker is None: self._worker = XOrbLookupWorker(self.isThreadEnabled()) self._worker.setBatchSize(self._batchSize) self._worker.setBatched(not self.isThreadEnabled()) # connect the worker self.loadRequested.connect(self._worker.loadRecords) self._worker.loadingStarted.connect(self.markLoadingStarted) self._worker.loadingFinished.connect(self.markLoadingFinished) self._worker.loadedRecords.connect(self.addRecordsFromThread) return self._worker x_batchSize = Property(int, batchSize, setBatchSize) x_required = Property(bool, isRequired, setRequired) x_tableTypeName = Property(str, tableTypeName, setTableTypeName) x_tableLookupIndex = Property(str, tableLookupIndex, setTableLookupIndex) x_showTreePopup = Property(bool, showTreePopup, setShowTreePopup) x_threadEnabled = Property(bool, isThreadEnabled, setThreadEnabled)
class XOrbSearchCompleter(QCompleter): def __init__( self, tableType, widget ): super(XOrbSearchCompleter, self).__init__(widget) # set default properties self.setModel(QStringListModel(self)) self.setCaseSensitivity(Qt.CaseInsensitive) self.setCompletionMode(QCompleter.UnfilteredPopupCompletion) # define custom properties self._currentRecord = None self._records = [] self._tableType = tableType self._baseQuery = None self._order = None self._lastSearch = '' self._cache = {} self._refreshTimer = QTimer(self) self._limit = 10 # limited number of search results self._pywidget = widget # need to store the widget as the parent # to avoid pyside crashing - # EKH 02/01/13 self._refreshTimer.setInterval(500) self._refreshTimer.setSingleShot(True) self._refreshTimer.timeout.connect(self.refresh) def baseQuery(self): """ Returns the base query that is used for filtering these results. :return <orb.Query> || None """ return self._baseQuery def currentRecord(self): """ Returns the current record based on the active index from the model. :return <orb.Table> || None """ completion = nativestring(self._pywidget.text()) options = map(str, self.model().stringList()) try: index = options.index(completion) except ValueError: return None return self._records[index] def eventFilter(self, object, event): """ Sets the completion prefor this instance, triggering a search query for the search query. :param prefix | <str> """ result = super(XOrbSearchCompleter, self).eventFilter(object, event) # update the search results if event.type() == event.KeyPress: # ignore return keys if event.key() in (Qt.Key_Return, Qt.Key_Enter): return False # ignore navigation keys if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right, Qt.Key_Shift, Qt.Key_Control, Qt.Key_Alt): return result text = self._pywidget.text() if text != self._lastSearch: # clear the current completion self.model().setStringList([]) # mark for reset self._refreshTimer.start() return result def limit(self): """ Returns the limit for the search results for this instance. :return <int> """ return self._limit def order(self): """ Returns the order that the results will be returned from the search. :return [(<str> columnName, <str> asc|desc), ..] """ return self._order def refresh(self): """ Refreshes the contents of the completer based on the current text. """ table = self.tableType() search = nativestring(self._pywidget.text()) if search == self._lastSearch: return self._lastSearch = search if not search: return if search in self._cache: records = self._cache[search] else: records = table.select(where = self.baseQuery(), order = self.order()) records = list(records.search(search, limit=self.limit())) self._cache[search] = records self._records = records self.model().setStringList(map(str, self._records)) self.complete() def setBaseQuery(self, query): """ Sets the base query for this completer to the inputed query. :param query | <orb.Query> """ self._baseQuery = query def setLimit(self, limit): """ Sets the limit of results to be pulled for this instance. :param limit | <int> """ self._limit = limit def setOrder(self, order): """ Sets the order for this search to the inputed order. :param order | [(<str> columnName, <str> asc|desc), ..] || None """ self._order = order def setTableType(self, tableType): """ Sets the table type for this instance to the inputed type. :param tableType | <subclass of orb.Table> """ self._tableType = tableType def tableType(self): """ Returns the table type that will be used for this completion mechanism. :return <subclass of orb.Table> """ return self._tableType
class XOrbSearchCompleter(QCompleter): def __init__(self, tableType, widget): super(XOrbSearchCompleter, self).__init__(widget) # set default properties self.setModel(QStringListModel(self)) self.setCaseSensitivity(Qt.CaseInsensitive) self.setCompletionMode(QCompleter.UnfilteredPopupCompletion) # define custom properties self._currentRecord = None self._records = [] self._tableType = tableType self._baseQuery = None self._order = None self._lastSearch = '' self._cache = {} self._refreshTimer = QTimer(self) self._limit = 10 # limited number of search results self._pywidget = widget # need to store the widget as the parent # to avoid pyside crashing - # EKH 02/01/13 self._refreshTimer.setInterval(500) self._refreshTimer.setSingleShot(True) self._refreshTimer.timeout.connect(self.refresh) def baseQuery(self): """ Returns the base query that is used for filtering these results. :return <orb.Query> || None """ return self._baseQuery def currentRecord(self): """ Returns the current record based on the active index from the model. :return <orb.Table> || None """ completion = nativestring(self._pywidget.text()) options = map(str, self.model().stringList()) try: index = options.index(completion) except ValueError: return None return self._records[index] def eventFilter(self, object, event): """ Sets the completion prefor this instance, triggering a search query for the search query. :param prefix | <str> """ result = super(XOrbSearchCompleter, self).eventFilter(object, event) # update the search results if event.type() == event.KeyPress: # ignore return keys if event.key() in (Qt.Key_Return, Qt.Key_Enter): return False # ignore navigation keys if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right, Qt.Key_Shift, Qt.Key_Control, Qt.Key_Alt): return result text = self._pywidget.text() if text != self._lastSearch: # clear the current completion self.model().setStringList([]) # mark for reset self._refreshTimer.start() return result def limit(self): """ Returns the limit for the search results for this instance. :return <int> """ return self._limit def order(self): """ Returns the order that the results will be returned from the search. :return [(<str> columnName, <str> asc|desc), ..] """ return self._order def refresh(self): """ Refreshes the contents of the completer based on the current text. """ table = self.tableType() search = nativestring(self._pywidget.text()) if search == self._lastSearch: return self._lastSearch = search if not search: return if search in self._cache: records = self._cache[search] else: records = table.select(where=self.baseQuery(), order=self.order()) records = list(records.search(search, limit=self.limit())) self._cache[search] = records self._records = records self.model().setStringList(map(str, self._records)) self.complete() def setBaseQuery(self, query): """ Sets the base query for this completer to the inputed query. :param query | <orb.Query> """ self._baseQuery = query def setLimit(self, limit): """ Sets the limit of results to be pulled for this instance. :param limit | <int> """ self._limit = limit def setOrder(self, order): """ Sets the order for this search to the inputed order. :param order | [(<str> columnName, <str> asc|desc), ..] || None """ self._order = order def setTableType(self, tableType): """ Sets the table type for this instance to the inputed type. :param tableType | <subclass of orb.Table> """ self._tableType = tableType def tableType(self): """ Returns the table type that will be used for this completion mechanism. :return <subclass of orb.Table> """ return self._tableType
class XMenu(QMenu): def __init__(self, parent=None): super(XMenu, self).__init__(parent) # define custom parameters self._acceptedAction = None self._showTitle = True self._advancedMap = {} self._customData = {} self._titleHeight = 24 self._toolTipAction = None self._toolTipTimer = QTimer(self) self._toolTipTimer.setInterval(1000) self._toolTipTimer.setSingleShot(True) # set default parameters self.setContentsMargins(0, self._titleHeight, 0, 0) self.setShowTitle(False) # create connections self.hovered.connect(self.startActionToolTip) self.aboutToShow.connect(self.clearAcceptedAction) self._toolTipTimer.timeout.connect(self.showActionToolTip) def acceptAdvanced(self): self._acceptedAction = self.sender().defaultAction() self.close() def acceptedAction(self): return self._acceptedAction def addMenu(self, submenu): """ Adds a new submenu to this menu. Overloads the base QMenu addMenu \ method so that it will return an XMenu instance vs. a QMenu when \ creating a submenu by passing in a string. :param submenu | <str> || <QMenu> :return <QMenu> """ # create a new submenu based on a string input if not isinstance(submenu, QMenu): title = nativestring(submenu) submenu = XMenu(self) submenu.setTitle(title) submenu.setShowTitle(self.showTitle()) super(XMenu, self).addMenu(submenu) else: super(XMenu, self).addMenu(submenu) submenu.menuAction().setData(wrapVariant('menu')) return submenu def addSearchAction(self): """ Adds a search action that will allow the user to search through the actions and sub-actions within in this menu. :return <XSearchAction> """ action = XSearchAction(self) self.addAction(action) return action def addSection(self, section): """ Adds a section to this menu. A section will create a label for the menu to separate sections of the menu out. :param section | <str> """ label = QLabel(section, self) label.setMinimumHeight(self.titleHeight()) # setup font font = label.font() font.setBold(True) # setup palette palette = label.palette() palette.setColor(palette.WindowText, palette.color(palette.Mid)) # setup label label.setFont(font) label.setAutoFillBackground(True) label.setPalette(palette) # create the widget action action = QWidgetAction(self) action.setDefaultWidget(label) self.addAction(action) return action def adjustMinimumWidth(self): """ Updates the minimum width for this menu based on the font metrics \ for its title (if its shown). This method is called automatically \ when the menu is shown. """ if not self.showTitle(): return metrics = QFontMetrics(self.font()) width = metrics.width(self.title()) + 20 if self.minimumWidth() < width: self.setMinimumWidth(width) def clearAdvancedActions(self): """ Clears out the advanced action map. """ self._advancedMap.clear() margins = list(self.getContentsMargins()) margins[2] = 0 self.setContentsMargins(*margins) def clearAcceptedAction(self): self._acceptedAction = None def customData(self, key, default=None): """ Returns data that has been stored on this menu. :param key | <str> default | <variant> :return <variant> """ key = nativestring(key) menu = self while (not key in menu._customData and \ isinstance(menu.parent(), XMenu)): menu = menu.parent() return menu._customData.get(nativestring(key), default) def paintEvent(self, event): """ Overloads the paint event for this menu to draw its title based on its \ show title property. :param event | <QPaintEvent> """ super(XMenu, self).paintEvent(event) if self.showTitle(): with XPainter(self) as painter: palette = self.palette() painter.setBrush(palette.color(palette.Button)) painter.setPen(Qt.NoPen) painter.drawRect(1, 1, self.width() - 2, 22) painter.setBrush(Qt.NoBrush) painter.setPen(palette.color(palette.ButtonText)) painter.drawText(1, 1, self.width() - 2, 22, Qt.AlignCenter, self.title()) def rebuildButtons(self): """ Rebuilds the buttons for the advanced actions. """ for btn in self.findChildren(XAdvancedButton): btn.close() btn.setParent(None) btn.deleteLater() for standard, advanced in self._advancedMap.items(): rect = self.actionGeometry(standard) btn = XAdvancedButton(self) btn.setFixedWidth(22) btn.setFixedHeight(rect.height()) btn.setDefaultAction(advanced) btn.setAutoRaise(True) btn.move(rect.right() + 1, rect.top()) btn.show() if btn.icon().isNull(): btn.setIcon(QIcon(resources.find('img/advanced.png'))) btn.clicked.connect(self.acceptAdvanced) def setAdvancedAction(self, standardAction, advancedAction): """ Links an advanced action with the inputed standard action. This will \ create a tool button alongside the inputed standard action when the \ menu is displayed. If the user selects the advanced action, then the \ advancedAction.triggered signal will be emitted. :param standardAction | <QAction> advancedAction | <QAction> || None """ if advancedAction: self._advancedMap[standardAction] = advancedAction margins = list(self.getContentsMargins()) margins[2] = 22 self.setContentsMargins(*margins) elif standardAction in self._advancedMap: self._advancedMap.pop(standardAction) if not self._advancedMap: margins = list(self.getContentsMargins()) margins[2] = 22 self.setContentsMargins(*margins) def setCustomData(self, key, value): """ Sets custom data for the developer on this menu instance. :param key | <str> value | <variant> """ self._customData[nativestring(key)] = value def setShowTitle(self, state): """ Sets whether or not the title for this menu should be displayed in the \ popup. :param state | <bool> """ self._showTitle = state margins = list(self.getContentsMargins()) if state: margins[1] = self.titleHeight() else: margins[1] = 0 self.setContentsMargins(*margins) def showEvent(self, event): """ Overloads the set visible method to update the advanced action buttons \ to match their corresponding standard action location. :param state | <bool> """ super(XMenu, self).showEvent(event) self.adjustSize() self.adjustMinimumWidth() self.rebuildButtons() def setTitleHeight(self, height): """ Sets the height for the title of this menu bar and sections. :param height | <int> """ self._titleHeight = height def showActionToolTip(self): """ Shows the tool tip of the action that is currently being hovered over. :param action | <QAction> """ if (not self.isVisible()): return geom = self.actionGeometry(self._toolTipAction) pos = self.mapToGlobal(QPoint(geom.left(), geom.top())) pos.setY(pos.y() + geom.height()) tip = nativestring(self._toolTipAction.toolTip()).strip().strip('.') text = nativestring(self._toolTipAction.text()).strip().strip('.') # don't waste time showing the user what they already see if (tip == text): return QToolTip.showText(pos, self._toolTipAction.toolTip()) def showTitle(self): """ Returns whether or not this menu should show the title in the popup. :return <bool> """ return self._showTitle def startActionToolTip(self, action): """ Starts the timer to hover over an action for the current tool tip. :param action | <QAction> """ self._toolTipTimer.stop() QToolTip.hideText() if not action.toolTip(): return self._toolTipAction = action self._toolTipTimer.start() def titleHeight(self): """ Returns the height for the title of this menu bar and sections. :return <int> """ return self._titleHeight def updateCustomData(self, data): """ Updates the custom data dictionary with the inputed data. :param data | <dict> """ if (not data): return self._customData.update(data) @staticmethod def fromString(parent, xmlstring, actions=None): """ Loads the xml string as xml data and then calls the fromXml method. :param parent | <QWidget> xmlstring | <str> actions | {<str> name: <QAction>, .. } || None :return <XMenu> || None """ try: xdata = ElementTree.fromstring(xmlstring) except ExpatError, e: logger.exception(e) return None return XMenu.fromXml(parent, xdata, actions)