Пример #1
0
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)
Пример #2
0
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()
Пример #3
0
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)
Пример #4
0
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
Пример #5
0
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
Пример #6
0
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)