Exemple #1
0
class XOrbQueryPlugin(object):
    Flags = enum('ReferenceRequired')
    
    def __init__(self):
        self._operatorMap = OrderedDict()
        
    def createEditor(self, parent, column, op, value):
        """
        Creates a new editor for the parent based on the plugin parameters.
        
        :param      parent | <QWidget>
        
        :return     <QWidget> || None
        """
        try:
            cls = self._operatorMap[nativestring(op)].cls
        except KeyError, AttributeError:
            return None
        
        # create the new editor
        if cls:
            widget = cls(parent)
            widget.setAttribute(Qt.WA_DeleteOnClose)
            projexui.setWidgetValue(widget, value)
            return widget
        
        return None
Exemple #2
0
class XColorIcon(QIcon):
    Style = enum('Plain')

    def __init__(self, icon, style=Style.Plain):
        if type(icon) in (str, unicode):
            icon = QColor(icon)

        if isinstance(icon, QColor):
            icon = self.generatePixmap(icon, style)

        super(XColorIcon, self).__init__(icon)

    @staticmethod
    def generatePixmap(color, style=Style.Plain, size=None):
        """
        Generates a new pixmap for the inputed color and style.  If no
        size is provided, then the default 32x32 will be used.
        
        :return     <QPixmap>
        """
        if size is None:
            size = QSize(32, 32)

        pixmap = QPixmap(size)

        # create a plain pixmap
        if style == XColorIcon.Style.Plain:
            pixmap.fill(color)

        return pixmap
Exemple #3
0
class QueryAggregate(object):
    Type = enum('Count', 'Maximum', 'Minimum', 'Sum')

    def __init__(self, typ, table, **options):
        self._type = typ
        self._table = table
        self._column = options.get('column', None)
        self._lookupOptions = orb.LookupOptions(**options)

    def columns(self):
        """
        Returns the column associated with this aggregate.
        
        :return     [<orb.Column>, ..]
        """
        if self._column:
            if not isinstance(self._column, orb.Column):
                col = self._table.schema().column(self._column)
            else:
                col = self._column
            return col,
        else:
            return self._table.schema().primaryColumns()

    def lookupOptions(self):
        """
        Returns the lookup options instance for this aggregate.
        
        :return     <orb.LookupOptions>
        """
        return self._lookupOptions

    def table(self):
        """
        Returns the table associated with this aggregate.
        
        :return     <orb.Table>
        """
        return self._table

    def type(self):
        """
        Returns the type for this aggregate.
        
        :return     <str>
        """
        return self._type
Exemple #4
0
    class TestAllColumns(orb.Table):
        Test = enum('Ok')

        # boolean
        bool = orb.BooleanColumn()

        # data
        binary = orb.BinaryColumn()
        json = orb.JSONColumn()
        query = orb.QueryColumn()
        yaml = orb.YAMLColumn()

        # datetime
        date = orb.DateColumn()
        datetime = orb.DatetimeColumn()
        datetime_tz = orb.DatetimeWithTimezoneColumn()
        interval = orb.IntervalColumn()
        time = orb.TimeColumn()
        timestamp = orb.TimestampColumn()
        utc_datetime = orb.UTC_DatetimeColumn()
        utc_timestamp = orb.UTC_TimestampColumn()

        # numeric
        id = orb.IdColumn()
        decimal = orb.DecimalColumn(scale=2)
        float = orb.FloatColumn()
        integer = orb.IntegerColumn()
        long = orb.LongColumn()
        enum = orb.EnumColumn()

        # reference
        reference = orb.ReferenceColumn(reference='TestReference')

        # string
        string = orb.StringColumn()
        text = orb.TextColumn()
        color = orb.ColorColumn()
        directory = orb.DirectoryColumn()
        email = orb.EmailColumn()
        filepath = orb.FilepathColumn()
        html = orb.HtmlColumn()
        password = orb.PasswordColumn(default='T3st1ng!')
        token = orb.TokenColumn()
        url = orb.UrlColumn()
        xml = orb.XmlColumn()
Exemple #5
0
class XLineEdit(QLineEdit):
    """
    Creates a new QLineEdit that allows the user to define a grayed out text
    hint that will be drawn when there is no text assigned to the widget.
    """

    __designer_icon__ = projexui.resources.find('img/ui/lineedit.png')

    hintChanged = Signal(str)
    textEntered = Signal(str)

    State = enum('Normal', 'Passed', 'Failed')

    InputFormat = enum('Normal', 'CamelHump', 'Underscore', 'Dash',
                       'ClassName', 'NoSpaces', 'Capitalize', 'Uppercase',
                       'Lowercase', 'Pretty', 'Package')

    def __init__(self, *args):
        super(XLineEdit, self).__init__(*args)

        palette = self.palette()
        hint_clr = palette.color(palette.Disabled, palette.Text)

        # set the hint property
        self._hint = ''
        self._hintPrefix = ''
        self._hintSuffix = ''
        self._spacer = '_'
        self._encoding = 'utf-8'
        self._hintColor = hint_clr
        self._buttonWidth = 0
        self._cornerRadius = 0
        self._currentState = XLineEdit.State.Normal
        self._inputFormat = XLineEdit.InputFormat.Normal
        self._selectAllOnFocus = False
        self._focusedIn = False
        self._useHintValue = True

        self._icon = QIcon()
        self._iconSize = QSize(14, 14)
        self._buttons = {}

        self.textChanged.connect(self.adjustText)
        self.returnPressed.connect(self.emitTextEntered)

    def adjustText(self):
        """
        Updates the text based on the current format options.
        """
        pos = self.cursorPosition()
        self.blockSignals(True)
        super(XLineEdit, self).setText(self.formatText(self.text()))
        self.setCursorPosition(pos)
        self.blockSignals(False)

    def addButton(self, button, alignment=None):
        """
        Adds a button the edit.  All the buttons will be layed out at the \
        end of the widget.
        
        :param      button      | <QToolButton>
                    alignment   | <Qt.Alignment>
        
        :return     <bool> | success
        """
        if alignment == None:
            if button.pos().x() < self.pos().x():
                alignment = Qt.AlignLeft
            else:
                alignment = Qt.AlignRight

        all_buttons = self.buttons()
        if button in all_buttons:
            return False

        # move the button to this edit
        button.setParent(self)
        button.setAutoRaise(True)
        button.setIconSize(self.iconSize())
        button.setCursor(Qt.ArrowCursor)
        button.setFixedSize(QSize(self.height() - 2, self.height() - 2))

        self._buttons.setdefault(alignment, [])
        self._buttons[alignment].append(button)
        self.adjustButtons()
        return True

    def adjustButtons(self):
        """
        Adjusts the placement of the buttons for this line edit.
        """
        y = 1

        for btn in self.buttons():
            btn.setIconSize(self.iconSize())
            btn.setFixedSize(QSize(self.height() - 2, self.height() - 2))

        # adjust the location for the left buttons
        left_buttons = self._buttons.get(Qt.AlignLeft, [])
        x = (self.cornerRadius() / 2.0) + 2

        for btn in left_buttons:
            btn.move(x, y)
            x += btn.width()

        # adjust the location for the right buttons
        right_buttons = self._buttons.get(Qt.AlignRight, [])

        w = self.width()
        bwidth = sum([btn.width() for btn in right_buttons])
        bwidth += (self.cornerRadius() / 2.0) + 1

        for btn in right_buttons:
            btn.move(w - bwidth, y)
            bwidth -= btn.width()

        self._buttonWidth = sum([btn.width() for btn in self.buttons()])
        self.adjustTextMargins()

    def adjustTextMargins(self):
        """
        Adjusts the margins for the text based on the contents to be displayed.
        """
        left_buttons = self._buttons.get(Qt.AlignLeft, [])

        if left_buttons:
            bwidth = left_buttons[-1].pos().x() + left_buttons[-1].width() - 4
        else:
            bwidth = 0 + (max(8, self.cornerRadius()) - 8)

        ico = self.icon()
        if ico and not ico.isNull():
            bwidth += self.iconSize().width()

        self.setTextMargins(bwidth, 0, 0, 0)

    def adjustStyleSheet(self):
        """
        Adjusts the stylesheet for this widget based on whether it has a \
        corner radius and/or icon.
        """
        radius = self.cornerRadius()
        icon = self.icon()

        if not self.objectName():
            self.setStyleSheet('')
        elif not (radius or icon):
            self.setStyleSheet('')
        else:
            palette = self.palette()

            options = {}
            options['corner_radius'] = radius
            options['padding'] = 5
            options['objectName'] = self.objectName()

            if icon and not icon.isNull():
                options['padding'] += self.iconSize().width() + 2

            self.setStyleSheet(LINEEDIT_STYLE % options)

    def buttons(self):
        """
        Returns all the buttons linked to this edit.
        
        :return     [<QToolButton>, ..]
        """
        all_buttons = []
        for buttons in self._buttons.values():
            all_buttons += buttons
        return all_buttons

    def clear(self):
        """
        Clears the text from the edit.
        """
        super(XLineEdit, self).clear()

        self.textEntered.emit('')
        self.textChanged.emit('')
        self.textEdited.emit('')

    def cornerRadius(self):
        """
        Returns the rounding radius for this widget's corner, allowing a \
        developer to round the edges for a line edit on the fly.
        
        :return     <int>
        """
        return self._cornerRadius

    def currentState(self):
        """
        Returns the current state for this line edit.
        
        :return     <XLineEdit.State>
        """
        return self._currentState

    def currentText(self):
        """
        Returns the text that is available currently, \
        if the user has set standard text, then that \
        is returned, otherwise the hint is returned.
        
        :return     <str>
        """
        text = nativestring(self.text())
        if text or not self.useHintValue():
            return text
        return self.hint()

    def emitTextEntered(self):
        """
        Emits the text entered signal for this line edit, provided the
        signals are not being blocked.
        """
        if not self.signalsBlocked():
            self.textEntered.emit(self.text())

    def encoding(self):
        return self._encoding

    def focusInEvent(self, event):
        """
        Updates the focus in state for this edit.
        
        :param      event | <QFocusEvent>
        """
        super(XLineEdit, self).focusInEvent(event)

        self._focusedIn = True

    def focusOutEvent(self, event):
        """
        Updates the focus in state for this edit.
        
        :param      event | <QFocusEvent>
        """
        super(XLineEdit, self).focusOutEvent(event)

        self._focusedIn = False

    def formatText(self, text):
        """
        Formats the inputed text based on the input format assigned to this
        line edit.
        
        :param      text | <str>
        
        :return     <str> | frormatted text
        """
        format = self.inputFormat()
        if format == XLineEdit.InputFormat.Normal:
            return text

        text = projex.text.nativestring(text)
        if format == XLineEdit.InputFormat.CamelHump:
            return projex.text.camelHump(text)

        elif format == XLineEdit.InputFormat.Pretty:
            return projex.text.pretty(text)

        elif format == XLineEdit.InputFormat.Underscore:
            return projex.text.underscore(text)

        elif format == XLineEdit.InputFormat.Dash:
            return projex.text.dashed(text)

        elif format == XLineEdit.InputFormat.ClassName:
            return projex.text.classname(text)

        elif format == XLineEdit.InputFormat.NoSpaces:
            return projex.text.joinWords(text, self.spacer())

        elif format == XLineEdit.InputFormat.Capitalize:
            return text.capitalize()

        elif format == XLineEdit.InputFormat.Uppercase:
            return text.upper()

        elif format == XLineEdit.InputFormat.Lowercase:
            return text.lower()

        elif format == XLineEdit.InputFormat.Package:
            return '.'.join(
                map(lambda x: x.lower(),
                    map(projex.text.classname, text.split('.'))))

        return text

    def hint(self):
        """
        Returns the hint value for this line edit.
        
        :return     <str>
        """
        parts = (self._hintPrefix, self._hint, self._hintSuffix)
        return ''.join(map(projex.text.nativestring, parts))

    def hintPrefix(self):
        """
        Returns the default prefix for the hint.
        
        :return     <str>
        """
        return self._hintPrefix

    def hintSuffix(self):
        """
        Returns the default suffix for the hint.
        
        :return     <str>
        """
        return self._hintSuffix

    def hintColor(self):
        """
        Returns the hint color for this text item.
        
        :return     <QColor>
        """
        return self._hintColor

    def icon(self):
        """
        Returns the icon instance that is being used for this widget.
        
        :return     <QIcon> || None
        """
        return self._icon

    def iconSize(self):
        """
        Returns the icon size that will be used for this widget.
        
        :return     <QSize>
        """
        return self._iconSize

    def inputFormat(self):
        """
        Returns the input format for this widget.
        
        :return     <int>
        """
        return self._inputFormat

    def inputFormatText(self):
        """
        Returns the input format as a text value for this widget.
        
        :return     <str>
        """
        return XLineEdit.InputFormat[self.inputFormat()]

    def mousePressEvent(self, event):
        """
        Selects all the text if the property is set after this widget
        first gains focus.
        
        :param      event | <QMouseEvent>
        """
        super(XLineEdit, self).mousePressEvent(event)

        if self._focusedIn and self.selectAllOnFocus():
            self.selectAll()
            self._focusedIn = False

    def paintEvent(self, event):
        """
        Overloads the paint event to paint additional \
        hint information if no text is set on the \
        editor.
        
        :param      event      | <QPaintEvent>
        """
        super(XLineEdit, self).paintEvent(event)

        # paint the hint text if not text is set
        if self.text() and not (self.icon() and not self.icon().isNull()):
            return

        # paint the hint text
        with XPainter(self) as painter:
            painter.setPen(self.hintColor())

            icon = self.icon()
            left, top, right, bottom = self.getTextMargins()

            w = self.width()
            h = self.height() - 2

            w -= (right + left)
            h -= (bottom + top)

            if icon and not icon.isNull():
                size = icon.actualSize(self.iconSize())
                x = self.cornerRadius() + 2
                y = (self.height() - size.height()) / 2.0

                painter.drawPixmap(x, y,
                                   icon.pixmap(size.width(), size.height()))

                w -= size.width() - 2
            else:
                x = 6 + left

            w -= self._buttonWidth
            y = 2 + top

            # create the elided hint
            if not self.text() and self.hint():
                rect = self.cursorRect()
                metrics = QFontMetrics(self.font())
                hint = metrics.elidedText(self.hint(), Qt.ElideRight, w)
                align = self.alignment()

                if align & Qt.AlignHCenter:
                    x = 0
                else:
                    x = rect.center().x()

                painter.drawText(x, y, w, h, align, hint)

    def resizeEvent(self, event):
        """
        Overloads the resize event to handle updating of buttons.
        
        :param      event | <QResizeEvent>
        """
        super(XLineEdit, self).resizeEvent(event)
        self.adjustButtons()

    def selectAllOnFocus(self):
        """
        Returns whether or not this edit will select all its contents on
        focus in.
        
        :return     <bool>
        """
        return self._selectAllOnFocus

    def setCornerRadius(self, radius):
        """
        Sets the corner radius for this widget tot he inputed radius.
        
        :param      radius | <int>
        """
        self._cornerRadius = radius

        self.adjustStyleSheet()

    def setCurrentState(self, state):
        """
        Sets the current state for this edit to the inputed state.
        
        :param      state | <XLineEdit.State>
        """
        self._currentState = state

        palette = self.palette()
        if state == XLineEdit.State.Normal:
            palette = QApplication.instance().palette()

        elif state == XLineEdit.State.Failed:
            palette.setColor(palette.Base, QColor('#ffc9bc'))
            palette.setColor(palette.Text, QColor('#aa2200'))
            palette.setColor(palette.Disabled, palette.Text, QColor('#e58570'))

        elif state == XLineEdit.State.Passed:
            palette.setColor(palette.Base, QColor('#d1ffd1'))
            palette.setColor(palette.Text, QColor('#00aa00'))
            palette.setColor(palette.Disabled, palette.Text, QColor('#75e575'))

        self._hintColor = palette.color(palette.Disabled, palette.Text)
        self.setPalette(palette)

    def setEncoding(self, enc):
        self._encoding = enc

    @Slot(str)
    def setHint(self, hint):
        """
        Sets the hint text to the inputed value.
        
        :param      hint       | <str>
        """
        self._hint = self.formatText(hint)
        self.update()
        self.hintChanged.emit(self.hint())

    def setHintColor(self, clr):
        """
        Sets the color for the hint for this edit.
        
        :param      clr     | <QColor>
        """
        self._hintColor = clr

    def setHintPrefix(self, prefix):
        """
        Ses the default prefix for the hint.
        
        :param     prefix | <str>
        """
        self._hintPrefix = str(prefix)

    def setHintSuffix(self, suffix):
        """
        Sets the default suffix for the hint.
        
        :param     suffix | <str>
        """
        self._hintSuffix = str(suffix)

    def setIcon(self, icon):
        """
        Sets the icon that will be used for this widget to the inputed icon.
        
        :param      icon | <QIcon> || None
        """
        self._icon = QIcon(icon)

        self.adjustStyleSheet()

    def setIconSize(self, size):
        """
        Sets the icon size that will be used for this edit.
        
        :param      size | <QSize>
        """
        self._iconSize = size
        self.adjustTextMargins()

    def setInputFormat(self, inputFormat):
        """
        Sets the input format for this text.
        
        :param      inputFormat | <int>
        """
        self._inputFormat = inputFormat

    def setInputFormatText(self, text):
        """
        Sets the input format text for this widget to the given value.
        
        :param      text | <str>
        """
        try:
            self._inputFormat = XLineEdit.InputFormat[nativestring(text)]
        except KeyError:
            pass

    def setObjectName(self, objectName):
        """
        Updates the style sheet for this line edit when the name changes.
        
        :param      objectName | <str>
        """
        super(XLineEdit, self).setObjectName(objectName)
        self.adjustStyleSheet()

    def setSelectAllOnFocus(self, state):
        """
        Returns whether or not this edit will select all its contents on
        focus in.
        
        :param      state | <bool>
        """
        self._selectAllOnFocus = state

    def setSpacer(self, spacer):
        """
        Sets the spacer that will be used for this line edit when replacing
        NoSpaces input formats.
        
        :param      spacer | <str>
        """
        self._spacer = spacer

    def setUseHintValue(self, state):
        """
        This method sets whether or not the value for this line edit should
        use the hint value if no text is found (within the projexui.xwidgetvalue
        plugin system).  When set to True, the value returned will first look
        at the text of the widget, and if it is blank, will then return the
        hint value.  If it is False, only the text value will be returned.
        
        :param      state | <bool>
        """
        self._useHintValue = state

    def setText(self, text):
        """
        Sets the text for this widget to the inputed text, converting it based \
        on the current input format if necessary.
        
        :param      text | <str>
        """
        if text is None:
            text = ''

        super(XLineEdit, self).setText(
            projex.text.encoded(self.formatText(text), self.encoding()))

    def setVisible(self, state):
        """
        Sets the visible state for this line edit.
        
        :param      state | <bool>
        """
        super(XLineEdit, self).setVisible(state)

        self.adjustStyleSheet()
        self.adjustTextMargins()

    def spacer(self):
        """
        Returns the spacer that is used to replace spaces when the NoSpaces
        input format is used.
        
        :return     <str>
        """
        return self._spacer

    def useHintValue(self):
        """
        This method returns whether or not the value for this line edit should
        use the hint value if no text is found (within the projexui.xwidgetvalue
        plugin system).  When set to True, the value returned will first look
        at the text of the widget, and if it is blank, will then return the
        hint value.  If it is False, only the text value will be returned.
        
        :return     <bool>
        """
        return self._useHintValue

    # create Qt properties
    x_hint = Property(str, hint, setHint)
    x_hintPrefix = Property(str, hintPrefix, setHintPrefix)
    x_hintSuffix = Property(str, hintSuffix, setHintSuffix)
    x_icon = Property('QIcon', icon, setIcon)
    x_iconSize = Property(QSize, iconSize, setIconSize)
    x_hintColor = Property('QColor', hintColor, setHintColor)
    x_cornerRadius = Property(int, cornerRadius, setCornerRadius)
    x_encoding = Property(str, encoding, setEncoding)
    x_inputFormatText = Property(str, inputFormatText, setInputFormatText)
    x_spacer = Property(str, spacer, setSpacer)
    x_selectAllOnFocus = Property(bool, selectAllOnFocus, setSelectAllOnFocus)
    x_useHintValue = Property(bool, useHintValue, setUseHintValue)

    # hack for qt
    setX_icon = setIcon
class XStackedWidget(QStackedWidget):
    __designer_container__ = True
    __designer_xml__ = """\
<widget class="XStackedWidget" name="stackedWidget">
    <property name="geometry">
        <rect>
            <x>100</x>
            <y>60</y>
            <width>321</width>
            <height>391</height>
        </rect>
    </property>
    <widget class="QWidget" name="page"/>
    <widget class="QWidget" name="page_2"/>
</widget>"""

    animationFinished = Signal()

    Direction = enum('LeftToRight', 'RightToLeft', 'TopToBottom',
                     'BottomToTop', 'Automatic')

    def __init__(self, parent=None):
        super(XStackedWidget, self).__init__(parent)

        # define custom properties
        self._animationType = QEasingCurve.Linear
        self._vertical = False
        self._wrap = False
        self._active = False
        self._speed = 250
        self._nextIndex = 0
        self._lastIndex = 0
        self._lastPoint = None

    def _finishAnimation(self):
        """
        Cleans up post-animation.
        """
        self.setCurrentIndex(self._nextIndex)
        self.widget(self._lastIndex).hide()
        self.widget(self._lastIndex).move(self._lastPoint)
        self._active = False

        if not self.signalsBlocked():
            self.animationFinished.emit()

    def animationType(self):
        """
        Returns the animation curve type for this widget.
        
        :return     <QEasingCurve.Type>
        """
        return self._animationType

    def clear(self):
        """
        Clears out the widgets from this stack.
        """
        for i in range(self.count() - 1, -1, -1):
            w = self.widget(i)
            if w:
                self.removeWidget(w)
                w.close()
                w.deleteLater()

    def isVerticalMode(self):
        """
        Returns whether or not the animation will play vertically or not.
        
        :return     <bool>
        """
        return self._vertical

    @Slot(QEasingCurve.Type)
    def setAnimationType(self, animationType):
        """
        Sets the animation curve type for this widget.
        
        :param      animationType | <QEasingCurve.Type>
        """
        self._animationType = animationType

    @Slot(int)
    def setSpeed(self, speed):
        """
        Sets the speed for this widget.
        
        :param      speed | <int>
        """
        self._speed = speed

    @Slot(bool)
    def setVerticalMode(self, state=True):
        """
        Sets whether or not the animation will play vertically or not.
        
        :param      state | <bool>
        """
        self._vertical = state

    @Slot(bool)
    def setWrap(self, state=True):
        """
        Sets whether or not the stacked widget will wrap during the animation.
        
        :param      state | <bool>
        """
        self._wrap = state

    @Slot(int)
    def slideIn(self, index, direction=Direction.Automatic):
        """
        Slides in the panel at the inputed index in the given
        direction for this widget.
        
        :param      index | <int>
                    direction | <XStackedWidget.Direction>
        
        :return     <bool> | success
        """
        # do not allow multiple slides while it is active
        if self._active:
            return False

        # determine the proper index to calculate
        invert = False
        if self.count() <= index:
            if not self.wrap():
                return False
            index = self.count() % index
            invert = True
        elif index < 0:
            if not self.wrap():
                return False
            index = self.count() + index
            invert = True

        # define the direction information
        if index == self.currentIndex():
            return False
        elif self.currentIndex() < index:
            if direction == XStackedWidget.Direction.Automatic:
                if self.isVerticalMode():
                    direction = XStackedWidget.Direction.BottomToTop
                else:
                    direction = XStackedWidget.Direction.RightToLeft
        else:
            if direction == XStackedWidget.Direction.Automatic:
                if self.isVerticalMode():
                    direction = XStackedWidget.Direction.TopToBottom
                else:
                    direction = XStackedWidget.Direction.LeftToRight

        # invert the animation if we are wrapping
        if invert:
            if direction == XStackedWidget.Direction.BottomToTop:
                direction = XStackedWidget.Direction.TopToBottom
            elif direction == XStackedWidget.Direction.TopToBottom:
                direction = XStackedWidget.Direction.BottomToTop
            elif direction == XStackedWidget.Direction.LeftToRight:
                direction = XStackedWidget.Direction.RightToLeft
            else:
                direction = XStackedWidget.Direction.LeftToRight

        self._active = True
        offset_x = self.frameRect().width()
        offset_y = self.frameRect().height()

        next_widget = self.widget(index)
        curr_widget = self.widget(self.currentIndex())

        next_widget.setGeometry(0, 0, offset_x, offset_y)

        if direction == XStackedWidget.Direction.BottomToTop:
            offset_x = 0
            offset_y = -offset_y
        elif direction == XStackedWidget.Direction.TopToBottom:
            offset_x = 0
        elif direction == XStackedWidget.Direction.RightToLeft:
            offset_x = -offset_x
            offset_y = 0
        elif direction == XStackedWidget.Direction.LeftToRight:
            offset_y = 0

        next_point = next_widget.pos()
        curr_point = curr_widget.pos()

        self._nextIndex = index
        self._lastIndex = self.currentIndex()
        self._lastPoint = QPoint(curr_point)

        next_widget.move(next_point.x() - offset_x, next_point.y() - offset_y)
        next_widget.raise_()
        next_widget.show()

        curr_anim = QPropertyAnimation(curr_widget, 'pos')
        curr_anim.setDuration(self.speed())
        curr_anim.setEasingCurve(self.animationType())
        curr_anim.setStartValue(curr_point)
        curr_anim.setEndValue(
            QPoint(curr_point.x() + offset_x,
                   curr_point.y() + offset_y))

        next_anim = QPropertyAnimation(next_widget, 'pos')
        next_anim.setDuration(self.speed())
        next_anim.setEasingCurve(self.animationType())
        next_anim.setStartValue(
            QPoint(next_point.x() - offset_x,
                   next_point.y() - offset_y))
        next_anim.setEndValue(next_point)

        anim_group = QParallelAnimationGroup(self)
        anim_group.addAnimation(curr_anim)
        anim_group.addAnimation(next_anim)

        anim_group.finished.connect(self._finishAnimation)
        anim_group.finished.connect(anim_group.deleteLater)
        anim_group.start()

        return True

    @Slot()
    def slideInNext(self):
        """
        Slides in the next slide for this widget.
        
        :return     <bool> | success
        """
        return self.slideIn(self.currentIndex() + 1)

    @Slot()
    def slideInPrev(self):
        """
        Slides in the previous slide for this widget.
        
        :return     <bool> | success
        """
        return self.slideIn(self.currentIndex() - 1)

    def speed(self):
        """
        Returns the speed property for this stacked widget.
        
        :return     <int>
        """
        return self._speed

    def wrap(self):
        """
        Returns whether or not the stacked widget will wrap during the
        animation.
        
        :return     <bool>
        """
        return self._wrap

    x_animationType = Property(QEasingCurve.Type, animationType,
                               setAnimationType)

    x_speed = Property(int, speed, setSpeed)
    x_verticalMode = Property(bool, isVerticalMode, setVerticalMode)
    x_wrap = Property(bool, wrap, setWrap)
Exemple #7
0
class XExporter(object):
    Flags = enum('SupportsTree')
    
    _plugins = []
    
    def __init__(self, name, filetype):
        self._name = name
        self._filetype = filetype
        self._flags = 0
    
    @abstractmethod()
    def exportTree(self, tree, filename):
        """
        Exports the tree information to the given filename.
        
        :param      tree     | <QTreeWidget>
                    filename | <str>
        
        :return     <bool> | success
        """
        return False
    
    def filetype(self):
        """
        Returns the filetype associated with this exporter.
        
        :return     <str>
        """
        return self._filetype
    
    def flags(self):
        """
        Returns the flags associated with this plugin.
        
        :return     <XExporter.Flags>
        """
        return self._flags
    
    def name(self):
        """
        Returns the name of this exporter.
        
        :return     <str>
        """
        return self._name
    
    def setFlag(self, flag, state=True):
        """
        Sets whether or not the given flag is enabled or disabled.
        
        :param      flag | <XExporter.Flags>
        """
        has_flag = self.testFlag(flag)
        if has_flag and not state:
            self.setFlags(self.flags() ^ flag)
        elif not has_flag and state:
            self.setFlags(self.flags() | flag)
    
    def setFlags(self, flags):
        """
        Sets the flags for this plugin.
        
        :param      flags | <XExporter.Flags>
        """
        self._flags = flags
    
    def testFlag(self, flag):
        """
        Tests to see if the inputed flag is applied for this plugin.
        
        :param      flag | <XExporter.Flags>
        """
        return (self.flags() & flag) != 0
    
    @staticmethod
    def init():
        """
        Initializes the exporter system.
        """
        if XExporter._plugins:
            return
        
        from projexui.exporters.xexcelexporter import XExcelExporter
    
    @staticmethod
    def plugins(flags=None):
        """
        Returns the plugins registered for the exporter.  If the optional
        flags are set then only plugins with the inputed flags will be
        returned.
        
        :param      flags | <XExporter.Flags>
        """
        XExporter.init()
        
        plugs = XExporter._plugins[:]
        if flags is not None:
            return filter(lambda x: x.testFlag(flags), plugs)
        return plugs
        
    @staticmethod
    def register(plugin):
        """
        Registers the inputed plugin to the system.
        
        :param      <XExporter>
        """
        XExporter._plugins.append(plugin)
Exemple #8
0
class XChartScene(QGraphicsScene):
    Type = enum('Bar', 'Pie', 'Line')

    chartTypeChanged = Signal()

    def __init__(self, chartWidget):
        super(XChartScene, self).__init__(chartWidget)

        # create custom properties
        self._chartWidget = chartWidget
        self._minimumWidth = -1
        self._minimumHeight = -1
        self._maximumWidth = -1
        self._maximumHeight = -1
        self._horizontalPadding = 6
        self._verticalPadding = 6
        self._showGrid = True
        self._showRows = True
        self._showColumns = True
        self._trackingEnabled = True
        self._chartType = XChartScene.Type.Line
        self._trackerItem = None

        # used with pie charts
        self._pieAxis = Qt.YAxis
        self._pieAlignment = Qt.AlignCenter

        self._horizontalRuler = XChartRuler(XChartRuler.Type.Number)
        self._verticalRuler = XChartRuler(XChartRuler.Type.Number)
        self._font = QApplication.font()

        self._alternatingColumnColors = False
        self._alternatingRowColors = True

        self._dirty = False
        self._buildData = {}

        palette = QApplication.palette()

        self._axisColor = palette.color(palette.Mid).darker(125)
        self._baseColor = palette.color(palette.Base)
        self._alternateColor = palette.color(palette.Base).darker(104)
        self._borderColor = palette.color(palette.Mid)

        # create custom properties
        chartWidget.installEventFilter(self)
        self.chartTypeChanged.connect(self.update)

    def alternateColor(self):
        """
        Returns the color to be used for the alternate background.
        
        :return     <QColor>
        """
        return self._alternateColor

    def alternatingColumnColors(self):
        """
        Returns whether or not to display alternating column colors.
        
        :return     <bool>
        """
        return self._alternatingColumnColors

    def alternatingRowColors(self):
        """
        Returns whehter or not to display alternating row colors.
        
        :return     <bool>
        """
        return self._alternatingRowColors

    def axisColor(self):
        """
        Returns the axis color for this chart.
        
        :return     <QColor>
        """
        return self._axisColor

    def baseColor(self):
        """
        Returns the color to be used for the primary background.
        
        :return     <QColor>
        """
        return self._baseColor

    def borderColor(self):
        """
        Returns the color to be used for the chart borders.
        
        :return     <QColor>
        """
        return self._borderColor

    def chartWidget(self):
        """
        Returns the chart widget this scene is linked to.
        
        :return     <XChartWidget>
        """
        return self._chartWidget

    def chartItems(self):
        """
        Returns the chart items that are found within this scene.
        
        :return     [<XChartWidgetItem>, ..]
        """
        from projexui.widgets.xchartwidget import XChartWidgetItem
        return filter(lambda x: isinstance(x, XChartWidgetItem), self.items())

    def chartType(self):
        """
        Returns the chart type for this scene.
        
        :return     <XChartScene.Type>
        """
        return self._chartType

    def drawBackground(self, painter, rect):
        """
        Draws the backgrounds for the different chart types.
        
        :param      painter | <QPainter>
                    rect    | <QRect>
        """
        if (self._dirty):
            self.rebuild()

        if (self.showGrid()):
            self.drawGrid(painter)

    def drawForeground(self, painter, rect):
        """
        Draws the foreground for the different chart types.
        
        :param      painter | <QPainter>
                    rect    | <QRect>
        """
        if (self.showGrid()):
            self.drawAxis(painter)

    def drawGrid(self, painter):
        """
        Draws the rulers for this scene.
        
        :param      painter | <QPainter>
        """
        # draw the minor grid lines
        pen = QPen(self.borderColor())
        painter.setPen(pen)
        painter.setBrush(self.baseColor())

        # draw the grid data
        painter.drawRect(self._buildData['grid_rect'])

        painter.setBrush(self.alternateColor())
        painter.setPen(Qt.NoPen)

        if (self.alternatingRowColors()):
            for alt_rect in self._buildData['grid_h_alt']:
                painter.drawRect(alt_rect)

        if (self.alternatingColumnColors()):
            for alt_rect in self._buildData['grid_v_alt']:
                painter.drawRect(alt_rect)

        if (self.showGrid()):
            painter.setPen(pen)

            grid = []
            if (self.showRows()):
                grid += self._buildData['grid_h_lines']

            if (self.showColumns()):
                grid += self._buildData['grid_v_lines']

            painter.drawLines(grid)

    def drawAxis(self, painter):
        """
        Draws the axis for this system.
        """
        # draw the axis lines
        pen = QPen(self.axisColor())
        pen.setWidth(4)
        painter.setPen(pen)
        painter.drawLines(self._buildData['axis_lines'])

        # draw the notches
        for rect, text in self._buildData['grid_h_notches']:
            painter.drawText(rect, Qt.AlignTop | Qt.AlignRight, text)

        for rect, text in self._buildData['grid_v_notches']:
            painter.drawText(rect, Qt.AlignCenter, text)

    def enterEvent(self, event):
        """
        Toggles the display for the tracker item.
        """
        item = self.trackerItem()
        if (item):
            item.setVisible(True)

    def eventFilter(self, object, event):
        """
        Filters the chart widget for the resize event to modify this scenes
        rect.
        
        :param      object | <QObject>
                    event | <QEvent>
        """
        if (event.type() != event.Resize):
            return False

        size = event.size()
        w = size.width()
        h = size.height()
        hpolicy = Qt.ScrollBarAlwaysOff
        vpolicy = Qt.ScrollBarAlwaysOff

        if (self._minimumHeight != -1 and h < self._minimumHeight):
            h = self._minimumHeight
            vpolicy = Qt.ScrollBarAsNeeded

        if (self._maximumHeight != -1 and self._maximumHeight < h):
            h = self._maximumHeight
            vpolicy = Qt.ScrollBarAsNeeded

        if (self._minimumWidth != -1 and w < self._minimumWidth):
            w = self._minimumWidth
            hpolicy = Qt.ScrollBarAsNeeded

        if (self._maximumWidth != -1 and self._maximumWidth < w):
            w = self._maximumWidth
            hpolicy = Qt.ScrollBarAsNeeded

        hruler = self.horizontalRuler()
        vruler = self.verticalRuler()

        hlen = hruler.minLength(Qt.Horizontal)
        vlen = hruler.minLength(Qt.Vertical)

        offset_w = 0
        offset_h = 0

        #        if ( hlen > w ):
        #            w        = hlen
        #            hpolicy  = Qt.ScrollBarAlwaysOn
        #            offset_h = 25
        #
        #        if ( vlen > h ):
        #            h        = vlen
        #            vpolicy  = Qt.ScrollBarAlwaysOn
        #            offset_w = 25

        self.setSceneRect(0, 0, w - offset_w, h - offset_h)
        object.setVerticalScrollBarPolicy(vpolicy)
        object.setHorizontalScrollBarPolicy(hpolicy)

        return False

    def font(self):
        """
        Returns the font for this scene.
        
        :return     <QFont>
        """
        return self._font

    def gridRect(self):
        """
        Returns the grid rect for this chart.
        
        :return     <QRectF>
        """
        if (self._dirty):
            self.rebuild()

        return self._buildData['grid_rect']

    def horizontalPadding(self):
        """
        Returns the horizontal padding for this scene.
        
        :return     <int>
        """
        return self._horizontalPadding

    def horizontalRuler(self):
        """
        Returns the horizontal (x-axis) ruler for this scene.
        """
        return self._horizontalRuler

    def isTrackingEnabled(self):
        """
        Returns whether or not tracking is enabled for this chart.
        
        :return     <bool>
        """
        return self._trackingEnabled

    def leaveEvent(self, event):
        """
        Toggles the display for the tracker item.
        """
        item = self.trackerItem()
        if (item):
            item.setVisible(False)

    def mapFromChart(self, x, y):
        """
        Maps a chart point to a pixel position within the grid based on the
        rulers.
        
        :param      x | <variant>
                    y | <variant>
        
        :return     <QPointF>
        """
        grid = self.gridRect()
        hruler = self.horizontalRuler()
        vruler = self.verticalRuler()

        xperc = hruler.percentAt(x)
        yperc = vruler.percentAt(y)

        xoffset = grid.width() * xperc
        yoffset = grid.height() * yperc

        xpos = grid.left() + xoffset
        ypos = grid.bottom() - yoffset

        return QPointF(xpos, ypos)

    def mouseMoveEvent(self, event):
        """
        Overloads the moving event to move the tracker item.
        
        :param      event | <QEvent>
        """
        super(XChartScene, self).mouseMoveEvent(event)

        self.updateTrackerItem(event.scenePos())

    def pieAxis(self):
        """
        Returns the axis that will be used when calculating percentages for the
        pie chart.
        
        :return     <Qt.Axis>
        """
        return self._pieAxis

    def pieAlignment(self):
        """
        Returns the alignment location to be used for the chart pie.
        
        :return     <Qt.Alignment>
        """
        return self._pieAlignment

    def rebuild(self):
        """
        Rebuilds the data for this scene to draw with.
        """
        global XChartWidgetItem
        if (XChartWidgetItem is None):
            from projexui.widgets.xchartwidget.xchartwidgetitem \
            import XChartWidgetItem

        self._buildData = {}

        # build the grid location
        x = 8
        y = 8
        w = self.sceneRect().width()
        h = self.sceneRect().height()
        hpad = self.horizontalPadding()
        vpad = self.verticalPadding()
        hmax = self.horizontalRuler().maxNotchSize(Qt.Horizontal)

        left_offset = hpad + self.verticalRuler().maxNotchSize(Qt.Vertical)
        right_offset = left_offset + hpad
        top_offset = vpad
        bottom_offset = top_offset + vpad + hmax

        left = x + left_offset
        right = w - right_offset
        top = y + top_offset
        bottom = h - bottom_offset

        rect = QRectF()
        rect.setLeft(left)
        rect.setRight(right)
        rect.setBottom(bottom)
        rect.setTop(top)

        self._buildData['grid_rect'] = rect

        # rebuild the ruler data
        self.rebuildGrid()
        self._dirty = False

        # rebuild all the items
        padding = self.horizontalPadding() + self.verticalPadding()
        grid = self.sceneRect()
        filt = lambda x: isinstance(x, XChartWidgetItem)
        items = filter(filt, self.items())
        height = float(grid.height())
        if height == 0:
            ratio = 1
        else:
            ratio = grid.width() / height
        count = len(items)

        if (not count):
            return

        if (ratio >= 1):
            radius = (grid.height() - padding * 2) / 2.0
            x = rect.center().x()
            y = rect.center().y()
            dx = radius * 2.5
            dy = 0
        else:
            radius = (grid.width() - padding * 2) / 2.0
            x = rect.center().x()
            y = rect.center().y()
            dx = 0
            dy = radius * 2.5

        for item in items:
            item.setPieCenter(QPointF(x, y))
            item.setRadius(radius)
            item.rebuild()

            x += dx
            y += dy

        if (self._trackerItem and self._trackerItem()):
            self._trackerItem().rebuild(self._buildData['grid_rect'])

    def rebuildGrid(self):
        """
        Rebuilds the ruler data.
        """
        vruler = self.verticalRuler()
        hruler = self.horizontalRuler()

        rect = self._buildData['grid_rect']

        # process the vertical ruler
        h_lines = []
        h_alt = []
        h_notches = []

        vpstart = vruler.padStart()
        vnotches = vruler.notches()
        vpend = vruler.padEnd()
        vcount = len(vnotches) + vpstart + vpend
        deltay = rect.height() / max((vcount - 1), 1)
        y = rect.bottom()
        alt = False

        for i in range(vcount):
            h_lines.append(QLineF(rect.left(), y, rect.right(), y))

            # store alternate color
            if (alt):
                alt_rect = QRectF(rect.left(), y, rect.width(), deltay)
                h_alt.append(alt_rect)

            # store notch information
            nidx = i - vpstart
            if (0 <= nidx and nidx < len(vnotches)):
                notch = vnotches[nidx]
                notch_rect = QRectF(0, y - 3, rect.left() - 3, deltay)
                h_notches.append((notch_rect, notch))

            y -= deltay
            alt = not alt

        self._buildData['grid_h_lines'] = h_lines
        self._buildData['grid_h_alt'] = h_alt
        self._buildData['grid_h_notches'] = h_notches

        # process the horizontal ruler
        v_lines = []
        v_alt = []
        v_notches = []

        hpstart = hruler.padStart()
        hnotches = hruler.notches()
        hpend = hruler.padEnd()
        hcount = len(hnotches) + hpstart + hpend
        deltax = rect.width() / max((hcount - 1), 1)
        x = rect.left()
        alt = False

        for i in range(hcount):
            v_lines.append(QLineF(x, rect.top(), x, rect.bottom()))

            # store alternate info
            if (alt):
                alt_rect = QRectF(x - deltax, rect.top(), deltax,
                                  rect.height())
                v_alt.append(alt_rect)

            # store notch information
            nidx = i - hpstart
            if (0 <= nidx and nidx < len(hnotches)):
                notch = hnotches[nidx]
                notch_rect = QRectF(x - (deltax / 2.0),
                                    rect.bottom() + 3, deltax, 13)
                v_notches.append((notch_rect, notch))

            x += deltax
            alt = not alt

        self._buildData['grid_v_lines'] = v_lines
        self._buildData['grid_v_alt'] = v_alt
        self._buildData['grid_v_notches'] = v_notches

        # draw the axis lines
        axis_lines = []
        axis_lines.append(
            QLineF(rect.left(), rect.top(), rect.left(), rect.bottom()))

        axis_lines.append(
            QLineF(rect.left(), rect.bottom(), rect.right(), rect.bottom()))

        self._buildData['axis_lines'] = axis_lines

    def setBarType(self):
        self.setChartType(XChartScene.Type.Bar)

    def setDirty(self, state=True):
        """
        Marks the scene as dirty and needing a rebuild.
        
        :param      state | <bool>
        """
        self._dirty = state

    def setChartType(self, chartType):
        """
        Sets the chart type for this scene to the inputed type.
        
        :param      chartType | <XChartScene.Type>
        """
        self._chartType = chartType
        self.setDirty()

        # setup default options
        if (chartType == XChartScene.Type.Pie):
            self.setShowGrid(False)

            self.horizontalRuler().setPadStart(0)
            self.horizontalRuler().setPadEnd(0)

        elif (chartType == XChartScene.Type.Bar):
            self.setShowGrid(True)
            self.setShowColumns(False)
            self.setShowRows(True)

            self.horizontalRuler().setPadStart(1)
            self.horizontalRuler().setPadEnd(1)

        else:
            self.setShowGrid(True)
            self.setShowColumns(True)
            self.setShowRows(True)

            self.horizontalRuler().setPadStart(0)
            self.horizontalRuler().setPadEnd(0)

        if (not self.signalsBlocked()):
            self.chartTypeChanged.emit()

    def setFont(self, font):
        """
        Sets the font for this scene.
        
        :param      font | <QFont>
        """
        self._font = font

    def setHorizontalPadding(self, padding):
        """
        Sets the horizontal padding amount for this chart to the given value.
        
        :param      padding | <int>
        """
        self._horizontalPadding = padding

    def setHorizontalRuler(self, ruler):
        """
        Sets the horizontal ruler for this chart to the inputed ruler.
        
        :param      ruler | <XChartRuler>
        """
        self._horizontalRuler = ruler

    def setLineType(self):
        self.setChartType(XChartScene.Type.Line)

    def setPieAlignment(self, alignment):
        """
        Sets the alignment to be used when rendering a pie chart.
        
        :param      alignment | <Qt.Alignment>
        """
        self._alignment = alignment

    def setPieAxis(self, axis):
        """
        Sets the axis to be used when calculating pie chart information.
        
        :param      axis | <Qt.Axis>
        """
        self._pieAxis = axis

    def setPieType(self):
        self.setChartType(XChartScene.Type.Pie)

    def setSceneRect(self, *args):
        """
        Overloads the set scene rect to handle rebuild information.
        """
        super(XChartScene, self).setSceneRect(*args)
        self._dirty = True

    def setShowColumns(self, state):
        """
        Sets whether or not to display the columns for this chart.
        
        :param      state | <bool>
        """
        self._showColumns = state

    def setShowGrid(self, state):
        """
        Sets whether or not the grid should be visible.
        
        :param      state | <bool>
        """
        self._showGrid = state

    def setShowRows(self, state):
        """
        Sets whether or not to display the rows for this chart.
        
        :param      state | <bool>
        """
        self._showRows = state

    def setTrackingEnabled(self, state):
        """
        Sets whether or not information tracking is enabled for this chart.
        
        :param      state | <bool>
        """
        self._trackingEnabled = state
        self.updateTrackerItem()

    def setVerticalPadding(self, padding):
        """
        Sets the vertical padding amount for this chart to the given value.
        
        :param      padding | <int>
        """
        self._verticalPadding = padding

    def setVerticalRuler(self, ruler):
        """
        Sets the vertical ruler for this chart to the inputed ruler.
        
        :param      ruler | <XChartRuler>
        """
        self._verticalRuler = ruler

    def showColumns(self):
        """
        Returns whether or not to show columns for this scene.
        
        :return     <bool>
        """
        return self._showColumns

    def showGrid(self):
        """
        Sets whether or not the grid should be visible for this scene.
        
        :return     <bool>
        """
        return self._showGrid

    def showRows(self):
        """
        Returns whether or not to show rows for this scene.
        
        :return     <bool>
        """
        return self._showRows

    def trackerItem(self):
        """
        Returns the tracker item for this chart.
        
        :return     <XChartTrackerItem> || None
        """
        # check for the tracking enabled state
        if not self.isTrackingEnabled():
            return None

        # generate a new tracker item
        if not (self._trackerItem and self._trackerItem()):
            item = XChartTrackerItem()
            self.addItem(item)
            self._trackerItem = weakref.ref(item)

        return self._trackerItem()

    def updateTrackerItem(self, point=None):
        """
        Updates the tracker item information.
        """
        item = self.trackerItem()
        if not item:
            return

        gridRect = self._buildData.get('grid_rect')

        if (not (gridRect and gridRect.isValid())):
            item.setVisible(False)
            return

        if (point is not None):
            item.setPos(point.x(), gridRect.top())

        if (not gridRect.contains(item.pos())):
            item.setVisible(False)
            return

        if (self.chartType() != self.Type.Line):
            item.setVisible(False)
            return

        if (not self.isTrackingEnabled()):
            item.setVisible(False)
            return

        item.rebuild(gridRect)

    def valueAt(self, point):
        """
        Returns the X, Y value for the given point.
        
        :param      point | <QPoint>
        
        :return     (<variant> x, <variant> y)
        """
        x = point.x()
        y = point.y()

        hruler = self.horizontalRuler()
        vruler = self.verticalRuler()

        grid = self._buildData.get('grid_rect')
        if (not grid):
            return (None, None)

        x_perc = 1 - ((grid.right() - x) / grid.width())
        y_perc = ((grid.bottom() - y) / grid.height())

        return (hruler.valueAt(x_perc), vruler.valueAt(y_perc))

    def verticalPadding(self):
        """
        Returns the vertical padding amount for this chart.
        
        :return     <int>
        """
        return self._verticalPadding

    def verticalRuler(self):
        """
        Returns the vertical (y-axis) ruler for this chart.
        
        :return     <XChartRuler>
        """
        return self._verticalRuler
Exemple #9
0
class XView(QWidget):
    activated = Signal()
    deactivated = Signal()
    currentStateChanged = Signal(bool)
    windowTitleChanged = Signal(str)
    sizeConstraintChanged = Signal()
    initialized = Signal()
    poppedOut = Signal()
    shown = Signal()
    hidden = Signal()
    visibleStateChanged = Signal(bool)

    _registry = {}
    _globals = {}

    # define static globals
    _dispatcher = None
    _dispatch = {}

    __designer_icon__ = resources.find('img/ui/view.png')

    SignalPolicy = enum('BlockIfNotCurrent', 'BlockIfNotInGroup',
                        'BlockIfNotVisible', 'BlockIfNotInitialized',
                        'NeverBlock')

    def __init__(self, parent):
        super(XView, self).__init__(parent)

        # define custom properties
        self._current = False
        self._initialized = False
        self._viewWidget = None
        self._viewingGroup = 0
        self._signalPolicy  = XView.SignalPolicy.BlockIfNotInitialized | \
                              XView.SignalPolicy.BlockIfNotVisible | \
                              XView.SignalPolicy.BlockIfNotInGroup

        self._visibleState = False  # storing this state for knowing if a
        # widget WILL be visible once Qt finishes
        # processing for purpose of signal
        # validation.

        # setup default properties
        self.setAutoFillBackground(True)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setWindowTitle(self.viewName())
        self.setFocus()

    def canClose(self):
        """
        Virtual method to determine whether or not this view can properly
        close.
        
        :return     <bool>
        """
        return True

    def closeEvent(self, event):
        """
        Determines whether or not this widget should be deleted after close.
        
        :param      event | <QtCore.QCloseEvent>
        """
        if not self.canClose():
            event.ignore()
            return

        elif not self.isViewSingleton():
            self.setAttribute(Qt.WA_DeleteOnClose)

        else:
            self.setParent(
                self.window()
            )  # attach the hidden singleton instance to the window vs. anything in the view

        super(XView, self).closeEvent(event)

    def dispatchConnect(self, signal, slot):
        """
        Connect the slot for this view to the given signal that gets
        emitted by the XView.dispatch() instance.
        
        :param      signal | <str>
                    slot   | <callable>
        """
        XView.dispatch().connect(signal, slot)

    def dispatchEmit(self, signal, *args):
        """
        Emits the given signal via the XView dispatch instance with the
        given arguments.
        
        :param      signal | <str>
                    args   | <tuple>
        """
        XView.setGlobal('emitGroup', self.viewingGroup())
        XView.dispatch().emit(signal, *args)

    def duplicate(self, parent):
        """
        Duplicates this current view for another.  Subclass this method to 
        provide any additional duplication options.
        
        :param      parent | <QWidget>
        
        :return     <XView> | instance of this class
        """
        # only return a single singleton instance
        if self.isViewSingleton():
            return self

        output = type(self).createInstance(parent, self.viewWidget())

        # save/restore the current settings
        xdata = ElementTree.Element('data')
        self.saveXml(xdata)
        new_name = output.objectName()
        output.setObjectName(self.objectName())
        output.restoreXml(xdata)
        output.setObjectName(new_name)

        return output

    def hideEvent(self, event):
        """
        Sets the visible state for this widget.  If it is the first time this
        widget will be visible, the initialized signal will be emitted.
        
        :param      state | <bool>
        """
        super(XView, self).hideEvent(event)

        # record the visible state for this widget to be separate of Qt's
        # system to know if this view WILL be visible or not once the
        # system is done processing.  This will affect how signals are
        # validated as part of the visible slot delegation
        self._visibleState = False

        if not self.signalsBlocked():
            self.visibleStateChanged.emit(False)
            QTimer.singleShot(0, self.hidden)

    def initialize(self, force=False):
        """
        Initializes the view if it is visible or being loaded.
        """
        if force or (self.isVisible() and \
                     not self.isInitialized() and \
                     not self.signalsBlocked()):

            self._initialized = True
            self.initialized.emit()

    def isCurrent(self):
        """
        Returns whether or not this view is current within its view widget.
        
        :return     <bool>
        """
        return self._current

    def isInitialized(self):
        """
        Returns whether or not this view has been initialized.  A view will
        be initialized the first time it becomes visible to the user.  You
        can use this to delay loading of information until it is needed by
        listening for the initialized signal.
        
        :return     <bool>
        """
        return self._initialized

    def mousePressEvent(self, event):
        btn = event.button()
        mid = btn == Qt.MidButton
        lft = btn == Qt.LeftButton
        shft = event.modifiers() == Qt.ShiftModifier

        if self.windowFlags() & Qt.Dialog and \
           (mid or (lft and shft)):
            pixmap = QPixmap.grabWidget(self)
            drag = QDrag(self)
            data = QMimeData()
            data.setData('x-application/xview/floating_view',\
                         QByteArray(self.objectName()))
            drag.setMimeData(data)
            drag.setPixmap(pixmap)
            self.hide()
            drag.exec_()
            self.show()
        else:
            super(XView, self).mousePressEvent(event)

    def popout(self):
        self.setParent(self.window())
        self.setWindowFlags(Qt.Dialog)
        self.show()
        self.raise_()
        self.activateWindow()

        pos = QCursor.pos()
        w = self.width()

        self.move(pos.x() - w / 2.0, pos.y() - 10)

        # set the popup instance for this class to this widget
        key = '_{0}__popupInstance'.format(type(self).__name__)
        if not hasattr(type(self), key):
            setattr(type(self), key, weakref.ref(self))

        self.poppedOut.emit()

    def restoreXml(self, xml):
        """
        Restores the settings for this view from the inputed XML node.
        
        :param      xml | <xml.etree.ElementTree.Element>
        """
        pass

    def saveXml(self, xml):
        """
        Saves the settings for this view to the inputed XML node.
        
        :param      xml | <xml.etree.ElementTree.Element>
        """
        pass

    def settingsName(self):
        """
        Returns the default settings name for this view.
        
        :return     <str>
        """
        return 'Views/%s' % self.objectName()

    def setCurrent(self, state=True):
        """
        Marks this view as the current source based on the inputed flag.  \
        This method will return True if the currency changes.
        
        :return     <bool> | changed
        """
        if self._current == state:
            return False

        widget = self.viewWidget()
        if widget:
            for other in widget.findChildren(type(self)):
                if other.isCurrent():
                    other._current = False
                    if not other.signalsBlocked():
                        other.currentStateChanged.emit(state)
                        other.deactivated.emit()

        self._current = state

        if not self.signalsBlocked():
            self.currentStateChanged.emit(state)
            if state:
                self.activated.emit()
            else:
                self.deactivated.emit()
        return True

    def setFixedHeight(self, height):
        """
        Sets the maximum height value to the inputed height and emits the \
        sizeConstraintChanged signal.
        
        :param      height | <int>
        """
        super(XView, self).setFixedHeight(height)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setFixedWidth(self, width):
        """
        Sets the maximum width value to the inputed width and emits the \
        sizeConstraintChanged signal.
        
        :param      width | <int>
        """
        super(XView, self).setFixedWidth(width)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setMaximumHeight(self, height):
        """
        Sets the maximum height value to the inputed height and emits the \
        sizeConstraintChanged signal.
        
        :param      height | <int>
        """
        super(XView, self).setMaximumHeight(height)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setMaximumSize(self, *args):
        """
        Sets the maximum size value to the inputed size and emits the \
        sizeConstraintChanged signal.
        
        :param      *args | <tuple>
        """
        super(XView, self).setMaximumSize(*args)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setMaximumWidth(self, width):
        """
        Sets the maximum width value to the inputed width and emits the \
        sizeConstraintChanged signal.
        
        :param      width | <int>
        """
        super(XView, self).setMaximumWidth(width)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setMinimumHeight(self, height):
        """
        Sets the minimum height value to the inputed height and emits the \
        sizeConstraintChanged signal.
        
        :param      height | <int>
        """
        super(XView, self).setMinimumHeight(height)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setMinimumSize(self, *args):
        """
        Sets the minimum size value to the inputed size and emits the \
        sizeConstraintChanged signal.
        
        :param      *args | <tuple>
        """
        super(XView, self).setMinimumSize(*args)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setMinimumWidth(self, width):
        """
        Sets the minimum width value to the inputed width and emits the \
        sizeConstraintChanged signal.
        
        :param      width | <int>
        """
        super(XView, self).setMinimumWidth(width)

        if (not self.signalsBlocked()):
            self.sizeConstraintChanged.emit()

    def setSignalPolicy(self, policy):
        """
        Sets the signal delegation policy for this instance to the given 
        policy.  By default, signals will be delegates for groups or
        by currency if they are not in a group.  This will not directly
        affect signal propogation, only the result of the validateSignal
        method, so if you want to test against this, then you will need
        to check in your slot.
        
        :param      policy | <XView.SignalPolicy>
        """
        self._signalPolicy = policy

    def setViewingGroup(self, grp):
        """
        Sets the viewing group that this view is associated with.
        
        :param      grp | <int>
        """
        self._viewingGroup = grp

    def setViewWidget(self, widget):
        """
        Sets the view widget that is associated with this view item.
        
        :param      widget | <projexui.widgets.xviewwidget.XViewWidget>
        """
        self._viewWidget = widget

    def setWindowTitle(self, title):
        """
        Sets the window title for this view, and emits the windowTitleChanged \
        signal if the signals are not blocked.  Setting this title will update \
        the tab title for the view within the widget.
        
        :param      title | <str>
        """
        super(XView, self).setWindowTitle(title)
        if (not self.signalsBlocked()):
            self.windowTitleChanged.emit(title)

    def showActiveState(self, state):
        """
        Shows this view in the active state based on the inputed state settings.
        
        :param      state | <bool>
        """
        return

        palette = self.window().palette()
        clr = palette.color(palette.Window)
        avg = (clr.red() + clr.green() + clr.blue()) / 3

        if avg < 180 and state:
            clr = clr.lighter(105)
        elif not state:
            clr = clr.darker(105)

        palette.setColor(palette.Window, clr)
        self.setPalette(palette)

    def showEvent(self, event):
        """
        Sets the visible state for this widget.  If it is the first time this
        widget will be visible, the initialized signal will be emitted.
        
        :param      state | <bool>
        """
        super(XView, self).showEvent(event)

        # record the visible state for this widget to be separate of Qt's
        # system to know if this view WILL be visible or not once the
        # system is done processing.  This will affect how signals are
        # validated as part of the visible slot delegation
        self._visibleState = True

        if not self.isInitialized():
            self.initialize()

        # after the initial time the view is loaded, the visibleStateChanged
        # signal will be emitted
        elif not self.signalsBlocked():
            self.visibleStateChanged.emit(True)
            QTimer.singleShot(0, self.shown)

    def signalPolicy(self):
        """
        Returns the signal policy for this instance.
        
        :return     <XView.SignalPolicy>
        """
        return self._signalPolicy

    def rootWidget(self):
        widget = self
        while widget.parent():
            widget = widget.parent()
        return widget

    def viewWidget(self):
        """
        Returns the view widget that is associated with this instance.
        
        :return     <projexui.widgets.xviewwidget.XViewWidget>
        """
        return self._viewWidget

    def validateSignal(self, policy=None):
        """
        Validates that this view is part of the group that was emitting
        the signal.  Views that are not in any viewing group will accept
        all signals.
        
        :param      policy | <XView.SignalPolicy> || None
        
        :return     <bool>
        """
        # validate whether or not to process a signal
        if policy is None:
            policy = self.signalPolicy()

        group_check = XView.getGlobal('emitGroup') == self.viewingGroup()
        current_check = self.isCurrent()

        # always delegate signals if they are not set to block,
        # or if the method is called directly (not from a signal)
        if not self.sender() or policy & XView.SignalPolicy.NeverBlock:
            return True

        # block delegation of the signal if the view is not initialized
        elif policy & XView.SignalPolicy.BlockIfNotInitialized and \
             not self.isInitialized():
            return False

        # block delegation if the view is not visible
        elif policy & XView.SignalPolicy.BlockIfNotVisible and \
            not self._visibleState:
            return False

        # block delegation if the view is not part of a group
        elif self.viewingGroup() and \
             policy & XView.SignalPolicy.BlockIfNotInGroup:
            return group_check

        # look for only currency releated connections
        elif policy & XView.SignalPolicy.BlockIfNotCurrent:
            return current_check

        else:
            return True

    def viewingGroup(self):
        """
        Returns the viewing group that this view is assigned to.
        
        :return     <int>
        """
        return self._viewingGroup

    @classmethod
    def currentView(cls, parent=None):
        """
        Returns the current view for the given class within a viewWidget.  If
        no view widget is supplied, then a blank view is returned.
        
        :param      viewWidget | <projexui.widgets.xviewwidget.XViewWidget> || None
        
        :return     <XView> || None
        """
        if parent is None:
            parent = projexui.topWindow()

        for inst in parent.findChildren(cls):
            if inst.isCurrent():
                return inst
        return None

    @classmethod
    def createInstance(cls, parent, viewWidget=None):
        singleton_key = '_{0}__singleton'.format(cls.__name__)
        singleton = getattr(cls, singleton_key, None)
        singleton = singleton() if singleton is not None else None

        # assign the singleton instance
        if singleton is not None:
            singleton.setParent(parent)
            return singleton
        else:
            # determine if we need to store a singleton
            inst = cls(parent)
            inst.setObjectName(cls.uniqueName())
            inst.setViewWidget(viewWidget)

            if cls.isViewSingleton():
                setattr(cls, singleton_key, weakref.ref(inst))

            return inst

    @classmethod
    def destroySingleton(cls):
        """
        Destroys the singleton instance of this class, if one exists.
        """
        singleton_key = '_{0}__singleton'.format(cls.__name__)
        singleton = getattr(cls, singleton_key, None)

        if singleton is not None:
            setattr(cls, singleton_key, None)

            singleton.close()
            singleton.deleteLater()

    @classmethod
    def instances(cls, parent=None):
        """
        Returns all the instances that exist for a given parent.  If
        no parent exists, then a blank list will be returned.
        
        :param      parent | <QtGui.QWidget>
        
        :return     [<XView>, ..]
        """
        if parent is None:
            parent = projexui.topWindow()
        return parent.findChildren(cls)

    @classmethod
    def isViewAbstract(cls):
        """
        Returns whether or not this view is a purely abstract view or not.

        :return     <bool>
        """
        return getattr(cls, '_{0}__viewAbstract'.format(cls.__name__), False)

    @classmethod
    def isViewSingleton(cls):
        return getattr(cls, '_{0}__viewSingleton'.format(cls.__name__), False)

    @classmethod
    def isPopupSingleton(cls):
        return getattr(cls, '_{0}__popupSingleton'.format(cls.__name__), True)

    @classmethod
    def popup(cls, parent=None, viewWidget=None):
        """
        Pops up this view as a new dialog.  If the forceDialog flag is set to \
        False, then it will try to activate the first instance of the view \
        within an existing viewwidget context before creating a new dialog.
        
        :param      parent      | <QWidget> || None
                    viewWidget  | <projexui.widgets.xviewwidget.XViewWidget> || None
        """
        if cls.isViewSingleton():
            inst = cls.createInstance(parent, viewWidget)
            inst.setWindowFlags(Qt.Dialog)
        else:
            inst = cls.popupInstance(parent, viewWidget)

        inst.showNormal()
        inst.setFocus()
        inst.raise_()
        inst.activateWindow()

    @classmethod
    def popupInstance(cls, parent, viewWidget=None):
        key = '_{0}__popupInstance'.format(cls.__name__)
        try:
            inst = getattr(cls, key, None)()
        except TypeError:
            inst = None

        if inst is not None:
            return inst

        # create a new instance for this popup
        inst = cls.createInstance(parent, viewWidget)
        inst.setWindowFlags(Qt.Dialog)
        if cls.isPopupSingleton():
            setattr(cls, key, weakref.ref(inst))

        return inst

    @classmethod
    def registerToWindow(cls, window):
        """
        Registers this view to the window to update additional menu items, \
        actions, and toolbars.
        
        :param      window | <QWidget>
        """
        pass

    @classmethod
    def restoreGlobalSettings(cls, settings):
        """
        Restores the global settings for the inputed view class type.
        
        :param      cls      | <subclass of XView>
                    settings | <QSettings>
        """
        pass

    @classmethod
    def saveGlobalSettings(cls, settings):
        """
        Saves the global settings for the inputed view class type.
        
        :param      cls      | <subclass of XView>
                    settings | <QSettings>
        """
        pass

    @classmethod
    def setViewAbstract(cls, state):
        """
        Sets whether or not this view is used only as an abstract class.

        :param      state | <bool>
        """
        setattr(cls, '_{0}__viewAbstract'.format(cls.__name__), state)

    @classmethod
    def setViewGroup(cls, grp):
        setattr(cls, '_{0}__viewGroup'.format(cls.__name__), grp)

    @classmethod
    def setViewIcon(cls, icon):
        setattr(cls, '_{0}__viewIcon'.format(cls.__name__), icon)

    @classmethod
    def setViewName(cls, name):
        setattr(cls, '_{0}__viewName'.format(cls.__name__), name)

    @classmethod
    def setViewSingleton(cls, state):
        setattr(cls, '_{0}__viewSingleton'.format(cls.__name__), state)

    @classmethod
    def setPopupSingleton(cls, state):
        setattr(cls, '_{0}__popupSingleton'.format(cls.__name__), state)

    @classmethod
    def uniqueName(cls):
        key = '_{0}__serial'.format(cls.__name__)
        next = getattr(cls, key, 0) + 1
        setattr(cls, key, next)
        return '{0}{1:02}'.format(cls.viewName(), next)

    @classmethod
    def unregisterToWindow(cls, window):
        """
        Registers this view to the window to update additional menu items, \
        actions, and toolbars.
        
        :param      window | <QWidget>
        """
        pass

    @classmethod
    def viewGroup(cls):
        return getattr(cls, '_{0}__viewGroup'.format(cls.__name__), 'Default')

    @classmethod
    def viewIcon(cls):
        default = resources.find('img/view/view.png')
        return getattr(cls, '_{0}__viewIcon'.format(cls.__name__), default)

    @classmethod
    def viewName(cls):
        return getattr(cls, '_{0}__viewName'.format(cls.__name__),
                       cls.__name__)

    @classmethod
    def viewTypeName(cls):
        """
        Returns the unique name for this view type by joining its group with \
        its name.
        
        :return     <str>
        """
        return '%s.%s' % (cls.viewGroup(), cls.viewName())

    #--------------------------------------------------------------------------

    @staticmethod
    def dispatch(location='Central'):
        """
        Returns the instance of the global view dispatching system.  All views \
        will route their signals through the central hub so no single view \
        necessarily depends on another.
        
        :return     <XViewDispatch>
        """
        dispatch = XView._dispatch.get(nativestring(location))
        if not dispatch:
            dispatch = XViewDispatch(QApplication.instance())
            XView._dispatch[nativestring(location)] = dispatch

        return dispatch

    @staticmethod
    def getGlobal(key, default=None):
        """
        Returns the global value for the inputed key.
        
        :param      key     | <str>
                    default | <variant>
        
        :return     <variant>
        """
        return XView._globals.get(key, default)

    @staticmethod
    def registeredView(viewName, location='Central'):
        """
        Returns the view that is registered to the inputed location for the \
        given name.
        
        :param      viewName | <str>
                    location | <str>
        
        :return     <subclass of XView> || None
        """
        loc = nativestring(location)
        view = XView._registry.get(loc, {}).get(viewName, None)
        if not view:
            for view in XView._registry.get(nativestring(location),
                                            {}).values():
                if view.__name__ == viewName:
                    return view
        return view

    @staticmethod
    def registeredViews(location='Central'):
        """
        Returns all the views types that have bene registered to a particular \
        location.
        
        :param      location | <str>
        
        :return     [<subclass of XView>, ..]
        """
        return [
            view for view in XView._registry.get(nativestring(
                location), {}).values() if not view.isViewAbstract()
        ]

    @staticmethod
    def registerView(viewType, location='Central'):
        """
        Registers the inputed view type to the given location.  The location \
        is just a way to group and organize potential view plugins for a \
        particular widget, and is determined per application.  This eases \
        use when building a plugin based system.  It has no relevance to the \
        XView class itself where you register a view.
        
        :param      viewType | <subclass of XView>
        """
        # update the dispatch signals
        sigs = getattr(viewType, '__xview_signals__', [])
        XView.dispatch(location).registerSignals(sigs)

        location = nativestring(location)
        XView._registry.setdefault(location, {})
        XView._registry[location][viewType.viewName()] = viewType
        XView.dispatch(location).emit('registeredView(QVariant)', viewType)

    @staticmethod
    def unregisterView(viewType, location='Central'):
        """
        Unregisteres the given view type from the inputed location.
        
        :param      viewType | <subclass of XView>
        """
        XView._registry.get(location, {}).pop(viewType.viewName(), None)
        XView.dispatch(location).emit('unregisteredView(QVariant)', viewType)

    @staticmethod
    def setGlobal(key, value):
        """
        Shares a global value across all views by setting the key in the \
        globals dictionary to the inputed value.
        
        :param      key     | <str> 
                    value   | <variant>
        """
        XView._globals[key] = value
Exemple #10
0
class Index(object):
    """ 
    Defines an indexed way to lookup information from a database.
    Creating an Index generates an object that works like a method, however
    has a preset query built into it, along with caching options.
    """

    Flags = enum('Unique', 'Private', 'Static')

    def __json__(self):
        output = {
            'name': self.__name,
            'dbname': self.__dbname,
            'columns': self.__columns,
            'flags': {k: True
                      for k in self.Flags.toSet(self.__flags)},
            'order': self.__order
        }
        return output

    def __init__(self, columns=None, name='', dbname='', flags=0, order=None):
        self.__name = self.__name__ = name
        self.__dbname = dbname
        self.__columns = columns or []
        self.__flags = self.Flags.fromSet(flags) if isinstance(flags,
                                                               set) else flags
        self.__order = order
        self.__schema = None

    def __call__(self, model, *values, **context):
        # make sure we have the right number of arguments
        if len(values) != len(self.__columns):
            name = self.__name
            columnCount = len(self.__columns)
            valueCount = len(values)
            opts = (name, columnCount, valueCount)
            text = '%s() takes exactly %i arguments (%i given)' % opts
            raise TypeError(text)

        # create the lookup query
        schema = model.schema()
        query = orb.Query()
        for i, col in enumerate(self.__columns):
            value = values[i]
            column = schema.column(col)

            if isinstance(value, orb.Model) and not value.isRecord():
                return None if self.testFlag(
                    self.Flags.Unique) else orb.Collection()
            elif not column:
                raise errors.ColumnNotFound(schema.name(), col)

            query &= orb.Query(col) == value

        context['where'] = query & context.get('where')

        records = model.select(**context)
        return records.first() if self.testFlag(self.Flags.Unique) else records

    def copy(self):
        other = type(self)(columns=self.__columns[:],
                           name=self.__name,
                           dbname=self.__dbname,
                           flags=self.__flags,
                           order=self.__order)
        return other

    def columns(self):
        """
        Returns the list of column names that this index will be expecting as \
        inputs when it is called.
        
        :return     [<str>, ..]
        """
        schema = self.schema()
        return [schema.column(col) for col in self.__columns]

    def dbname(self):
        return self.__dbname or orb.system.syntax().indexdb(
            self.__schema, self.__name)

    def flags(self):
        return self.__flags

    def name(self):
        """
        Returns the name of this index.
        
        :return     <str>
        """
        return self.__name

    def schema(self):
        return self.__schema

    def setColumns(self, columns):
        """
        Sets the list of the column names that this index will use when \
        looking of the records.
        
        :param      columns | [<str>, ..]
        """
        self.__columns = columns

    def setOrder(self, order):
        """
        Sets the order information for this index for how to sort and \
        organize the looked up data.
        
        :param      order   | [(<str> field, <str> direction), ..]
        """
        self.__order = order

    def setDbName(self, dbname):
        self.__dbname = dbname

    def setFlags(self, flags):
        self.__flags = flags

    def setName(self, name):
        """
        Sets the name for this index to this index.
        
        :param      name    | <str>
        """
        self.__name = self.__name__ = name

    def setSchema(self, schema):
        self.__schema = schema

    def validate(self, record, values):
        """
        Validates whether or not this index's requirements are satisfied by the inputted record and
        values.  If this index fails validation, a ValidationError will be raised.

        :param      record | subclass of <orb.Table>
                    values | {<orb.Column>: <variant>, ..}

        :return     <bool>
        """
        schema = record.schema()
        columns = self.columns()
        try:
            column_values = [values[col] for col in columns]
        except KeyError as err:
            msg = 'Missing {0} from {1}.{2} index'.format(
                err[0].name(),
                record.schema().name(), self.name())
            raise errors.IndexValidationError(self, msg=msg)

        # # ensure a unique record is preserved
        # if self.unique():
        #     lookup = getattr(record, self.name())
        #     other = lookup(*column_values)
        #     if other and other != record:
        #         msg = 'A record already exists with the same {0} combination.'.format(', '.join(self.columnNames()))
        #         raise errors.IndexValidationError(self, msg=msg)

        return True

    def testFlag(self, flags):
        return (self.__flags & flags) > 0
Exemple #11
0
class XView(QWidget):
    activated               = qt.Signal()
    currentStateChanged     = qt.Signal()
    windowTitleChanged      = qt.Signal(str)
    sizeConstraintChanged   = qt.Signal()
    initialized             = qt.Signal()
    visibleStateChanged     = qt.Signal(bool)
    
    _registry       = {}
    _globals        = {}
    
    _viewName           = ''
    _viewGroup          = 'Default'
    _viewIcon           = resources.find('img/view/view.png')
    _viewSingleton      = False
    _popupSingleton     = True
    _popupInstance      = None
    
    _currentViewRef     = None
    _instances          = None
    _instanceSingleton  = None
    _dispatcher         = None
    _dispatch           = {}
    
    __designer_icon__ = resources.find('img/ui/view.png')
    
    SignalPolicy        = enum('BlockIfNotCurrent',
                               'BlockIfNotInGroup',
                               'BlockIfNotVisible',
                               'BlockIfNotInitialized',
                               'NeverBlock')
    
    def __init__(self, parent, autoKillThreads=True):
        super(XView, self).__init__( parent )
        
        if not self._viewSingleton:
            self.setAttribute(Qt.WA_DeleteOnClose)
        
        # define custom properties
        self._initialized = False
        self._destroyThreadsOnClose = True
        self._viewingGroup = 0
        self._signalPolicy = XView.SignalPolicy.BlockIfNotInitialized | \
                             XView.SignalPolicy.BlockIfNotVisible | \
                             XView.SignalPolicy.BlockIfNotInGroup
        
        self._visibleState = False  # storing this state for knowing if a
                                    # widget WILL be visible once Qt finishes
                                    # processing for purpose of signal
                                    # validation.
        
        # setup default properties
        self.setFocusPolicy( Qt.StrongFocus )
        self.setWindowTitle( self.viewName() )
        self.registerInstance(self)
        
        if autoKillThreads:
            QApplication.instance().aboutToQuit.connect(self.killChildThreads)
    
    def canClose( self ):
        return True
    
    def closeEvent( self, event ):
        if self.isViewSingleton():
            self.setParent(None)
        
        # make sure to destroy any threads running on close
        if self.testAttribute(Qt.WA_DeleteOnClose) and \
           self.destroyThreadsOnClose():
            self.killChildThreads()
        
        # clear out any progress loaders
        XLoaderWidget.stopAll(self)
        
        # remove any registered instances
        if self.testAttribute(Qt.WA_DeleteOnClose):
            self.unregisterInstance(self)
        
        super(XView, self).closeEvent(event)
    
    def dispatchConnect( self, signal, slot ):
        """
        Connect the slot for this view to the given signal that gets
        emitted by the XView.dispatch() instance.
        
        :param      signal | <str>
                    slot   | <callable>
        """
        XView.dispatch().connect(signal, slot)
    
    def dispatchEmit( self, signal, *args ):
        """
        Emits the given signal via the XView dispatch instance with the
        given arguments.
        
        :param      signal | <str>
                    args   | <tuple>
        """
        XView.setGlobal('emitGroup', self.viewingGroup())
        XView.dispatch().emit(signal, *args)
    
    def destroyThreadsOnClose( self ):
        """
        Marks whether or not any child threads should be destroyed when \
        this view is closed.
        
        :return     <bool>
        """
        return self._destroyThreadsOnClose
    
    def duplicate( self, parent ):
        """
        Duplicates this current view for another.  Subclass this method to 
        provide any additional duplication options.
        
        :param      parent | <QWidget>
        
        :return     <XView> | instance of this class
        """
        # only return a single singleton instance
        if ( self.isViewSingleton() ):
            return self
            
        output = type(self).createInstance(parent)
        
        # save/restore the current settings
        xdata = ElementTree.Element('data')
        self.saveXml(xdata)
        new_name = output.objectName()
        output.setObjectName(self.objectName())
        output.restoreXml(xdata)
        output.setObjectName(new_name)
        
        return output
    
    def killChildThreads(self):
        """
        Kills all child threads for this view.
        """
        threads = self.findChildren(QThread)
        for thread in threads:
            thread.finished.connect(thread.deleteLater)
            thread.quit()
            thread.wait(100)
    
    def hideEvent(self, event):
        """
        Sets the visible state for this widget.  If it is the first time this
        widget will be visible, the initialized signal will be emitted.
        
        :param      state | <bool>
        """
        super(XView, self).hideEvent(event)
        
        # record the visible state for this widget to be separate of Qt's
        # system to know if this view WILL be visible or not once the 
        # system is done processing.  This will affect how signals are
        # validated as part of the visible slot delegation
        self._visibleState = False
        
        if not self.signalsBlocked():
            self.visibleStateChanged.emit(False)
    
    def initialize(self, force=False):
        """
        Initializes the view if it is visible or being loaded.
        """
        if force or (self.isVisible() and \
                     not self.isInitialized() and \
                     not self.signalsBlocked()):
            
            self._initialized = True
            self.initialized.emit()
    
    def isCurrent( self ):
        return self == self.currentView()
    
    def isInitialized(self):
        """
        Returns whether or not this view has been initialized.  A view will
        be initialized the first time it becomes visible to the user.  You
        can use this to delay loading of information until it is needed by
        listening for the initialized signal.
        
        :return     <bool>
        """
        return self._initialized
    
    @deprecatedmethod('XView', 'Use restoreXml instead.')
    def restoreSettings( self, settings ):
        """
        Restores the settings for this view from the inputed QSettings.
        
        :param      settings | <QSettings>
        """
        pass
    
    def restoreXml( self, xml ):
        """
        Restores the settings for this view from the inputed XML node.
        
        :param      xml | <xml.etree.ElementTree.Element>
        """
        pass
    
    @deprecatedmethod('XView', 'Use saveXml instead.')
    def saveSettings( self, settings ):
        """
        Saves the current settings for this view to the inputed QSettings.
        
        :param      settings | <QSettings>
        """
        pass
    
    def saveXml( self, xml ):
        """
        Saves the settings for this view to the inputed XML node.
        
        :param      xml | <xml.etree.ElementTree.Element>
        """
        pass
    
    def settingsName( self ):
        """
        Returns the default settings name for this view.
        
        :return     <str>
        """
        return 'Views/%s' % self.objectName()
    
    def setCurrent( self, state = True ):
        """
        Marks this view as the current source based on the inputed flag.  \
        This method will return True if the currency changes.
        
        :return     <bool>
        """
        if ( state ):
            changed = self.setCurrentView(self)
            
        elif ( self.currentView() == self ):
            changed = self.setCurrentView(None)
        
        if ( changed and not self.signalsBlocked() ):
            self.currentStateChanged.emit()
            if state:
                self.activated.emit()
        
        return changed
    
    def setDestroyThreadsOnClose( self, state ):
        """
        Marks whether or not any child threads should be destroyed when \
        this view is closed.
        
        :param     state | <bool>
        """
        self._destroyThreadsOnClose = state
    
    def setFixedHeight( self, height ):
        """
        Sets the maximum height value to the inputed height and emits the \
        sizeConstraintChanged signal.
        
        :param      height | <int>
        """
        super(XView, self).setFixedHeight(height)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setFixedWidth( self, width ):
        """
        Sets the maximum width value to the inputed width and emits the \
        sizeConstraintChanged signal.
        
        :param      width | <int>
        """
        super(XView, self).setFixedWidth(width)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setMaximumHeight( self, height ):
        """
        Sets the maximum height value to the inputed height and emits the \
        sizeConstraintChanged signal.
        
        :param      height | <int>
        """
        super(XView, self).setMaximumHeight(height)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setMaximumSize( self, *args ):
        """
        Sets the maximum size value to the inputed size and emits the \
        sizeConstraintChanged signal.
        
        :param      *args | <tuple>
        """
        super(XView, self).setMaximumSize(*args)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setMaximumWidth( self, width ):
        """
        Sets the maximum width value to the inputed width and emits the \
        sizeConstraintChanged signal.
        
        :param      width | <int>
        """
        super(XView, self).setMaximumWidth(width)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setMinimumHeight( self, height ):
        """
        Sets the minimum height value to the inputed height and emits the \
        sizeConstraintChanged signal.
        
        :param      height | <int>
        """
        super(XView, self).setMinimumHeight(height)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setMinimumSize( self, *args ):
        """
        Sets the minimum size value to the inputed size and emits the \
        sizeConstraintChanged signal.
        
        :param      *args | <tuple>
        """
        super(XView, self).setMinimumSize(*args)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setMinimumWidth( self, width ):
        """
        Sets the minimum width value to the inputed width and emits the \
        sizeConstraintChanged signal.
        
        :param      width | <int>
        """
        super(XView, self).setMinimumWidth(width)
        
        if ( not self.signalsBlocked() ):
            self.sizeConstraintChanged.emit()
    
    def setSignalPolicy( self, policy ):
        """
        Sets the signal delegation policy for this instance to the given 
        policy.  By default, signals will be delegates for groups or
        by currency if they are not in a group.  This will not directly
        affect signal propogation, only the result of the validateSignal
        method, so if you want to test against this, then you will need
        to check in your slot.
        
        :param      policy | <XView.SignalPolicy>
        """
        self._signalPolicy = policy
    
    def setViewingGroup( self, grp ):
        """
        Sets the viewing group that this view is associated with.
        
        :param      grp | <int>
        """
        self._viewingGroup = grp
    
    def setWindowTitle( self, title ):
        """
        Sets the window title for this view, and emits the windowTitleChanged \
        signal if the signals are not blocked.  Setting this title will update \
        the tab title for the view within the widget.
        
        :param      title | <str>
        """
        super(XView, self).setWindowTitle(title)
        if ( not self.signalsBlocked() ):
            self.windowTitleChanged.emit(title)
    
    def showActiveState( self, state ):
        """
        Shows this view in the active state based on the inputed state settings.
        
        :param      state | <bool>
        """
        self.setAutoFillBackground(True)
        
        palette = self.window().palette()
        clr = palette.color(palette.Window)
        avg = (clr.red() + clr.green() + clr.blue()) / 3
        
        if ( avg < 180 and state ):
            clr = clr.lighter(105)
        elif ( not state ):
            clr = clr.darker(105)
        
        palette.setColor(palette.Window, clr)
        self.setPalette(palette)
    
    def showEvent(self, event):
        """
        Sets the visible state for this widget.  If it is the first time this
        widget will be visible, the initialized signal will be emitted.
        
        :param      state | <bool>
        """
        super(XView, self).showEvent(event)
        
        # record the visible state for this widget to be separate of Qt's
        # system to know if this view WILL be visible or not once the 
        # system is done processing.  This will affect how signals are
        # validated as part of the visible slot delegation
        self._visibleState = True
        
        if not self.isInitialized():
            self.initialize()
        
        # after the initial time the view is loaded, the visibleStateChanged
        # signal will be emitted
        elif not self.signalsBlocked():
            self.visibleStateChanged.emit(True)
            
    def signalPolicy( self ):
        """
        Returns the signal policy for this instance.
        
        :return     <XView.SignalPolicy>
        """
        return self._signalPolicy
    
    def rootWidget( self ):
        widget = self
        while ( widget.parent() ):
            widget = widget.parent()
        return widget
    
    def validateSignal( self, policy = None ):
        """
        Validates that this view is part of the group that was emitting
        the signal.  Views that are not in any viewing group will accept
        all signals.
        
        :param      policy | <XView.SignalPolicy> || None
        
        :return     <bool>
        """
        # validate whether or not to process a signal
        if policy is None:
            policy = self.signalPolicy()
        
        group_check   = XView.getGlobal('emitGroup') == self.viewingGroup()
        current_check = self.isCurrent()
        
        # always delegate signals if they are not set to block,
        # or if the method is called directly (not from a signal)
        if not self.sender() or policy & XView.SignalPolicy.NeverBlock:
            return True
        
        # block delegation of the signal if the view is not initialized
        elif policy & XView.SignalPolicy.BlockIfNotInitialized and \
             not self.isInitialized():
            return False
        
        # block delegation if the view is not visible
        elif policy & XView.SignalPolicy.BlockIfNotVisible and \
            not self._visibleState:
            return False
        
        # block delegation if the view is not part of a group
        elif self.viewingGroup() and \
             policy & XView.SignalPolicy.BlockIfNotInGroup:
            return group_check
        
        # look for only currency releated connections
        elif policy & XView.SignalPolicy.BlockIfNotCurrent:
            return current_check
        
        else:
            return True
        
    def viewingGroup( self ):
        """
        Returns the viewing group that this view is assigned to.
        
        :return     <int>
        """
        return self._viewingGroup
    
    @classmethod
    def currentView( cls ):
        # look for last focused view
        if ( cls._currentViewRef ):
            inst = cls._currentViewRef()
            if ( inst ):
                return inst
        
        cls._currentViewRef = None
        return None
    
    @classmethod
    def createInstance( cls, parent ):
        # assign the singleton instance
        if ( cls._instanceSingleton ):
            instance = cls._instanceSingleton()
            
            # make sure the instance is still in use
            if ( not instance ):
                cls._instanceSingleton = None
            else:
                instance.setParent(parent)
                return instance
        
        # determine if we need to store a singleton
        inst = cls(parent)
        inst.setObjectName( cls.uniqueName() )
        if ( cls.isViewSingleton() ):
            cls._instanceSingleton = weakref.ref(inst)
        
        return inst
    
    @classmethod
    def destroyInstance( cls, inst ):
        if ( cls.isViewSingleton() ):
            inst.close()
            return
        
        inst.close()
        inst.setParent(None)
        inst.deleteLater()
    
    @classmethod
    @deprecatedmethod('XView', 'Use the XView.dispatch() syntax now.')
    def dispatcher( cls ):
        if ( not cls._dispatcher ):
            cls._dispatcher = cls.MetaDispatcher(cls)
            
        return cls._dispatcher
    
    @classmethod
    def instances( cls ):
        """
        Returns all generated instances of a particular view type.
        
        :return     [<XView>, ..]
        """
        if ( not cls._instances ):
            return []
        
        # purge the instance list
        insts = []
        refs  = []
        for ref in cls._instances:
            inst = ref()
            if ( not inst ):
                continue
            
            insts.append(inst)
            refs.append(ref)
        
        cls._instances = refs
        return insts
    
    @classmethod
    def isViewSingleton( cls ):
        return cls._viewSingleton
    
    @classmethod
    def isPopupSingleton( cls ):
        return cls._popupSingleton
    
    @classmethod
    def popup( cls, parent = None ):
        """
        Pops up this view as a new dialog.  If the forceDialog flag is set to \
        False, then it will try to activate the first instance of the view \
        within an existing viewwidget context before creating a new dialog.
        
        :param      parent      | <QWidget> || None
        """
        # popup the singleton view for this class
        if ( cls.isViewSingleton() ):
            inst = cls.currentView()
            if ( not inst ):
                inst = cls.createInstance(parent)
                
            inst.setWindowFlags(Qt.Dialog)
            inst.show()
        
        # popup the popupSingleton for this class
        inst = cls.popupInstance(parent)
        inst.show()
    
    @classmethod
    def popupInstance( cls, parent ):
        if ( cls._popupInstance ):
            return cls._popupInstance
        
        inst = cls.createInstance(parent)
        inst.setWindowFlags(Qt.Dialog)
        if cls.isPopupSingleton():
            inst.setAttribute(Qt.WA_DeleteOnClose, False)
            cls._popupInstance = inst
        
        return inst
    
    @classmethod
    def registerInstance( cls, instance ):
        if ( not cls._instances ):
            cls._instances = []
        
        cls._instances.append(weakref.ref(instance))
    
    @classmethod
    def registerToWindow( cls, window ):
        """
        Registers this view to the window to update additional menu items, \
        actions, and toolbars.
        
        :param      window | <QWidget>
        """
        pass
    
    @classmethod
    def restoreGlobalSettings( cls, settings ):
        """
        Restores the global settings for the inputed view class type.
        
        :param      cls      | <subclass of XView>
                    settings | <QSettings>
        """
        pass
    
    @classmethod
    def saveGlobalSettings( cls, settings ):
        """
        Saves the global settings for the inputed view class type.
        
        :param      cls      | <subclass of XView>
                    settings | <QSettings>
        """
        pass
    
    @classmethod
    def setViewGroup( cls, grp ):
        cls._viewGroup = grp
    
    @classmethod
    def setCurrentView( cls, view ):
        current = cls.currentView()
        if ( current == view ):
            return False
            
        elif ( current ):
            current.showActiveState(False)
        
        if ( view ):
            view.showActiveState(True)
        
        cls._currentViewRef = weakref.ref(view)
        return True
    
    @classmethod
    def setViewIcon( cls, icon ):
        cls._viewIcon = icon
    
    @classmethod
    def setViewName( cls, name ):
        cls._viewName = name
    
    @classmethod
    def setViewSingleton( cls, state ):
        cls._viewSingleton = state
    
    @classmethod
    def setPopupSingleton( cls, state ):
        cls._popupSingleton = state
    
    @classmethod
    def uniqueName( cls ):
        names = map(lambda x: str(x.objectName()), cls.instances())
        index = 1
        base  = cls.viewName()
        name  = '%s%02i' % (base, index)
        
        while ( name in names ):
            index += 1
            name = '%s%02i' % (base, index)
        
        return name
    
    @classmethod
    def unregisterInstance( cls, instance ):
        if ( not cls._instances ):
            return
        
        if ( cls._currentViewRef and instance == cls._currentViewRef() ):
            cls._currentViewRef = None
        
        refs = []
        for ref in cls._instances:
            inst = ref()
            if ( not inst or inst == instance ):
                continue
            
            refs.append(ref)
        
        cls._instances = refs
    
    @classmethod
    def unregisterToWindow( cls, window ):
        """
        Registers this view to the window to update additional menu items, \
        actions, and toolbars.
        
        :param      window | <QWidget>
        """
        pass
    
    @classmethod
    def viewGroup( cls ):
        return cls._viewGroup
    
    @classmethod
    def viewIcon( cls ):
        return cls._viewIcon
    
    @classmethod
    def viewName( cls ):
        return cls._viewName
    
    @classmethod
    def viewTypeName( cls ):
        """
        Returns the unique name for this view type by joining its group with \
        its name.
        
        :return     <str>
        """
        return '%s.%s' % (cls.viewGroup(), cls.viewName())
    
    #--------------------------------------------------------------------------
    
    @staticmethod
    def dispatch( location = 'Central' ):
        """
        Returns the instance of the global view dispatching system.  All views \
        will route their signals through the central hub so no single view \
        necessarily depends on another.
        
        :return     <XViewDispatch>
        """
        dispatch = XView._dispatch.get(str(location))
        if ( not dispatch ):
            dispatch = XViewDispatch(QApplication.instance())
            XView._dispatch[str(location)] = dispatch
        
        return dispatch
        
    @staticmethod
    def getGlobal( key, default = None ):
        """
        Returns the global value for the inputed key.
        
        :param      key     | <str>
                    default | <variant>
        
        :return     <variant>
        """
        return XView._globals.get(key, default)
    
    @staticmethod
    def registeredView( viewName, location = 'Central' ):
        """
        Returns the view that is registered to the inputed location for the \
        given name.
        
        :param      viewName | <str>
                    location | <str>
        
        :return     <subclass of XView> || None
        """
        for viewType in XView._registry.get(str(location), []):
            if ( viewType.viewName() == viewName ):
                return viewType
        return None
    
    @staticmethod
    def registeredViews( location = 'Central' ):
        """
        Returns all the views types that have bene registered to a particular \
        location.
        
        :param      location | <str>
        
        :return     [<subclass of XView>, ..]
        """
        return XView._registry.get(str(location), [])
        
    @staticmethod
    def registerView( viewType, location = 'Central' ):
        """
        Registers the inputed view type to the given location.  The location \
        is just a way to group and organize potential view plugins for a \
        particular widget, and is determined per application.  This eases \
        use when building a plugin based system.  It has no relevance to the \
        XView class itself where you register a view.
        
        :param      viewType | <subclass of XView>
        """
        # update the dispatch signals
        if ( '__xview_signals__' in viewType.__dict__ ):
            XView.dispatch(location).registerSignals(viewType.__xview_signals__)
            
        location = str(location)
        XView._registry.setdefault(location, [])
        XView._registry[str(location)].append(viewType)
    
    @staticmethod
    def setGlobal( key, value ):
        """
        Shares a global value across all views by setting the key in the \
        globals dictionary to the inputed value.
        
        :param      key     | <str> 
                    value   | <variant>
        """
        XView._globals[key] = value
    
    @staticmethod
    def updateCurrentView( oldWidget, newWidget ):
        """
        Updates the current view for each view class type based on the new \
        focused widget.
        
        :param      oldWidget | <QWidget>
                    newWidget | <QWidget>
        """
        widget = newWidget
        
        while widget:
            if isinstance(widget, XView):
                widget.setCurrent()
                break
                
            widget = widget.parent()
class XGanttWidget(QWidget):
    dateRangeChanged = qt.Signal()

    Timescale = enum('Week', 'Month', 'Year')

    def __init__(self, parent=None, _availabilityEnabled=0):
        super(XGanttWidget, self).__init__(parent)
        '''
	menubar = QMenuBar(self)
	#menubar.sizeHint(QSize.setHeight(10))
	
	fileMenu = menubar.addMenu('&File')
	
	fileMenu.addAction('Create Project')
	fileMenu.addSeparator()
	fileMenu.addAction('Save')
	fileMenu.addSeparator()
	fileMenu.addAction('Exit')
	fileMenu.triggered.connect( self.fileMenuActions )
	'''

        # load the user interface
        if getattr(sys, 'frozen', None):
            #print (sys._MEIPASS+"/ui/xganttwidget.ui");
            projexui.loadUi(sys._MEIPASS,
                            self,
                            uifile=(sys._MEIPASS + "/ui/xganttwidget.ui"))

        else:
            projexui.loadUi(__file__, self)

        # define custom properties
        self._backend = None
        self._dateStart = QDate.currentDate()
        self._dateEnd = QDate.currentDate().addMonths(12)
        self._alternatingRowColors = False
        self._cellWidth = 15
        self._cellHeight = 15
        self._first = True
        self._dateFormat = 'M/d/yy'
        self._timescale = XGanttWidget.Timescale.Year
        self._scrolling = False

        # setup the palette colors
        palette = self.palette()
        color = palette.color(palette.Base)

        self._gridPen = QPen(color.darker(135))
        self._brush = QBrush(color)
        self._alternateBrush = QBrush(color.darker(105))
        self._currentDayBrush = QBrush(QColor(146, 252, 186))
        self._holidayBrush = QBrush(QColor(166, 46, 46))
        self._bookedBrush = QBrush(QColor(20, 250, 0))
        self._unavailableBrush = QBrush(QColor(75, 75, 75))
        self._underbookedBrush = QBrush(QColor(255, 255, 20))
        self._overbookedBrush = QBrush(QColor(255, 25, 25))
        self._overbookedAmount = {}
        self._unassignedBrush = QBrush(QColor(25, 25, 255))

        weekendColor = color.darker(148)

        self._availabilityEnabled = _availabilityEnabled

        self._weekendBrush = QBrush(weekendColor)

        # setup the columns for the tree
        if _availabilityEnabled:
            self.setColumns(['Name'])
        else:
            self.setColumns(
                ['Name', 'Start', 'End', 'Calendar Days', 'Work Days'])
        header = self.uiGanttTREE.header()
        header.setFixedHeight(self._cellHeight * 2)
        header.setResizeMode(0, header.ResizeToContents)
        header.setDefaultSectionSize(60)
        headerItem = self.uiGanttTREE.headerItem()
        headerItem.setSizeHint(0, QSize(10, header.height()))

        self.uiGanttTREE.setContextMenuPolicy(Qt.CustomContextMenu)

        # connect signals
        self.uiGanttTREE.customContextMenuRequested.connect(
            self.showProjectMenu)

        # initialize the tree widget
        self.uiGanttTREE.setShowGrid(False)

        #enable drag and drop
        self.uiGanttTREE.setDragDropFilter(
            self.uiGanttTREE.setDragDropFilter(XGanttWidget.handleDragDrop))

        if (sharedDB.currentUser._idPrivileges == 3):
            self.uiGanttTREE.setEditable(False)
        for act in sharedDB.mainWindow._fileMenu.actions():
            if act.text() == "Save" or act.text() == "Create Project":
                act.setEnabled(False)

        else:
            self.uiGanttTREE.setEditable(True)

        self.uiGanttTREE.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.uiGanttTREE.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.uiGanttTREE.setVerticalScrollMode(self.uiGanttTREE.ScrollPerPixel)

        #left half size
        self.uiGanttTREE.resize(400, 20)

        # initialize the view widget
        #self.uiGanttVIEW.setDragMode( self.uiGanttVIEW.RubberBandDrag )
        self.uiGanttVIEW.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.uiGanttVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.uiGanttVIEW.setScene(XGanttScene(self))
        self.uiGanttVIEW.installEventFilter(self)
        #self.uiGanttVIEW.horizontalScrollBar().setValue(50)

        # create connections
        self.uiGanttTREE.itemExpanded.connect(self.syncView)
        self.uiGanttTREE.itemCollapsed.connect(self.syncView)

        # connect scrollbars
        tree_bar = self.uiGanttTREE.verticalScrollBar()
        view_bar = self.uiGanttVIEW.verticalScrollBar()

        tree_bar.rangeChanged.connect(self.__updateViewRect)
        tree_bar.valueChanged.connect(self.__scrollView)
        view_bar.valueChanged.connect(self.__scrollTree)

        # connect selection
        self.uiGanttTREE.itemSelectionChanged.connect(self.__selectView)
        self.uiGanttVIEW.scene().selectionChanged.connect(self.__selectTree)
        self.uiGanttTREE.itemChanged.connect(self.updateItemData)

        if self._availabilityEnabled:
            self._currentDayBrush = None
            self._holidayBrush = QBrush(QColor(75, 75, 75))
            weekendColor = QBrush(QColor(75, 75, 75))
            self.uiGanttTREE.setEditable(False)
            #self._cellHeight = 12

    def __del__(self):
        self.uiGanttVIEW.scene().selectionChanged.disconnect(self.__selectTree)

    def __scrollTree(self, value):
        """
	Updates the tree view scrolling to the inputed value.
	
	:param      value | <int>
	"""
        if (self._scrolling):
            return

        tree_bar = self.uiGanttTREE.verticalScrollBar()
        self._scrolling = True
        tree_bar.setValue(value)
        self._scrolling = False

    def __scrollView(self, value):
        """
	Updates the gantt view scrolling to the inputed value.
	
	:param      value | <int>
	"""
        if (self._scrolling):
            return

        view_bar = self.uiGanttVIEW.verticalScrollBar()
        self._scrolling = True
        view_bar.setValue(value)
        self._scrolling = False

    def __selectTree(self):
        """
	Matches the tree selection to the views selection.
	"""
        self.uiGanttTREE.blockSignals(True)
        self.uiGanttTREE.clearSelection()
        for item in self.uiGanttVIEW.scene().selectedItems():
            item.treeItem().setSelected(True)
            sharedDB.sel.select([item.treeItem()._dbEntry])
        self.uiGanttTREE.blockSignals(False)

    def __selectView(self):
        """
	Matches the view selection to the trees selection.
	"""
        self.uiGanttVIEW.scene().blockSignals(True)
        self.uiGanttVIEW.scene().clearSelection()
        for item in self.uiGanttTREE.selectedItems():
            item.viewItem().setSelected(True)
            sharedDB.sel.select([item._dbEntry])
        self.uiGanttVIEW.scene().blockSignals(False)

    def __updateViewRect(self):
        """
	Updates the view rect to match the current tree value.
	"""
        header_h = self._cellHeight * 2
        rect = self.uiGanttVIEW.scene().sceneRect()
        sbar_max = self.uiGanttTREE.verticalScrollBar().maximum()
        sbar_max += self.uiGanttTREE.viewport().height() + header_h
        widget_max = self.uiGanttVIEW.height()
        widget_max -= (self.uiGanttVIEW.horizontalScrollBar().height() + 10)

        rect.setHeight(max(widget_max, sbar_max))
        self.uiGanttVIEW.scene().setSceneRect(rect)

    def addTopLevelItem(self, item):
        """
	Adds the inputed item to the gantt widget.
	
	:param      item | <XGanttWidgetItem>
	"""
        vitem = item.viewItem()

        self.treeWidget().addTopLevelItem(item)
        if not self._availabilityEnabled:
            self.viewWidget().scene().addItem(vitem)

            item._viewItem = weakref.ref(vitem)

        #set scrollbar offset
        #item.treeWidget._scrollBar = self.uiGanttTREE.verticalScrollBar()

        item.sync(recursive=True)

    def alternateBrush(self):
        """
	Returns the alternate brush to be used for the grid view.
	
	:return     <QBrush>
	"""
        return self._alternateBrush

    def alternatingRowColors(self):
        """
	Returns whether or not this widget should show alternating row colors.
	
	:return     <bool>
	"""
        return self._alternatingRowColors

    def brush(self):
        """
	Returns the background brush to be used for the grid view.
	
	:return     <QBrush>
	"""
        return self._brush

    def cellHeight(self):
        """
	Returns the height for the cells in this gantt's views.
	
	:return     <int>
	"""
        return self._cellHeight

    def cellWidth(self):
        """
	Returns the width for the cells in this gantt's views.
	
	:return     <int>
	"""
        return self._cellWidth

    def clear(self):
        """
	Clears all the gantt widget items for this widget.
	"""
        self.uiGanttTREE.clear()
        self.uiGanttVIEW.scene().clear()

    def closeEvent(self, event):
        if sharedDB.changesToBeSaved and sharedDB.users.currentUser._idPrivileges != 3:
            quit_msg = "Save before exit?"
            reply = QtGui.QMessageBox.question(self, 'Message', quit_msg,
                                               QtGui.QMessageBox.Yes,
                                               QtGui.QMessageBox.No,
                                               QtGui.QMessageBox.Cancel)

            if reply == QtGui.QMessageBox.Yes:
                self.SaveToDatabase()
                event.accept()
            elif reply == QtGui.QMessageBox.No:
                event.accept()
            else:
                event.ignore()

    def columns(self):
        """
	Returns a list of the columns being used in the treewidget of this gantt
	chart.
	
	:return     [<str>, ..]
	"""
        return self.treeWidget().columns()

    '''def CreateProject(self):
	#self._myCreateProjectWidget = CreateProjectWidget()
	#self._myCreateProjectWidget.show()
	sharedDB.app.CreateProjectWidget() 
    '''

    def dateEnd(self):
        """
	Returns the date end for this date range of this gantt widget.
	
	:return     <QDate>
	"""
        return self._dateEnd

    def dateFormat(self):
        """
	Returns the date format that will be used when rendering items in the
	view.
	
	:return     <str>
	"""
        return self._dateFormat

    def dateStart(self):
        """
	Returns the date start for the date range of this gantt widget.
	
	:return     <QDate>
	"""
        return self._dateStart

    def emitDateRangeChanged(self):
        """
	Emits the date range changed signal provided signals aren't being
	blocked.
	"""
        if (not self.signalsBlocked()):
            self.dateRangeChanged.emit()

    def collapseAllTrees(self):
        self.uiGanttTREE.blockSignals(True)
        #for x in range(0,self.treeWidget().topLevelItemCount()):
        #self.treeWidget().topLevelItem(x).setExpanded(True)
        self.treeWidget().collapseAll()
        self.uiGanttTREE.blockSignals(False)
        self.syncView()

    def expandAllTrees(self):
        self.uiGanttTREE.blockSignals(True)
        #for x in range(0,self.treeWidget().topLevelItemCount()):
        #self.treeWidget().topLevelItem(x).setExpanded(True)
        self.treeWidget().expandAll()
        self.uiGanttTREE.blockSignals(False)
        self.syncView()

    def eventFilter(self, object, event):
        if (event.type() == event.Resize):
            self.__updateViewRect()
        return False

    def frameCurrentDate(self):
        # Subtract start date from current date
        prerollDays = self._dateStart.day() - QDate.currentDate().day()
        #set scroll to multiply difference against cel width

        view_bar = self.uiGanttVIEW.horizontalScrollBar()
        self._scrolling = True
        view_bar.setMaximum(1)
        view_bar.setMinimum(0)
        #print view_bar.maximum()
        view_bar.setValue(1)
        #view_bar.update()
        #self.update()
        #self.uiGanttVIEW.update()
        self._scrolling = False

        #self.__scrollView(self._cellWidth * prerollDays)

    def gridPen(self):
        """
	Returns the pen that this widget uses to draw in the view.
	
	:return     <QPen>
	"""
        return self._gridPen

    @staticmethod
    def handleDragDrop(object, event):
        if (event.type() == QEvent.DragEnter):
            event.acceptProposedActions()
        elif (event.type() == QEvent.Drop):
            print 'dropping'

    def indexOfTopLevelItem(self, item):
        """
	Returns the index for the inputed item from the tree.
	
	:return     <int>
	"""
        return self.treeWidget().indexOfTopLevelItem(item)

    def insertTopLevelItem(self, index, item):
        """
	Inserts the inputed item at the given index in the tree.
	
	:param      index   | <int>
		    item    | <XGanttWidgetItem>
	"""
        self.treeWidget().insertTopLevelItem(index, item)

        item.sync(recursive=True)

    def setAlternateBrush(self, brush):
        """
	Sets the alternating brush used for this widget to the inputed brush.
	
	:param      brush | <QBrush> || <QColor>
	"""
        self._alternateBrush = QBrush(brush)

    def setAlternatingRowColors(self, state):
        """
	Sets the alternating row colors state for this widget.
	
	:param      state | <bool>
	"""
        self._alternatingRowColors = state

        self.treeWidget().setAlternatingRowColors(state)

    def setBrush(self, brush):
        """
	Sets the main background brush used for this widget to the inputed
	brush.
	
	:param      brush | <QBrush> || <QColor>
	"""
        self._brush = QBrush(brush)

    def setCellHeight(self, cellHeight):
        """
	Sets the height for the cells in this gantt's views.
	
	:param      cellHeight | <int>
	"""
        self._cellHeight = cellHeight

    def setCellWidth(self, cellWidth):
        """
	Sets the width for the cells in this gantt's views.
	
	:param      cellWidth | <int>
	"""
        self._cellWidth = cellWidth

    def setColumns(self, columns):
        """
	Sets the columns for this gantt widget's tree to the inputed list of
	columns.
	
	:param      columns | {<str>, ..]
	"""
        self.treeWidget().setColumns(columns)
        item = self.treeWidget().headerItem()
        for i in range(item.columnCount()):
            item.setTextAlignment(i, Qt.AlignBottom | Qt.AlignHCenter)

    def setDateEnd(self, dateEnd):
        """
	Sets the end date for the range of this gantt widget.
	
	:param      dateEnd | <QDate>
	"""
        self._dateEnd = dateEnd
        self.emitDateRangeChanged()

    def setDateFormat(self, format):
        """
	Sets the date format that will be used when rendering in the views.
	
	:return     <str>
	"""
        return self._dateFormat

    def setDateStart(self, dateStart):
        """
	Sets the start date for the range of this gantt widget.
	
	:param      dateStart | <QDate>
	"""
        self._dateStart = dateStart
        self.emitDateRangeChanged()

    def setGridPen(self, pen):
        """
	Sets the pen used to draw the grid lines for the view.
	
	:param      pen | <QPen> || <QColor>
	"""
        self._gridPen = QPen(pen)

    def setTimescale(self, timescale):
        """
	Sets the timescale value for this widget to the inputed value.
	
	:param      timescale | <XGanttWidget.Timescale>
	"""
        self._timescale = timescale

    #def setupUserView(self, privileges, department):
        #sif department

    def setWeekendBrush(self, brush):
        """
	Sets the brush to be used when coloring weekend columns.
	
	:param      brush | <QBrush> || <QColor>
	"""
        self._weekendBrush = QBrush(brush)

    def syncView(self):
        """
	Syncs all the items to the view.
	"""
        if (not self.signalsBlocked()):
            for i in range(self.topLevelItemCount()):
                item = self.topLevelItem(i)
                item.syncView(recursive=True)

    def takeTopLevelItem(self, index):
        """
	Removes the top level item at the inputed index from the widget.
	
	:param      index | <int>
	
	:return     <XGanttWidgetItem> || None
	"""
        item = self.topLevelItem(index)
        if (item):
            self.viewWidget().scene().removeItem(item.viewItem())
            self.treeWidget().takeTopLevelItem(index)

            return item
        return None

    def timescale(self):
        """
	Returns the timescale that is being used for this widget.
	
	:return     <XGanttWidget.Timescale>
	"""
        return self._timescale

    def topLevelItem(self, index):
        """
	Returns the top level item at the inputed index.
	
	:return     <XGanttWidgetItem>
	"""
        return self.treeWidget().topLevelItem(index)

    def topLevelItemCount(self):
        """
	Returns the number of top level items for this widget.
	
	:return     <int>
	"""
        return self.treeWidget().topLevelItemCount()

    def treeWidget(self):
        """
	Returns the tree widget for this gantt widget.
	
	:return     <QTreeWidget>
	"""
        return self.uiGanttTREE

    def updateItemData(self, item, index):
        """
	Updates the item information from the tree.
	
	:param      item    | <XGanttWidgetItem>
		    index   | <int>
	"""
        value = qt.unwrapVariant(item.data(index, Qt.EditRole))

        if type(value) == QDateTime:
            value = value.date()
            item.setData(index, Qt.EditRole, qt.wrapVariant(value))

        if type(value) == QDate:
            value = value.toPython()

        columnName = self.treeWidget().columnOf(index)
        item.setProperty(columnName, value)
        item.sync()

    def updatePhaseVisibility(self, visibility, phaseName=''):

        if (phaseName != ''):
            #print ("Changing "+ phaseName + " to : "+ str(visibility))
            #iterate through all projects
            for x in range(0, self.treeWidget().topLevelItemCount()):
                projectWidgetItem = self.treeWidget().topLevelItem(x)
                keepVisible = False
                for c in range(projectWidgetItem.childCount()):
                    child = projectWidgetItem.child(c)
                    if (child._dbEntry._phase._name == phaseName):
                        #projectWidgetItem.setHidden(not visibility)
                        child.setHidden(not visibility)
                        if (visibility):
                            keepVisible = True

                    elif (visibility == False and not child.isHidden()
                          and keepVisible == False):
                        keepVisible = True

                        #print child._name
                #print ("item: " + str(x))
                #iterate through all phases

                if (keepVisible):
                    if (projectWidgetItem.isHidden()):
                        projectWidgetItem.setHidden(False)
                elif (not visibility):
                    if (not projectWidgetItem.isHidden()):
                        #self.syncView()
                        projectWidgetItem.setHidden(True)

            #if phase matches, change visibility

            for phase in sharedDB.myPhases.values():
                if (phase._name == phaseName):
                    phase._visible = visibility

        else:
            #iterate through all projects
            #iterate through all phases
            #change visibility
            if visibility == False:
                self.collapseAllTrees()

            for x in range(0, self.treeWidget().topLevelItemCount()):
                projectWidgetItem = self.treeWidget().topLevelItem(x)
                for c in range(projectWidgetItem.childCount()):
                    child = projectWidgetItem.child(c)
                    child.setHidden(not visibility)
                #self.syncView()
                projectWidgetItem.setHidden(not visibility)

            for phase in sharedDB.myPhases.values():
                phase._visible = visibility
            #print ("Changing all phases to: "+ str(visibility))

        self.syncView()

    def updateUserVisibility(self, visibility, username=''):
        if (username != ''):
            #print ("Changing "+ phaseName + " to : "+ str(visibility))
            #iterate through all projects
            for x in range(0, self.treeWidget().topLevelItemCount()):
                projectWidgetItem = self.treeWidget().topLevelItem(x)
                keepProjectVisible = False
                for c in range(projectWidgetItem.childCount()):
                    child = projectWidgetItem.child(c)

                    if (child._dbEntry.type() == "phaseassignment"):
                        print "keeping " + username + " visible."

                        if self.CheckUserAssignmentForUser(
                                child._dbEntry._userAssignments.values(),
                                username):
                            child.setHidden(not visibility)
                            if (visibility):
                                keepProjectVisible = True

                    if (visibility == False and not child.isHidden()
                            and keepProjectVisible == False):
                        keepProjectVisible = True

                if (keepProjectVisible):
                    if (projectWidgetItem.isHidden()):
                        projectWidgetItem.setHidden(False)
                elif (not visibility):
                    if (not projectWidgetItem.isHidden()):
                        #self.syncView()
                        for c in range(projectWidgetItem.childCount()):
                            projectWidgetItem.child(c).setHidden(True)

                        projectWidgetItem.setHidden(True)

            #if phase matches, change visibility

            for user in sharedDB.myUsers.values():
                if (user.name() == username):
                    user._calendarVisibility = visibility

        else:
            #iterate through all projects
            #iterate through all phases
            #change visibility
            if visibility == False:
                self.collapseAllTrees()

            for x in range(0, self.treeWidget().topLevelItemCount()):
                projectWidgetItem = self.treeWidget().topLevelItem(x)
                for c in range(projectWidgetItem.childCount()):
                    child = projectWidgetItem.child(c)
                    child.setHidden(not visibility)
                #self.syncView()
                projectWidgetItem.setHidden(not visibility)

            for phase in sharedDB.myPhases.values():
                phase._visible = visibility

            for user in sharedDB.myUsers.values():
                user._calendarVisibility = visibility

            #print ("Changing all phases to: "+ str(visibility))

        self.syncView()

    def CheckUserAssignmentForUser(self, userAssignments, username):
        for ua in userAssignments:
            if ua._hours > 1 and ua.idUsers() is not None:
                if (sharedDB.myUsers[str(ua.idUsers())].name() == username):
                    return True

        return False

    def viewWidget(self):
        """
	Returns the view widget for this gantt widget.
	
	:return     <QGraphicsView>
	"""
        return self.uiGanttVIEW

    def weekendBrush(self):
        """
	Returns the weekend brush to be used for coloring in weekends.
	
	:return     <QBrush>
	"""
        return self._weekendBrush

    def bookedBrush(self):
        """
	Returns the booked brush to be used for coloring in booked days.
	
	:return     <QBrush>
	"""
        return self._bookedBrush

    def unavailableBrush(self):
        """
	Returns the unavailable brush to be used for coloring in unavailable days.
	
	:return     <QBrush>
	"""
        return self._unavailableBrush

    def underbookedBrush(self):
        """
	Returns the underbookedBrush brush to be used for coloring in underbooked days.
	
	:return     <QBrush>
	"""
        return self._underbookedBrush

    def overbookedBrush(self):
        """
	Returns the overbookedBrush brush to be used for coloring in overbooked days.
	
	:return     <QBrush>
	"""
        return self._overbookedBrush

    def unassignedBrush(self):
        """
	Returns the unassignedBrush brush to be used for coloring in unassigned days.
	
	:return     <QBrush>
	"""
        return self._unassignedBrush

    def showProjectMenu(self, pos):
        """
        Displays the header menu for this tree widget.
        
        :param      pos | <QPoint> || None
        """
        '''
	
	header = self.header()
        index  = header.logicalIndexAt(pos)
        self._headerIndex = index
        
        # show a pre-set menu
        if self._headerMenu:
            menu = self._headerMenu
        else:
            menu = self.createHeaderMenu(index)
        '''
        # determine the point to show the menu from
        #if pos is not None:
        #    point = header.mapToGlobal(pos)
        #else:
        index = self.uiGanttTREE.indexAt(pos)

        typ = "None"
        if index.isValid():
            dbentry = self.uiGanttTREE.itemFromIndex(index)._dbEntry
            if hasattr(dbentry, '_type'):
                typ = dbentry._type

        point = QCursor.pos()

        #self.headerMenuAboutToShow.emit(menu, index)

        menu = QtGui.QMenu()

        if typ == "phaseassignment":
            statusMenu = menu.addMenu("TEST")
        elif typ == "project":
            statusAction = menu.addAction("Open in Project View")
            statusAction.setData(dbentry.id())
            menu.addSeparator()
            if sharedDB.currentUser._idPrivileges < 2:
                addPhaseMenu = menu.addMenu("Add Phase")

                phases = sharedDB.myPhases.values()
                phases.sort(key=operator.attrgetter('_name'))

                middleChar = phases[len(phases) / 2]._name[0]

                AMMenu = addPhaseMenu.addMenu('A - ' + middleChar)
                NZMenu = addPhaseMenu.addMenu(
                    chr(ord(middleChar) + 1) + ' - Z')

                for x in range(0, len(phases)):
                    phase = phases[x]

                    if phase._name == "DUE":
                        continue

                    #col    = self.column(column)
                    if x < len(phases) / 2 or phase._name[0] == middleChar:
                        action = AMMenu.addAction(phase._name)
                        action.setData("addphase_" + str(phase.id()) + "_" +
                                       str(dbentry.id()))
                    else:
                        action = NZMenu.addAction(phase._name)
                        action.setData("addphase_" + str(phase.id()) + "_" +
                                       str(dbentry.id()))
                '''
		for phase in sharedDB.myPhases.values():
		    if phase._name != "DUE":
			addPhaseAction = addPhaseMenu.addAction(phase._name)
			addPhaseAction.setData("addphase_"+str(phase.id())+"_"+str(dbentry.id()))
		'''
            if sharedDB.currentUser._idPrivileges == 1:
                archiveAction = menu.addAction("Archive Project")
                archiveAction.setData(dbentry.id())
        else:
            if sharedDB.currentUser._idPrivileges < 2:
                menu.addAction("Create Project")

        menu.triggered.connect(self.mActions)

        menu.exec_(point)

    def mActions(self, action):
        act = action.text()

        if act == "Open in Project View":
            self.loadinprojectview(sharedDB.myProjects[str(
                action.data().toPyObject())])
            #print sharedDB.myProjects[str(projectId)]._name
        elif act == "Archive Project":
            sharedDB.myProjects[str(action.data().toPyObject())].setArchived(1)
        elif act == "Create Project":
            if not hasattr(sharedDB, 'myCreateProjectWidget'):
                sharedDB.myCreateProjectWidget = createprojectwidget.CreateProjectWidget(
                    sharedDB.mainWindow)

            sharedDB.myCreateProjectWidget.setDefaults()
            sharedDB.myCreateProjectWidget.dockWidget.show()
        elif "addphase" in str(action.data().toPyObject()):
            phaseId = str(action.data().toPyObject()).split("_")[1]
            proj = sharedDB.myProjects[str(
                action.data().toPyObject()).split("_")[2]]
            phase = sharedDB.phaseAssignments.PhaseAssignments(
                _idphases=phaseId,
                _startdate=proj._startdate,
                _enddate=proj._startdate,
                _updated=0)
            proj.AddPhase(phase)

            #iterate through shots for
            for image in proj._images.values():
                currentTask = sharedDB.tasks.Tasks(
                    _idphaseassignments=phase._idphaseassignments,
                    _idprojects=proj._idprojects,
                    _idshots=image._idshots,
                    _idphases=phase._idphases,
                    _new=1)
                currentTask.Save()
                image._tasks[str(currentTask.id())] = (currentTask)

                currentTask.Save()
            for seq in proj._sequences.values():
                for shot in seq._shots.values():
                    currentTask = sharedDB.tasks.Tasks(
                        _idphaseassignments=phase._idphaseassignments,
                        _idprojects=proj._idprojects,
                        _idshots=shot._idshots,
                        _idphases=phase._idphases,
                        _new=1)
                    currentTask.Save()
                    shot._tasks[str(currentTask.id())] = (currentTask)

    def loadinprojectview(self, project):
        #print "Loading Project"+self.cellWidget(row,column)._phaseassignment._name
        sharedDB.mainWindow.centralTabbedWidget.setCurrentIndex(0)
        sharedDB.myProjectViewWidget._currentProject = project

        sharedDB.myProjectViewWidget.LoadProjectValues()

        sharedDB.myProjectViewWidget.projectPartWidget.setCurrentIndex(0)
class XOverlayWizard(XOverlayWidget):
    WizardButton = enum(BackButton=0,
                        NextButton=1,
                        CommitButton=2,
                        FinishButton=3,
                        CancelButton=4,
                        HelpButton=5,
                        RetryButton=6)

    WizardButtonColors = {
        WizardButton.BackButton: '#58A6D6',
        WizardButton.NextButton: '#58A6D6',
        WizardButton.CommitButton: '#49B84D',
        WizardButton.FinishButton: '#369939',
        WizardButton.CancelButton: '#B0B0B0',
        WizardButton.HelpButton: '#8CA4E6',
        WizardButton.RetryButton: '#D93D3D'
    }

    currentIdChanged = QtCore.Signal(int)
    helpRequested = QtCore.Signal()
    pageAdded = QtCore.Signal(int)
    pageRemoved = QtCore.Signal(int)

    def __init__(self, parent=None):
        super(XOverlayWizard, self).__init__(parent)

        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        # define custom properties
        self._fixedPageSize = QtCore.QSize()
        self._minimumPageSize = QtCore.QSize(0, 0)
        self._maximumPageSize = QtCore.QSize(10000, 10000)
        self._currentId = -1
        self._startId = -1
        self._pages = OrderedDict()
        self._buttons = {}
        self._fields = {}
        self._navigation = []
        self._animationSpeed = 350

        # create the buttons
        self._buttons[self.WizardButton.HelpButton] = QtGui.QPushButton(
            self.tr('Help'), self)
        self._buttons[self.WizardButton.BackButton] = QtGui.QPushButton(
            self.tr('Back'), self)
        self._buttons[self.WizardButton.NextButton] = QtGui.QPushButton(
            self.tr('Next'), self)
        self._buttons[self.WizardButton.CommitButton] = QtGui.QPushButton(
            self.tr('Commit'), self)
        self._buttons[self.WizardButton.FinishButton] = QtGui.QPushButton(
            self.tr('Finish'), self)
        self._buttons[self.WizardButton.CancelButton] = QtGui.QPushButton(
            self.tr('Cancel'), self)
        self._buttons[self.WizardButton.RetryButton] = QtGui.QPushButton(
            self.tr('Retry'), self)

        # don't show any buttons by default
        pal = self.palette()
        for which, btn in self._buttons.items():
            pal.setColor(pal.Active, pal.Button,
                         QtGui.QColor(self.WizardButtonColors[which]))
            pal.setColor(pal.Active, pal.ButtonText, QtGui.QColor('white'))
            pal.setColor(pal.Inactive, pal.Button,
                         QtGui.QColor(self.WizardButtonColors[which]))
            pal.setColor(pal.Inactive, pal.ButtonText, QtGui.QColor('white'))
            btn.setPalette(pal)
            btn.setFixedSize(QtCore.QSize(120, 30))
            btn.hide()

        # create connections
        self._buttons[self.WizardButton.HelpButton].clicked.connect(
            self.helpRequested)
        self._buttons[self.WizardButton.BackButton].clicked.connect(self.back)
        self._buttons[self.WizardButton.NextButton].clicked.connect(self.next)
        self._buttons[self.WizardButton.CommitButton].clicked.connect(
            self.commit)
        self._buttons[self.WizardButton.FinishButton].clicked.connect(
            self.accept)
        self._buttons[self.WizardButton.CancelButton].clicked.connect(
            self.reject)
        self._buttons[self.WizardButton.RetryButton].clicked.connect(
            self.retry)

    def accept(self):
        if not self.currentPage() or self.currentPage().validatePage():
            return super(XOverlayWizard, self).accept()
        return False

    def addPage(self, page):
        """
        Adds a new overlay wizard page to this wizard.

        :param      page | <projexui.widgets.xoverlaywizard.XOverlayWizardPage>
        """
        self.setPage(len(self._pages), page)

    def adjustSize(self):
        """
        Adjusts the size of this wizard and its page contents to the inputed size.

        :param      size | <QtCore.QSize>
        """
        super(XOverlayWizard, self).adjustSize()

        # adjust the page size
        page_size = self.pageSize()

        # resize the current page to the inputed size
        x = (self.width() - page_size.width()) / 2
        y = (self.height() - page_size.height()) / 2
        btn_height = max([btn.height() for btn in self._buttons.values()])

        curr_page = self.currentPage()
        for page in self._pages.values():
            page.resize(page_size.width(), page_size.height() - btn_height - 6)
            if page == curr_page:
                page.move(x, y)
            else:
                page.move(self.width(), y)

        # move the left most buttons
        y += page_size.height() - btn_height
        left_btns = (self._buttons[self.WizardButton.HelpButton],
                     self._buttons[self.WizardButton.BackButton])
        for btn in left_btns:
            if btn.isVisible():
                btn.move(x, y)
                btn.raise_()
                x += btn.width() + 6

        # move the right buttons
        x = self.width() - (self.width() - page_size.width()) / 2
        for key in reversed(sorted(self._buttons.keys())):
            btn = self._buttons[key]
            if not btn.isVisible() or btn in left_btns:
                continue

            btn.move(x - btn.width(), y)
            btn.raise_()
            x -= btn.width() + 6

    def animationSpeed(self):
        """
        Returns the speed that the pages should animate in/out in milliseconds.

        :return     <int>
        """
        return self._animationSpeed

    def back(self):
        """
        Goes to the previous page for this wizard.
        """
        try:
            pageId = self._navigation[-2]
            last_page = self.page(pageId)
        except IndexError:
            return

        curr_page = self.page(self._navigation.pop())
        if not (last_page and curr_page):
            return

        self._currentId = pageId

        y = curr_page.y()
        last_page.move(-last_page.width(), y)
        last_page.show()

        # animate the last page in
        anim_in = QtCore.QPropertyAnimation(self)
        anim_in.setTargetObject(last_page)
        anim_in.setPropertyName('pos')
        anim_in.setStartValue(last_page.pos())
        anim_in.setEndValue(curr_page.pos())
        anim_in.setDuration(self.animationSpeed())
        anim_in.setEasingCurve(QtCore.QEasingCurve.Linear)

        # animate the current page out
        anim_out = QtCore.QPropertyAnimation(self)
        anim_out.setTargetObject(curr_page)
        anim_out.setPropertyName('pos')
        anim_out.setStartValue(curr_page.pos())
        anim_out.setEndValue(QtCore.QPoint(self.width() + curr_page.width(),
                                           y))
        anim_out.setDuration(self.animationSpeed())
        anim_out.setEasingCurve(QtCore.QEasingCurve.Linear)

        # create the anim group
        anim_grp = QtCore.QParallelAnimationGroup(self)
        anim_grp.addAnimation(anim_in)
        anim_grp.addAnimation(anim_out)
        anim_grp.finished.connect(curr_page.hide)
        anim_grp.finished.connect(anim_grp.deleteLater)

        # update the button states
        self._buttons[self.WizardButton.BackButton].setVisible(
            self.canGoBack())
        self._buttons[self.WizardButton.NextButton].setVisible(True)
        self._buttons[self.WizardButton.RetryButton].setVisible(
            self.canRetry())
        self._buttons[self.WizardButton.CommitButton].setVisible(
            last_page.isCommitPage())
        self._buttons[self.WizardButton.FinishButton].setVisible(
            last_page.isFinalPage())

        self.adjustSize()
        self.currentIdChanged.emit(pageId)
        anim_grp.start()

    def button(self, which):
        """
        Returns the button associated with the inputed wizard button.

        :param      which | <XOverlayWizard.WizardButton>

        :return     <bool> || None
        """
        return self._buttons.get(which)

    def buttonText(self, which):
        """
        Returns the button text for a given wizard button.

        :param      which | <XOverlayWizard.WizardButton>
        """
        try:
            return self._buttons[which].text()
        except KeyError:
            return ''

    def canGoBack(self):
        """
        Returns whether or not this wizard can move forward.

        :return     <bool>
        """
        try:
            backId = self._navigation.index(self.currentId()) - 1
            if backId >= 0:
                self._navigation[backId]
            else:
                return False
        except StandardError:
            return False
        else:
            return True

    def canGoForward(self):
        """
        Returns whether or not this wizard can move forward.

        :return     <bool>
        """
        try:
            return not self.currentPage().isFinalPage()
        except AttributeError:
            return False

    def canRetry(self):
        """
        Returns whether or not this wizard can retry the current page.

        :return     <bool>
        """
        try:
            return self.currentPage().retryEnabled()
        except AttributeError:
            return False

    def commit(self):
        """
        Commits the current page information.
        """
        try:
            self.currentPage().commit()
        except AttributeError:
            pass

    def currentId(self):
        """
        Returns the page ID of the current page that this wizard is on.

        :return     <int>
        """
        return self._currentId

    def currentPage(self):
        """
        Returns the current page for this wizard.

        :return     <projexui.widgets.xoverlaywizard.XOverlayWizardPage> || None
        """
        try:
            return self._pages[self.currentId()]
        except KeyError:
            return None

    def field(self, name, default=None):
        """
        Returns the value for the inputed field property for this wizard.

        :param      name | <str>
                    default | <variant>

        :return     <variant>
        """
        return self._fields.get(name, default)

    def fixedPageSize(self):
        """
        Returns the fixed page size for this wizard's contents.

        :return     <QtCore.QSize>
        """
        return self._fixedPageSize

    def hasVisitedPage(self, pageId):
        """
        Returns whether or not the user has seen the inputed page id.

        :return     <bool>
        """
        return False

    def minimumPageSize(self):
        """
        Returns the minimum page size for this wizard's contents.

        :return     <QtCore.QSize>
        """
        return self._minimumPageSize

    def maximumPageSize(self):
        """
        Returns the maximum page size for this wizard's contents.

        :return     <QtCore.QSize>
        """
        return self._maximumPageSize

    def next(self):
        """
        Goes to the previous page for this wizard.
        """
        curr_page = self.currentPage()
        if not curr_page:
            return
        elif not curr_page.validatePage():
            return

        pageId = curr_page.nextId()
        try:
            next_page = self._pages[pageId]
        except KeyError:
            return

        self._currentId = pageId
        self._navigation.append(pageId)

        y = curr_page.y()
        next_page.move(self.width(), y)

        # animate the last page in
        anim_in = QtCore.QPropertyAnimation(self)
        anim_in.setTargetObject(curr_page)
        anim_in.setPropertyName('pos')
        anim_in.setStartValue(curr_page.pos())
        anim_in.setEndValue(QtCore.QPoint(-curr_page.width(), y))
        anim_in.setDuration(self.animationSpeed())
        anim_in.setEasingCurve(QtCore.QEasingCurve.Linear)

        # animate the current page out
        anim_out = QtCore.QPropertyAnimation(self)
        anim_out.setTargetObject(next_page)
        anim_out.setPropertyName('pos')
        anim_out.setStartValue(next_page.pos())
        anim_out.setEndValue(curr_page.pos())
        anim_out.setDuration(self.animationSpeed())
        anim_out.setEasingCurve(QtCore.QEasingCurve.Linear)

        # create the anim group
        anim_grp = QtCore.QParallelAnimationGroup(self)
        anim_grp.addAnimation(anim_in)
        anim_grp.addAnimation(anim_out)
        anim_grp.finished.connect(curr_page.hide)
        anim_grp.finished.connect(anim_grp.deleteLater)

        next_page.show()

        # update the button states
        self._buttons[self.WizardButton.BackButton].setVisible(True)
        self._buttons[self.WizardButton.NextButton].setVisible(
            self.canGoForward())
        self._buttons[self.WizardButton.RetryButton].setVisible(
            self.canRetry())
        self._buttons[self.WizardButton.CommitButton].setVisible(
            next_page.isCommitPage())
        self._buttons[self.WizardButton.FinishButton].setVisible(
            next_page.isFinalPage())
        self.adjustSize()

        # initialize the new page
        self.currentIdChanged.emit(pageId)
        next_page.initializePage()
        anim_grp.start()

    def page(self, pageId):
        """
        Returns the page at the inputed id.

        :return     <projexui.widgets.xoverlaywizard.XOverlayWizardPage> || None
        """
        return self._pages.get(pageId)

    def pageCount(self):
        """
        Returns the number of pages associated with this wizard.

        :return     <int>
        """
        return len(self._pages)

    def pageIds(self):
        """
        Returns a list of all the page ids for this wizard.

        :return     [<int>, ..]
        """
        return self._pages.keys()

    def pageSize(self):
        """
        Returns the current page size for this wizard.

        :return     <QtCore.QSize>
        """

        # update the size based on the new size
        page_size = self.fixedPageSize()
        if page_size.isEmpty():
            w = self.width() - 80
            h = self.height() - 80

            min_w = self.minimumPageSize().width()
            min_h = self.minimumPageSize().height()
            max_w = self.maximumPageSize().width()
            max_h = self.maximumPageSize().height()

            page_size = QtCore.QSize(min(max(min_w, w), max_w),
                                     min(max(min_h, h), max_h))
        return page_size

    def exec_(self):
        """
        Executes this wizard within the system.
        """
        self.show()
        self.adjustSize()
        self.restart()

    def restart(self):
        """
        Restarts the whole wizard from the beginning.
        """
        # hide all of the pages
        for page in self._pages.values():
            page.hide()

        pageId = self.startId()
        try:
            first_page = self._pages[pageId]
        except KeyError:
            return

        self._currentId = pageId
        self._navigation = [pageId]

        page_size = self.pageSize()
        x = (self.width() - page_size.width()) / 2
        y = (self.height() - page_size.height()) / 2

        first_page.move(self.width() + first_page.width(), y)
        first_page.show()

        # animate the current page out
        anim_out = QtCore.QPropertyAnimation(self)
        anim_out.setTargetObject(first_page)
        anim_out.setPropertyName('pos')
        anim_out.setStartValue(first_page.pos())
        anim_out.setEndValue(QtCore.QPoint(x, y))
        anim_out.setDuration(self.animationSpeed())
        anim_out.setEasingCurve(QtCore.QEasingCurve.Linear)
        anim_out.finished.connect(anim_out.deleteLater)

        # update the button states
        self._buttons[self.WizardButton.BackButton].setVisible(False)
        self._buttons[self.WizardButton.NextButton].setVisible(
            self.canGoForward())
        self._buttons[self.WizardButton.CommitButton].setVisible(
            first_page.isCommitPage())
        self._buttons[self.WizardButton.FinishButton].setVisible(
            first_page.isFinalPage())
        self.adjustSize()

        first_page.initializePage()
        self.currentIdChanged.emit(pageId)
        anim_out.start()

    def removePage(self, pageId):
        """
        Removes the inputed page from this wizard.

        :param      pageId | <int>
        """
        try:
            self._pages[pageId].deleteLater()
            del self._pages[pageId]
        except KeyError:
            pass

    def retry(self):
        """
        Reruns the current page operation.
        """
        try:
            self.currentPage().initializePage()
        except AttributeError:
            pass

    def setAnimationSpeed(self, speed):
        """
        Sets the speed that the pages should animate in/out in milliseconds.

        :param      speed | <int> | milliseconds
        """
        self._animationSpeed = speed

    def setButton(self, which, button):
        """
        Sets the button for this wizard for the inputed type.

        :param      which  | <XOverlayWizard.WizardButton>
                    button | <QtGui.QPushButton>
        """
        try:
            self._buttons[which].deleteLater()
        except KeyError:
            pass

        button.setParent(self)
        self._buttons[which] = button

    def setButtonText(self, which, text):
        """
        Sets the display text for the inputed button to the given text.

        :param      which | <XOverlayWizard.WizardButton>
                    text  | <str>
        """
        try:
            self._buttons[which].setText(text)
        except KeyError:
            pass

    def setField(self, name, value):
        """
        Sets the field value for this wizard to the given value.

        :param      name | <str>
                    value | <variant>
        """
        self._fields[name] = value

    def setFixedPageSize(self, size):
        """
        Sets the page size for the wizard.  This will define the width/height for the contents
        for this wizards page widgets as well as the bounding information for the buttons.  If
        an empty size is given, then the size will automatically be recalculated as the widget sizes.

        :param      size | <QtCore.QSize>
        """
        self._fixedPageSize = size
        self.adjustSize()

    def setMinimumPageSize(self, size):
        """
        Sets the page size for the wizard.  This will define the width/height for the contents
        for this wizards page widgets as well as the bounding information for the buttons.  If
        an empty size is given, then the size will automatically be recalculated as the widget sizes.

        :param      size | <QtCore.QSize>
        """
        self._minimumPageSize = size
        self.adjustSize()

    def setMaximumPageSize(self, size):
        """
        Sets the page size for the wizard.  This will define the width/height for the contents
        for this wizards page widgets as well as the bounding information for the buttons.  If
        an empty size is given, then the size will automatically be recalculated as the widget sizes.

        :param      size | <QtCore.QSize>
        """
        self._maximumPageSize = size
        self.adjustSize()

    def setPage(self, pageId, page):
        """
        Sets the page and id for the given page vs. auto-constructing it.  This will allow the
        developer to create a custom order for IDs.

        :param      pageId | <int>
                    page   | <projexui.widgets.xoverlaywizard.XOverlayWizardPage>
        """
        page.setParent(self)

        if self.property("useShadow") is not False:
            # create the drop shadow effect
            effect = QtGui.QGraphicsDropShadowEffect(page)
            effect.setColor(QtGui.QColor('black'))
            effect.setBlurRadius(50)
            effect.setOffset(0, 0)

            page.setGraphicsEffect(effect)

        self._pages[pageId] = page
        if self._startId == -1:
            self._startId = pageId

    def setStartId(self, pageId):
        """
        Sets the starting page id for this wizard to the inputed page id.

        :param      pageId | <int>
        """
        self._startId = pageId

    def startId(self):
        """
        Returns the starting id for this wizard.

        :return     <int>
        """
        return self._startId
Exemple #14
0
ColumnType = enum(
    # simple types
    'Bool',
    'Decimal',
    'Double',
    'Integer',  # 32-bit integer
    'BigInt',  # 64-bit integer
    'Enum',  # equates to an integer in databases, but links to an enum

    # string types
    'String',  # used for limited char sets
    'Text',  # used for larger string data
    'Url',  # similar to String, but uses url regex validation
    'Email',  # similar to String, but uses email regex validation
    'Password',  # similar to String, but uses password regex validation
    'Filepath',  # similar to String, but uses filepath regex validation
    'Directory',  # similar to String, but uses directory regex validation
    'Xml',  # similar to Text,   but auto-escape data stored
    'Html',  # similar to Text,   but store rich text data & auto-escape
    'Color',  # similar to String, stores the HEX value of a color (#ff00)

    # date/time types
    'Datetime',
    'Date',
    'Interval',
    'Time',
    'DatetimeWithTimezone',
    'Timestamp',
    'Timestamp_UTC',

    # data types
    'Image',  # stores images in the database as binary
    'ByteArray',  # stores additional binary information
    'Dict',  # stores python dictionary types
    'Pickle',  # stores python pickle data
    'Yaml',  # stores python data as yaml (requires PyYaml)
    'JSON',  # stores python data as JSON
    'Query',  # stores an orb.Query class as xml

    # relation types
    'ForeignKey',  # tells the system to use the relation's information
)
Exemple #15
0
__maintainer__      = 'Projex Software, LLC'
__email__           = '*****@*****.**'

import datetime
import projex.text

from projex.enum import enum

# define global enumerations
RepeatFlags = enum( # Weekly Flags
                    'EveryMonday',
                    'EveryTuesday',
                    'EveryWednesday',
                    'EveryThursday',
                    'EveryFriday',
                    'EverySaturday',
                    'EverySunday',
                    
                    # Repeating Flags
                    'DayOfTheMonth',
                    'DayOfTheWeek')

RepeatMode = enum(  Weekly = 2,         # removed the 'Daily' key, was 1
                    Monthly = 4,
                    Yearly = 8)

Names = enum( # default
              'Sometime',
              
              # Preset Names
              'Today',
Exemple #16
0
class Column(AddonManager):
    """ Used to define database schema columns when defining Table classes. """
    TypeMap = {}
    MathMap = {
        'Default': {
            'Add': u'{field} + {value}',
            'Subtract': u'{field} - {value}',
            'Multiply': u'{field} * {value}',
            'Divide': u'{field} / {value}',
            'And': u'{field} & {value}',
            'Or': u'{field} | {value}'
        }
    }

    Flags = enum(
        'ReadOnly',
        'Polymorphic',
        'AutoAssign',
        'Required',
        'Unique',
        'Encrypted',
        'Searchable',
        'I18n',
        'I18n_NoDefault',
        'CaseSensitive',
        'Virtual',
        'Static',
        'Protected',
        'Private',
        'AutoExpand',
        'RequiresExpand',
        'Keyable'
    )

    def __json__(self):
        output = {
            'type': self.addonName(),
            'name': self.name(),
            'field': self.field(),
            'display': self.display(),
            'flags': {k: True for k in self.Flags.toSet(self.__flags)},
            'default': self.default()
        }
        return output

    def __init__(self,
                 name=None,
                 field=None,
                 display=None,
                 getterName=None,
                 setterName=None,
                 flags=0,
                 default=None,
                 defaultOrder='asc',
                 shortcut='',
                 readPermit=None,
                 writePermit=None,
                 permit=None,
                 getter=None,
                 setter=None,
                 queryFilter=None,
                 schema=None,
                 order=99999):
        # constructor items
        self.__name = name
        self.__field = field
        self.__display = display
        self.__flags = self.Flags.fromSet(flags) if isinstance(flags, set) else flags
        self.__default = default
        self.__defaultOrder = defaultOrder
        self.__getterName = getterName
        self.__setterName = setterName
        self.__shortcut = shortcut
        self.__query_filter = queryFilter
        self.__settermethod = setter
        self.__gettermethod = getter
        self.__readPermit = readPermit or permit
        self.__writePermit = writePermit or permit
        self.__order = order

        # custom options
        self.__schema = None
        self.__timezone = None

        if shortcut:
            self.setFlag(self.Flags.Virtual)

        # auto-register to the schema if provided
        if schema:
            schema.register(self)

    def copy(self):
        """
        Returns a new instance copy of this column.

        :return: <orb.Column>
        """
        out = type(self)(
            name=self.__name,
            field=self.__field,
            display=self.__display,
            flags=self.__flags,
            default=self.__default,
            defaultOrder=self.__defaultOrder,
            getterName=self.__getterName,
            setterName=self.__setterName,
            getter=self.__gettermethod,
            setter=self.__settermethod,
            queryFilter=self.__query_filter,
            shortcut=self.__shortcut,
            readPermit=self.__readPermit,
            writePermit=self.__writePermit,
            order=self.__order
        )

        return out

    def dbRestore(self, db_value, context=None):
        """
        Converts a stored database value to Python.

        :param typ: <str>
        :param py_value: <variant>
        :param context: <orb.Context>

        :return: <variant>
        """
        # restore translatable column
        if self.testFlag(self.Flags.I18n):
            if isinstance(db_value, (str, unicode)):
                if db_value.startswith('{'):
                    try:
                        value = projex.text.safe_eval(db_value)
                    except StandardError:
                        value = {context.locale: db_value}
                else:
                    value = {context.locale: db_value}
            else:
                value = db_value

            return value
        else:
            return db_value

    def dbMath(self, typ, field, op, value):
        """
        Performs some database math on the given field.  This will be database specific
        implementations and should return the resulting database operation.

        :param field: <str>
        :param op: <orb.Query.Math>
        :param target: <variant>
        :param context: <orb.Context> || None

        :return: <str>
        """
        ops = orb.Query.Math(op)
        format = self.MathMap.get(typ, {}).get(ops) or self.MathMap.get('Default').get(ops) or '{field}'
        return format.format(field=field, value=value)

    def dbStore(self, typ, py_value):
        """
        Prepares to store this column for the a particular backend database.

        :param backend: <orb.Database>
        :param py_value: <variant>
        :param context: <orb.Context>

        :return: <variant>
        """
        # convert base types to work in the database
        if isinstance(py_value, (list, tuple, set)):
            py_value = tuple((self.dbStore(x) for x in py_value))
        elif isinstance(py_value, orb.Collection):
            py_value = py_value.ids()
        elif isinstance(py_value, orb.Model):
            py_value = py_value.id()

        return py_value

    def dbType(self, typ):
        """
        Returns the database object type based on the given connection type.

        :param typ:  <str>

        :return: <str>
        """
        return self.TypeMap.get(typ, self.TypeMap.get('Default'))

    def default(self):
        """
        Returns the default value for this column to return
        when generating new instances.

        :return     <variant>
        """
        if isinstance(self.__default, (str, unicode)):
            return self.valueFromString(self.__default)
        else:
            return self.__default

    def defaultOrder(self):
        """
        Returns the default ordering for this column when sorting.

        :return     <str>
        """
        return self.__defaultOrder

    def display(self):
        """
        Returns the display text for this column.

        :return     <str>
        """
        return self.__display or orb.system.syntax().display(self.__name)

    def field(self):
        """
        Returns the field name that this column will have inside the database.

        :return     <str>
        """
        return self.__field or orb.system.syntax().field(self.__name, isinstance(self, orb.ReferenceColumn))

    def firstMemberSchema(self, schemas):
        """
        Returns the first schema within the list that this column is a member
        of.

        :param      schemas | [<orb.TableSchema>, ..]

        :return     <orb.TableSchema> || None
        """
        for schema in schemas:
            if schema.hasColumn(self):
                return schema
        return self.schema()

    def flags(self):
        """
        Returns the flags that have been set for this column.

        :return     <Column.Flags>
        """
        return self.__flags

    def getter(self):
        def wrapped(func):
            self.__gettermethod = func
            return func
        return wrapped

    def getterName(self):
        return self.__getterName or orb.system.syntax().getterName(self.__name)

    def gettermethod(self):
        return self.__gettermethod

    def isMemberOf(self, schemas):
        """
        Returns whether or not this column is a member of any of the given
        schemas.

        :param      schemas | [<orb.TableSchema>, ..] || <orb.TableSchema>

        :return     <bool>
        """
        if type(schemas) not in (tuple, list, set):
            schemas = (schemas,)

        for schema in schemas:
            if schema.hasColumn(self):
                return True
        return False

    def isNull(self, value):
        """
        Returns whether or not the given value is considered null for this column.

        :param value: <variant>

        :return: <bool>
        """
        return value is None

    def loadJSON(self, jdata):
        """
        Initializes the information for this class from the given JSON data blob.

        :param jdata: <dict>
        """
        # required params
        self.__name = jdata['name']
        self.__field = jdata['field']

        # optional fields
        self.__display = jdata.get('display') or self.__display
        self.__flags = jdata.get('flags') or self.__flags
        self.__defaultOrder = jdata.get('defaultOrder') or self.__defaultOrder
        self.__default = jdata.get('default') or self.__default

    def memberOf(self, schemas):
        """
        Returns a list of schemas this column is a member of from the inputted
        list.

        :param      schemas | [<orb.TableSchema>, ..]

        :return     [<orb.TableSchema>, ..]
        """
        for schema in schemas:
            if schema.hasColumn(self):
                yield schema

    def name(self):
        """
        Returns the accessor name that will be used when
        referencing this column around the app.

        :return     <str>
        """
        return self.__name

    def order(self):
        """
        Returns the priority order for this column.  Lower
        number priorities will be processed first.

        :return: <int>
        """
        return self.__order

    def queryFilter(self, function=None):
        """
        Defines a decorator that can be used to filter
        queries.  It will assume the function being associated
        with the decorator will take a query as an input and
        return a modified query to use.

        :usage

            class MyModel(orb.Model):
                objects = orb.ReverseLookup('Object')

                @classmethod
                @objects.queryFilter()
                def objectsFilter(cls, query, **context):
                    return orb.Query()

        :param function: <callable>

        :return: <wrapper>
        """
        if function is not None:
            self.__query_filter = function
            return function

        def wrapper(func):
            self.__query_filter = func
            return func

        return wrapper

    def queryFilterMethod(self):
        """
        Returns the actual query filter method, if any,
        that is associated with this collector.

        :return: <callable>
        """
        return self.__query_filter

    def random(self):
        """
        Returns a random value that fits this column's parameters.

        :return: <variant>
        """
        return self.default()

    def readPermit(self):
        """
        Returns the read permit for this column.

        :return: <str> || None
        """
        return self.__readPermit

    def restore(self, value, context=None, inflated=True):
        """
        Restores the value from a table cache for usage.

        :param      value   | <variant>
                    context | <orb.Context> || None
        """
        context = context or orb.Context()

        # check to see if this column is translatable before restoring
        if self.testFlag(self.Flags.I18n):
            locales = context.locale.split(',')

            if not isinstance(value, dict):
                default_locale = locales[0]
                if default_locale == 'all':
                    default_locale = orb.system.settings().default_locale
                value = {default_locale: value}

            if 'all' in locales:
                return value

            if len(locales) == 1:
                return value.get(locales[0])
            else:
                return {locale: value.get(locale) for locale in locales}
        else:
            return value

    def setShortcut(self, shortcut):
        """
        Sets the shortcut information for this column.

        :param shortcut: <str>
        """
        self.__shortcut = shortcut

    def shortcut(self):
        """
        Returns the shortcut path that will be taken through
        this column.

        :return: <str> || None
        """
        return self.__shortcut

    def store(self, value, context=None):
        """
        Converts the value to one that is safe to store on a record within
        the record values dictionary

        :param      value | <variant>

        :return     <variant>
        """
        if isinstance(value, (str, unicode)):
            value = self.valueFromString(value)

        # store the internationalized property
        if self.testFlag(self.Flags.I18n):
            if not isinstance(value, dict):
                context = context or orb.Context()
                return {context.locale: value}
            else:
                return value
        else:
            return value

    def schema(self):
        """
        Returns the table that this column is linked to in the database.

        :return     <TableSchema>
        """
        return self.__schema

    def setterName(self):
        return self.__setterName or orb.system.syntax().setterName(self.__name)

    def settermethod(self):
        return self.__settermethod

    def setter(self):
        def wrapped(func):
            self.__settermethod = func
            return func
        return wrapped

    def setDefault(self, default):
        """
        Sets the default value for this column to the inputted value.

        :param      default | <str>
        """
        self.__default = default

    def setDisplay(self, display):
        """
        Sets the display name for this column.

        :param      displayName | <str>
        """
        self.__display = display

    def setField(self, field):
        """
        Sets the field name for this column.

        :param      field | <str>
        """
        self.__field = field

    def setFlag(self, flag, state=True):
        """
        Sets whether or not this flag should be on.

        :param      flag  | <Column.Flags>
                    state | <bool>
        """
        if state:
            self.__flags |= flag
        else:
            self.__flags &= ~flag

    def setFlags(self, flags):
        """
        Sets the global flags for this column to the inputted flags.

        :param      flags | <Column.Flags>
        """
        self.__flags = flags

    def setName(self, name):
        """
        Sets the name of this column to the inputted name.

        :param      name    | <str>
        """
        self.__name = name

    def setReadPermit(self, permit):
        """
        Sets the read permission for this column.

        :param permit: <str> || None
        """
        self.__readPermit = permit

    def setSchema(self, schema):
        self.__schema = schema

    def setGetterName(self, getterName):
        self.__getterName = getterName

    def setOrder(self, order):
        """
        Sets the priority order for this column.  Lower
        orders will be processed first during updates
        (in case one column should be set before another).

        :param order: <int>
        """
        self.__order = order

    def setSetterName(self, setterName):
        self.__setterName = setterName

    def setWritePermit(self, permit):
        """
        Sets the write permission for this column.

        :param permit: <str> || None
        """
        self.__writePermit = permit

    def testFlag(self, flag):
        """
        Tests to see if this column has the inputted flag set.

        :param      flag | <Column.Flags>
        """
        if isinstance(flag, (str, unicode)):
            flag = self.Flags(flag)

        return bool(self.flags() & flag) if flag >= 0 else not bool(self.flags() & ~flag)

    def validate(self, value):
        """
        Validates the inputted value against this columns rules.  If the inputted value does not pass, then
        a validation error will be raised.  Override this method in column sub-classes for more
        specialized validation.

        :param      value | <variant>

        :return     <bool> success
        """
        # check for the required flag
        if self.testFlag(self.Flags.Required) and not self.testFlag(self.Flags.AutoAssign):
            if self.isNull(value):
                msg = '{0} is a required column.'.format(self.name())
                raise orb.errors.ColumnValidationError(self, msg)

        # otherwise, we're good
        return True

    def valueFromString(self, value, context=None):
        """
        Converts the inputted string text to a value that matches the type from
        this column type.

        :param      value | <str>
        """
        return projex.text.nativestring(value)

    def valueToString(self, value, context=None):
        """
        Converts the inputted string text to a value that matches the type from
        this column type.

        :sa         engine

        :param      value | <str>
                    extra | <variant>
        """
        return projex.text.nativestring(value)

    def writePermit(self):
        """
        Returns the permit required to modify a particular column.

        :return: <str> || None
        """
        return self.__writePermit

    @classmethod
    def fromJSON(cls, jdata):
        """
        Generates a new column from the given json data.  This should
        be already loaded into a Python dictionary, not a JSON string.

        :param      jdata | <dict>

        :return     <orb.Column> || None
        """
        cls_type = jdata.get('type')
        col_cls = cls.byName(cls_type)

        if not col_cls:
            raise orb.errors.InvalidColumnType(cls_type)
        else:
            col = col_cls()
            col.loadJSON(jdata)
            return col
Exemple #17
0
class XChartRuler(object):
    Type = enum('Number', 'Date', 'Datetime', 'Time', 'Monthly', 'Custom')

    def __init__(self, rulerType):

        # define custom properties
        self._rulerType = rulerType
        self._minimum = None
        self._maximum = None
        self._step = None
        self._notches = None
        self._format = None
        self._title = ''
        self._padStart = 0
        self._padEnd = 0
        self._notchPadding = 6

        self.setRulerType(rulerType)

    def calcTotal(self, values):
        """
        Calculates the total for the given values.  For Datetime/Time objects,
        this will be seconds from minimum to maximum - defaulting to 1 second.
        For Date objects, this will be days from minimum to maximum, defaulting
        to 1 day.  For Number objects, this will be the sum of the list.  For
        Custom objects, a -1 value is returned.
        
        :param      values | [<variant>, ..]
        
        :return     <int>
        """
        rtype = self.rulerType()

        if (not values):
            return 0

        if (rtype == XChartRuler.Type.Number):
            return sum(values)

        elif (rtype == XChartRuler.Type.Date):
            values.sort()
            days = values[0].daysTo(values[-1])
            return max(1, days)

        elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)):
            values.sort()
            secs = values[0].secsTo(values[-1])
            return max(1, secs)

        return -1

    def clear(self):
        """
        Clears all the cached information about this ruler.
        """
        self._minimum = None
        self._maximum = None
        self._step = None
        self._notches = None
        self._format = None
        self._formatter = None
        self._padEnd = 0
        self._padStart = 0

    def compareValues(self, a, b):
        """
        Compares two values based on the notches and values for this ruler.
        
        :param      a | <variant>
                    b | <variant>
        
        :return     <int> 1 || 0 || -1
        """
        if (self.rulerType()
                in (XChartRuler.Type.Custom, XChartRuler.Type.Monthly)):

            try:
                aidx = self._notches.index(a)
            except ValueError:
                return -1

            try:
                bidx = self._notches.index(b)
            except ValueError:
                return 1

            return cmp(aidx, bidx)

        return cmp(a, b)

    def format(self):
        """
        Returns the format that will be used for this ruler.
        
        :return     <str>
        """
        if (self._format is not None):
            return self._format

        rtype = self.rulerType()
        if (rtype == XChartRuler.Type.Number):
            self._format = '%i'

        elif (rtype == XChartRuler.Type.Date):
            self._format = 'M.d.yy'

        elif (rtype == XChartRuler.Type.Datetime):
            self._format = 'M.d.yy @ h:mm ap'

        elif (rtype == XChartRuler.Type.Time):
            self._format = 'h:mm ap'

        return self._format

    def formatter(self):
        """
        Returns the formatter callable that is linked with this ruler for 
        formatting the value for a notch to a string.
        
        :return     <callable>
        """
        return self._formatter

    def formatValue(self, value):
        """
        Formats the inputed value based on the formatting system for this ruler.
        
        :param      value | <variant>
        """
        formatter = self.formatter()
        if (formatter is not None):
            return formatter(value)

        elif (self.format()):
            rtype = self.rulerType()
            if (rtype in (XChartRuler.Type.Date, XChartRuler.Type.Datetime,
                          XChartRuler.Type.Time)):
                return value.toString(self.format())
            else:
                try:
                    return self.format() % value
                except TypeError:
                    return nativestring(value)

        return nativestring(value)

    def maxNotchSize(self, orientation):
        """
        Returns the maximum size for this ruler based on its notches and the
        given orientation.
        
        :param      orientation | <Qt.Orientation>
        
        :return     <int>
        """
        metrics = QFontMetrics(QApplication.font())

        if orientation == Qt.Vertical:
            notch = ''
            for n in self.notches():
                if len(nativestring(n)) > len(nativestring(notch)):
                    notch = nativestring(n)

            return metrics.width(notch)
        else:
            return metrics.height()

    def minLength(self, orientation):
        """
        Returns the minimum length for this ruler based on its notches and the
        given orientation.
        
        :param      orientation | <Qt.Orientation>
        
        :return     <int>
        """
        padding = self.padStart() + self.padEnd()
        count = len(self.notches())
        notch_padding = self.notchPadding()

        if (orientation == Qt.Horizontal):
            section = self.maxNotchSize(Qt.Vertical)
        else:
            section = self.maxNotchSize(Qt.Horizontal)

        return notch_padding * count + section * count + padding

    def maximum(self):
        """
        Returns the maximum value for this ruler.  If the cached value is None,
        then a default value will be specified based on the ruler type.
        
        :return     <variant>
        """
        if (self._maximum is not None):
            return self._maximum

        rtype = self.rulerType()

        if (rtype == XChartRuler.Type.Number):
            self._maximum = 100

        elif (rtype == XChartRuler.Type.Date):
            self._maximum = QDate.currentDate().addDays(10)

        elif (rtype == XChartRuler.Type.Datetime):
            self._maximum = QDateTime.currentDateTime().addDays(10)

        elif (rtype == XChartRuler.Type.Time):
            self._maximum = QDateTime.currentDateTime().time().addSecs(60 * 60)

        else:
            notches = self.notches()
            if (notches):
                self._maximum = notches[-1]

        return self._maximum

    def minimum(self):
        """
        Returns the minimum value for this ruler.  If the cached value is None,
        then a default value will be specified based on the ruler type.
        
        :return     <variant>
        """
        if (self._minimum is not None):
            return self._minimum

        rtype = self.rulerType()

        if (rtype == XChartRuler.Type.Number):
            self._minimum = 0

        elif (rtype == XChartRuler.Type.Date):
            self._minimum = QDate.currentDate()

        elif (rtype == XChartRuler.Type.Datetime):
            self._minimum = QDateTime.currentDateTime()

        elif (rtype == XChartRuler.Type.Time):
            self._minimum = QDateTime.currentDateTime().time()

        else:
            notches = self.notches()
            if (notches):
                self._minimum = notches[0]

        return self._minimum

    def notches(self):
        """
        Reutrns a list of the notches that are going to be used for this ruler.
        If the notches have not been explicitly set (per a Custom type), then
        the notches will be generated based on the minimum, maximum and step
        values the current ruler type.
        
        :return     [<str>, ..]
        """
        if (self._notches is not None):
            return self._notches

        rtype = self.rulerType()
        formatter = self.formatter()
        format = self.format()
        self._notches = []

        minimum = self.minimum()
        maximum = self.maximum()
        step = self.step()

        if (step <= 0):
            return []

        curr = minimum
        while (curr < maximum):
            self._notches.append(self.formatValue(curr))

            if (rtype == XChartRuler.Type.Number):
                curr += step
            elif (rtype == XChartRuler.Type.Date):
                curr = curr.addDays(step)
            elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)):
                curr = curr.addSecs(step)
            else:
                break

        self._notches.append(self.formatValue(maximum))

        return self._notches

    def notchPadding(self):
        """
        Returns the minimum padding amount used between notches.
        
        :return     <int>
        """
        return self._notchPadding

    def padEnd(self):
        """
        Returns the number of units to use as padding for the end of this ruler.
        
        :return     <int>
        """
        return self._padEnd

    def padStart(self):
        """
        Returns the number of units to use as padding for the beginning of
        this ruler.
        
        :return     <int>
        """
        return self._padStart

    def percentAt(self, value):
        """
        Returns the percentage where the given value lies between this rulers
        minimum and maximum values.  If the value equals the minimum, then the
        percent is 0, if it equals the maximum, then the percent is 1 - any
        value between will be a floating point.  If the ruler is a custom type,
        then only if the value matches a notch will be successful.
        
        :param      value | <variant>
        
        :return     <float>
        """
        if (value is None):
            return 0.0

        minim = self.minimum()
        maxim = self.maximum()
        rtype = self.rulerType()

        # simple minimum
        if (value == minim and not self.padStart()):
            perc = 0.0

        # simple maximum
        elif (value == maxim and not self.padEnd()):
            perc = 1.0

        # calculate a numeric percentage value
        elif (rtype == XChartRuler.Type.Number):
            perc = float(value - minim) / float(maxim - minim)

        # calculate a time percentage value
        elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)):
            maxsecs = minim.secsTo(maxim)
            valsecs = minim.secsTo(value)

            perc = float(valsecs) / maxsecs

        # calculate a date percentage value
        elif (rtype == XChartRuler.Type.Date):
            maxdays = minim.daysTo(maxim)
            valdays = minim.daysTo(value)

            perc = float(valdays) / maxdays

        # otherwise, compare against the notches
        else:
            perc = 0.0
            notches = self.notches()
            count = len(notches)
            count += self.padStart() + self.padEnd()
            count = max(1, count - 1)

            perc = float(self.padStart()) / count

            for i, notch in enumerate(notches):
                if (notch == value):
                    perc += float(i) / count
                    break

        # normalize the percentage
        perc = min(perc, 1.0)
        perc = max(0, perc)

        return perc

    def rulerType(self):
        """
        Returns the ruler type for this ruler.
        
        :reutrn     <XChartRuler.Type>
        """
        return self._rulerType

    def setFormat(self, format):
        """
        Sets the string format that will be used when processing the ruler
        notches.  For a Number type, this will be used as a standard string
        format in Python (%i, $%0.02f, etc.).  For Datetime, Date, and Time
        types, it will be used with the QDate/QDateTime/QTime toString method.
        
        For Custom types, notches should be set manually.
        """
        self._format = format

    def setFormatter(self, formatter):
        """
        Sets the formatter method or callable that will accept one of the values
        for this ruler to render out the text display for the value.
        
        :param      formatter | <callable>
        """
        self._formatter = formatter

    def setMaximum(self, maximum):
        """
        Sets the maximum value for this ruler to the inputed value.
        
        :param      maximum | <variant>
        """
        self._maximum = maximum
        self._notches = None

    def setMinimum(self, minimum):
        """
        Sets the minimum value for this ruler to the inputed value.
        
        :param      minimum | <variant>
        """
        self._minimum = minimum
        self._notches = None

    def setNotches(self, notches):
        """
        Manually sets the notches list for this ruler to the inputed notches.
        
        :param      notches | [<str>, ..] || None
        """
        self._rulerType = XChartRuler.Type.Custom
        self._notches = notches

    def setNotchPadding(self, padding):
        """
        Sets the minimum padding amount used between notches.
        
        :param      padding | <int>
        """
        self._notchPadding = padding

    def setPadEnd(self, amount):
        """
        Sets the number of units to use as padding for the end of this ruler.
        
        :param      amount | <int>
        """
        self._padEnd = amount

    def setPadStart(self, amount):
        """
        Sets the number of units to use as padding for the beginning of
        this ruler.
        
        :param      amount | <int>
        """
        self._padStart = amount

    def setRulerType(self, rulerType):
        """
        Sets the ruler type for this ruler to the inputed type.
        
        :param      rulerType | <XChartRuler.Type>
        """
        self._rulerType = rulerType
        self.clear()

        # handle custom types
        if (rulerType == XChartRuler.Type.Monthly):
            self.setNotches([
                'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
                'Oct', 'Nov', 'Dec'
            ])

    def setStep(self, step):
        """
        Sets the step value for this ruler to the inputed value.  If the ruler
        type is Number, then an int or float value is acceptable.  If the type
        is Datetime or Date, then the step value is an integer representing
        days.  If the type is Time, then the step is an integer representing 
        seconds.
        
        :param      step | <int> || <float>
        """
        self._step = int(step)
        self._notches = None

    def setTitle(self, title):
        """
        Sets the title for this ruler to the inputed title.
        
        :param      title | <str>
        """
        self._title = title

    def step(self):
        """
        Returns the step value for this ruler.  If the cached value is None,
        then a default value will be specified based on the ruler type.
        
        :return     <variant>
        """
        if (self._step is not None):
            return self._step

        elif (self.rulerType() == XChartRuler.Type.Number):
            self._step = int((self.maximum() - self.minimum()) / 10.0)

        elif (self.rulerType() == XChartRuler.Type.Date):

            self._step = int(self.minimum().daysTo(self.maximum()) / 10.0) - 1

        elif ( self.rulerType() & (XChartRuler.Type.Time | \
                                   XChartRuler.Type.Datetime) ):
            self._step = int(self.minimum().secsTo(self.maximum()) / 10.0) - 1

        return self._step

    def title(self):
        """
        Returns the title for this ruler.
        
        :return     <str>
        """
        return self._title

    def valueAt(self, percent):
        """
        Returns the value at the inputed percent.
        
        :param     percent | <float>
        
        :return     <variant>
        """
        minim = self.minimum()
        maxim = self.maximum()
        rtype = self.rulerType()

        # simple minimum
        if (percent <= 0):
            return minim

        # simple maximum
        elif (1 <= percent):
            return maxim

        # calculate a numeric percentage value
        elif (rtype == XChartRuler.Type.Number):
            return (maxim - minim) * percent

        # calculate a time percentage value
        elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)):
            maxsecs = minim.secsTo(maxim)

            diff = maxssecs * percent
            return minim.addSecs(diff)

        # calculate a date percentage value
        elif (rtype == XChartRuler.Type.Date):
            maxdays = minim.daysTo(maxim)

            diff = maxdays * percent
            return minim.addDays(diff)

        # otherwise, compare against the notches
        else:
            perc = 0.0
            notches = self.notches()
            count = len(notches)
            count += self.padStart() + self.padEnd()
            count = max(1, count - 1)

            perc = float(self.padStart()) / count
            last = None

            for i, notch in enumerate(notches):
                perc += float(i) / count
                if (perc <= percent):
                    break

                last = notch

            return last
Exemple #18
0
class XFilepathEdit(QWidget):
    """
    The XFilepathEdit class provides a common interface to prompt the user to
    select a filepath from the filesystem.  It can be configured to load
    directories, point to a save file path location, or to an open file path
    location.  It can also be setup to color changed based on the validity
    of the existance of the filepath.
    
    == Example ==
    
    |>>> from projexui.widgets.xfilepathedit import XFilepathEdit
    |>>> import projexui
    |
    |>>> # create the edit
    |>>> edit = projexui.testWidget(XFilepathEdit)
    |
    |>>> # set the filepath
    |>>> edit.setFilepath('/path/to/file')
    |
    |>>> # prompt the user to select the filepath
    |>>> edit.pickFilepath()
    |
    |>>> # enable the coloring validation
    |>>> edit.setValidated(True)
    """

    __designer_icon__ = projexui.resources.find('img/file.png')

    Mode = enum('OpenFile', 'SaveFile', 'Path', 'OpenFiles')

    filepathChanged = qt.Signal(str)

    def __init__(self, parent=None):
        super(XFilepathEdit, self).__init__(parent)

        # define custom properties
        self._validated = False
        self._validForeground = QColor(0, 120, 0)
        self._validBackground = QColor(0, 120, 0, 100)
        self._invalidForeground = QColor(255, 0, 0)
        self._invalidBackground = QColor(255, 0, 0, 100)
        self._normalizePath = False

        self._filepathMode = XFilepathEdit.Mode.OpenFile
        self._filepathEdit = XLineEdit(self)
        self._filepathButton = QToolButton(self)
        self._filepathTypes = 'All Files (*.*)'

        # set default properties
        ico = projexui.resources.find('img/folder.png')
        self._filepathEdit.setReadOnly(False)
        self._filepathButton.setText('...')
        self._filepathButton.setFixedSize(25, 23)
        self._filepathButton.setAutoRaise(True)
        self._filepathButton.setIcon(QIcon(ico))
        self._filepathEdit.setContextMenuPolicy(Qt.CustomContextMenu)

        self.setWindowTitle('Load File')
        self.setAcceptDrops(True)

        # define the layout
        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self._filepathEdit)
        layout.addWidget(self._filepathButton)
        self.setLayout(layout)

        # create connections
        self._filepathEdit.installEventFilter(self)

        self._filepathButton.clicked.connect(self.pickFilepath)
        self._filepathEdit.textChanged.connect(self.emitFilepathChanged)
        self._filepathEdit.textChanged.connect(self.validateFilepath)
        self._filepathEdit.customContextMenuRequested.connect(self.showMenu)

    def autoRaise(self):
        """
        Returns whether or not the tool button will auto raise.
        
        :return     <bool>
        """
        return self._filepathButton.autoRaise()

    @qt.Slot()
    def clearFilepath(self):
        """
        Clears the filepath contents for this path.
        """
        self.setFilepath('')

    @qt.Slot()
    def copyFilepath(self):
        """
        Copies the current filepath contents to the current clipboard.
        """
        clipboard = QApplication.instance().clipboard()
        clipboard.setText(self.filepath())
        clipboard.setText(self.filepath(), clipboard.Selection)

    def dragEnterEvent(self, event):
        """
        Processes drag enter events.
        
        :param      event | <QDragEvent>
        """
        if (event.mimeData().hasUrls()):
            event.acceptProposedAction()

    def dragMoveEvent(self, event):
        """
        Processes drag move events.
        
        :param      event | <QDragEvent>
        """
        if (event.mimeData().hasUrls()):
            event.acceptProposedAction()

    def dropEvent(self, event):
        """
        Processes drop event.
        
        :param      event | <QDropEvent>
        """
        if (event.mimeData().hasUrls()):
            url = event.mimeData().urls()[0]
            filepath = url.toLocalFile()
            if (filepath):
                self.setFilepath(filepath)

    def emitFilepathChanged(self):
        """
        Emits the filepathChanged signal for this widget if the signals are \
        not being blocked.
        """
        if (not self.signalsBlocked()):
            self.filepathChanged.emit(self.filepath())

    def eventFilter(self, object, event):
        """
        Overloads the eventFilter to look for click events on the line edit.
        
        :param      object | <QObject>
                    event  | <QEvent>
        """
        if ( object == self._filepathEdit and \
             self._filepathEdit.isReadOnly() and \
             event.type() == event.MouseButtonPress and \
             event.button() == Qt.LeftButton ):
            self.pickFilepath()

        return False

    def filepath(self, validated=False):
        """
        Returns the filepath for this widget.  If the validated flag is set \
        then this method will only return if the file or folder actually \
        exists for this path.  In the case of a SaveFile, only the base folder \
        needs to exist on the system, in other modes the actual filepath must \
        exist.  If not validated, the text will return whatever is currently \
        entered.
        
        :return     <str>
        """
        paths = self.filepaths()
        if not paths:
            return ''

        if not validated or self.isValid():
            return paths[0]
        return ''

    def filepaths(self):
        """
        Returns a list of the filepaths for this edit.
        
        :return     [<str>, ..]
        """
        return str(self._filepathEdit.text()).split(os.path.pathsep)

    def filepathMode(self):
        """
        Returns the filepath mode for this widget.
        
        :return     <XFilepathEdit.Mode>
        """
        return self._filepathMode

    def filepathModeText(self):
        """
        Returns the text representation for this filepath mode.
        
        :return     <str>
        """
        return XFilepathEdit.Mode[self._filepathMode]

    def filepathTypes(self):
        """
        Returns the filepath types that will be used for this widget.
        
        :return     <str>
        """
        return self._filepathTypes

    def hint(self):
        """
        Returns the hint for this filepath.
        
        :return     <str>
        """
        return self._filepathEdit.hint()

    def icon(self):
        """
        Returns the icon that is used for this filepath widget.
        
        :return     <QIcon>
        """
        return self._filepathButton.icon()

    def invalidBackground(self):
        """
        Returns the invalid background color for this widget.
        
        :return     <QColor>
        """
        return self._invalidBackground

    def invalidForeground(self):
        """
        Returns the invalid foreground color for this widget.
        
        :return     <QColor>
        """
        return self._invalidForeground

    def isValid(self):
        """
        Returns whether or not the filepath exists on the system. \
        In the case of a SaveFile, only the base folder \
        needs to exist on the system, in other modes the actual filepath must \
        exist.
        
        :return     <bool>
        """
        check = str(self._filepathEdit.text()).split(os.path.pathsep)[0]
        if (self.filepathMode() == XFilepathEdit.Mode.SaveFile):
            check = os.path.dirname(check)

        return os.path.exists(check)

    def isValidated(self):
        """
        Set whether or not to validate the filepath as the user is working \
        with it.
        
        :return     <bool>
        """
        return self._validated

    def isReadOnly(self):
        """
        Returns if the widget is read only for text editing or not.
        
        :return     <bool>
        """
        return self._filepathEdit.isReadOnly()

    def normalizePath(self):
        """
        Returns whether or not the path should be normalized for the current
        operating system.  When off, it will be defaulted to forward slashes
        (/).
        
        :return     <bool>
        """
        return self._normalizePath

    def pickFilepath(self):
        """
        Prompts the user to select a filepath from the system based on the \
        current filepath mode.
        """
        mode = self.filepathMode()

        filepath = ''
        filepaths = []
        curr_dir = str(self._filepathEdit.text())
        if (not curr_dir):
            curr_dir = QDir.currentPath()

        if mode == XFilepathEdit.Mode.SaveFile:
            filepath = QFileDialog.getSaveFileName(self, self.windowTitle(),
                                                   curr_dir,
                                                   self.filepathTypes())

        elif mode == XFilepathEdit.Mode.OpenFile:
            filepath = QFileDialog.getOpenFileName(self, self.windowTitle(),
                                                   curr_dir,
                                                   self.filepathTypes())

        elif mode == XFilepathEdit.Mode.OpenFiles:
            filepaths = QFileDialog.getOpenFileNames(self, self.windowTitle(),
                                                     curr_dir,
                                                     self.filepathTypes())

        else:
            filepath = QFileDialog.getExistingDirectory(
                self, self.windowTitle(), curr_dir)

        if filepath:
            if type(filepath) == tuple:
                filepath = filepath[0]
            self.setFilepath(str(filepath))
        elif filepaths:
            self.setFilepaths(map(str, filepaths))

    def setAutoRaise(self, state):
        """
        Sets whether or not the tool button will auto raise.
        
        :param      state | <bool>
        """
        self._filepathButton.setAutoRaise(state)

    @qt.Slot(int)
    def setFilepathMode(self, mode):
        """
        Sets the filepath mode for this widget to the inputed mode.
        
        :param      mode | <XFilepathEdit.Mode>
        """
        self._filepathMode = mode

    @qt.Slot(str)
    def setFilepathModeText(self, text):
        """
        Sets the filepath mode for this widget based on the inputed text.
        
        :param      text | <str>
        
        :return     <bool> | success
        """
        try:
            self.setFilepathMode(XFilepathEdit.Mode[str(text)])
            return True
        except KeyError:
            return False

    @qt.Slot(str)
    def setFilepathTypes(self, filepathTypes):
        """
        Sets the filepath type string that will be used when looking up \
        filepaths.
        
        :param      filepathTypes | <str>
        """
        self._filepathTypes = filepathTypes

    @qt.Slot(str)
    def setFilepath(self, filepath):
        """
        Sets the filepath text for this widget to the inputed path.
        
        :param      filepath | <str>
        """
        if self.normalizePath():
            filepath = os.path.normpath(str(filepath))
        else:
            filepath = os.path.normpath(str(filepath)).replace('\\', '/')

        self._filepathEdit.setText(filepath)

    def setFilepaths(self, filepaths):
        """
        Sets the list of the filepaths for this widget to the inputed paths.
        
        :param      filepaths | [<str>, ..]
        """
        self.setFilepath(os.path.pathsep.join(filepaths))

    def setHint(self, hint):
        """
        Sets the hint for this filepath.
        
        :param      hint | <str>
        """
        if self.normalizePath():
            filepath = os.path.normpath(str(hint))
        else:
            filepath = os.path.normpath(str(hint)).replace('\\', '/')

        self._filepathEdit.setHint(hint)

    def setIcon(self, icon):
        """
        Sets the icon that will be used for this widget's tool button.
        
        :param      icon | <QIcon> || <str>
        """
        self._filepathButton.setIcon(QIcon(icon))

    def setInvalidBackground(self, bg):
        """
        Sets the invalid background color for this widget to the inputed widget.
        
        :param      bg | <QColor>
        """
        self._invalidBackground = QColor(bg)

    def setInvalidForeground(self, fg):
        """
        Sets the invalid foreground color for this widget to the inputed widget.
        
        :param      fg | <QColor>
        """
        self._invalidForeground = QColor(fg)

    def setNormalizePath(self, state):
        """
        Sets whether or not the path should be normalized for the current
        operating system.  When off, it will be defaulted to forward slashes
        (/).
        
        :param      state | <bool>
        """
        self._normalizePath = state

    @qt.Slot(bool)
    def setReadOnly(self, state):
        """
        Sets whether or not this filepath widget is readonly in the text edit.
        
        :param      state | <bool>
        """
        self._filepathEdit.setReadOnly(state)

    @qt.Slot(bool)
    def setValidated(self, state):
        """
        Set whether or not to validate the path as the user edits it.
        
        :param      state | <bool>
        """
        self._validated = state
        palette = self.palette()

        # reset the palette to default, revalidate
        self._filepathEdit.setPalette(palette)
        self.validate()

    def setValidBackground(self, bg):
        """
        Sets the valid background color for this widget to the inputed color.
        
        :param      bg | <QColor>
        """
        self._validBackground = QColor(bg)

    def setValidForeground(self, fg):
        """
        Sets the valid foreground color for this widget to the inputed color.
        
        :param      fg | <QColor>
        """
        self._validForeground = QColor(fg)

    def showMenu(self, pos):
        """
        Popups a menu for this widget.
        """
        menu = QMenu(self)
        menu.setAttribute(Qt.WA_DeleteOnClose)
        menu.addAction('Clear').triggered.connect(self.clearFilepath)
        menu.addSeparator()
        menu.addAction('Copy Filepath').triggered.connect(self.copyFilepath)

        menu.exec_(self.mapToGlobal(pos))

    def validBackground(self):
        """
        Returns the valid background color for this widget.
        
        :return     <QColor>
        """
        return self._validBackground

    def validForeground(self):
        """
        Returns the valid foreground color for this widget.
        
        :return     <QColor>
        """
        return self._validForeground

    def validateFilepath(self):
        """
        Alters the color scheme based on the validation settings.
        """
        if (not self.isValidated()):
            return

        valid = self.isValid()
        if (not valid):
            fg = self.invalidForeground()
            bg = self.invalidBackground()
        else:
            fg = self.validForeground()
            bg = self.validBackground()

        palette = self.palette()
        palette.setColor(palette.Base, bg)
        palette.setColor(palette.Text, fg)
        self._filepathEdit.setPalette(palette)

    # map Qt properties
    x_autoRaise = qt.Property(bool, autoRaise, setAutoRaise)
    x_filepathTypes = qt.Property(str, filepathTypes, setFilepathTypes)
    x_filepath = qt.Property(str, filepath, setFilepath)
    x_readOnly = qt.Property(bool, isReadOnly, setReadOnly)
    x_validated = qt.Property(bool, isValidated, setValidated)
    x_hint = qt.Property(str, hint, setHint)
    x_icon = qt.Property('QIcon', icon, setIcon)
    x_normalizePath = qt.Property(bool, normalizePath, setNormalizePath)

    x_invalidForeground = qt.Property('QColor', invalidForeground,
                                      setInvalidForeground)

    x_invalidBackground = qt.Property('QColor', invalidBackground,
                                      setInvalidBackground)

    x_validForeground = qt.Property('QColor', validForeground,
                                    setValidForeground)

    x_validBackground = qt.Property('QColor', validBackground,
                                    setValidBackground)

    x_filepathModeText = qt.Property(str, filepathModeText,
                                     setFilepathModeText)
Exemple #19
0
class XGanttWidget(QWidget):
    dateRangeChanged = Signal()
    itemMenuRequested = Signal(QTreeWidgetItem, QPoint)
    viewMenuRequested = Signal(QGraphicsItem, QPoint)
    treeMenuRequested = Signal(QTreeWidgetItem, QPoint)

    Timescale = enum('Minute', 'Hour', 'Day', 'Week', 'Month', 'Year')

    def __init__(self, parent=None):
        super(XGanttWidget, self).__init__(parent)

        # load the user interface
        projexui.loadUi(__file__, self)

        # define custom properties
        self._backend = None
        self._dateStart = QDate.currentDate().addMonths(-2)
        self._dateEnd = QDate.currentDate().addMonths(2)
        self._timeStart = QTime(0, 0, 0)
        self._timeEnd = QTime(23, 59, 59)
        self._alternatingRowColors = False
        self._cellWidth = 20
        self._cellHeight = 20
        self._first = True
        self._dateFormat = 'M/d/yy'
        self._timescale = XGanttWidget.Timescale.Month
        self._scrolling = False
        self._dirty = False

        # setup the palette colors
        palette = self.palette()
        color = palette.color(palette.Base)

        self._gridPen = QPen(color.darker(115))
        self._brush = QBrush(color)
        self._alternateBrush = QBrush(color.darker(105))

        weekendColor = color.darker(108)
        self._weekendBrush = QBrush(weekendColor)

        # setup the columns for the tree
        self.setColumns(['Name', 'Start', 'End', 'Calendar Days', 'Work Days'])

        header = self.uiGanttTREE.header()
        header.setFixedHeight(self._cellHeight * 2)
        headerItem = self.uiGanttTREE.headerItem()
        headerItem.setSizeHint(0, QSize(80, header.height()))

        # initialize the tree widget
        self.uiGanttTREE.setShowGrid(False)
        self.uiGanttTREE.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.uiGanttTREE.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.uiGanttTREE.setVerticalScrollMode(self.uiGanttTREE.ScrollPerPixel)
        self.uiGanttTREE.setResizeToContentsInteractive(True)
        self.uiGanttTREE.setEditable(True)
        self.uiGanttTREE.resize(500, 20)
        self.uiGanttTREE.setContextMenuPolicy(Qt.CustomContextMenu)

        # initialize the view widget
        self.uiGanttVIEW.setDragMode(self.uiGanttVIEW.RubberBandDrag)
        self.uiGanttVIEW.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.uiGanttVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.uiGanttVIEW.setScene(XGanttScene(self))
        self.uiGanttVIEW.installEventFilter(self)
        self.uiGanttVIEW.horizontalScrollBar().setValue(50)
        self.uiGanttVIEW.setContextMenuPolicy(Qt.CustomContextMenu)

        # create connections
        self.uiGanttTREE.itemExpanded.connect(self.syncView)
        self.uiGanttTREE.itemCollapsed.connect(self.syncView)

        # connect scrollbars
        tree_bar = self.uiGanttTREE.verticalScrollBar()
        view_bar = self.uiGanttVIEW.verticalScrollBar()

        tree_bar.rangeChanged.connect(self._updateViewRect)
        tree_bar.valueChanged.connect(self._scrollView)
        view_bar.valueChanged.connect(self._scrollTree)

        # connect selection
        self.uiGanttTREE.itemSelectionChanged.connect(self._selectView)
        self.uiGanttVIEW.scene().selectionChanged.connect(self._selectTree)
        self.uiGanttTREE.itemChanged.connect(self.updateItemData)
        self.uiGanttTREE.customContextMenuRequested.connect(
            self.requestTreeMenu)
        self.uiGanttVIEW.customContextMenuRequested.connect(
            self.requestViewMenu)

    def _scrollTree(self, value):
        """
        Updates the tree view scrolling to the inputed value.
        
        :param      value | <int>
        """
        if self._scrolling:
            return

        tree_bar = self.uiGanttTREE.verticalScrollBar()
        self._scrolling = True
        tree_bar.setValue(value)
        self._scrolling = False

    def _scrollView(self, value):
        """
        Updates the gantt view scrolling to the inputed value.
        
        :param      value | <int>
        """
        if self._scrolling:
            return

        view_bar = self.uiGanttVIEW.verticalScrollBar()
        self._scrolling = True
        view_bar.setValue(value)
        self._scrolling = False

    def _selectTree(self):
        """
        Matches the tree selection to the views selection.
        """
        self.uiGanttTREE.blockSignals(True)
        self.uiGanttTREE.clearSelection()
        for item in self.uiGanttVIEW.scene().selectedItems():
            item.treeItem().setSelected(True)
        self.uiGanttTREE.blockSignals(False)

    def _selectView(self):
        """
        Matches the view selection to the trees selection.
        """
        scene = self.uiGanttVIEW.scene()
        scene.blockSignals(True)
        scene.clearSelection()
        for item in self.uiGanttTREE.selectedItems():
            item.viewItem().setSelected(True)
        scene.blockSignals(False)

        curr_item = self.uiGanttTREE.currentItem()
        vitem = curr_item.viewItem()

        if vitem:
            self.uiGanttVIEW.centerOn(vitem)

    def _updateViewRect(self):
        """
        Updates the view rect to match the current tree value.
        """
        if not self.updatesEnabled():
            return

        header_h = self._cellHeight * 2
        rect = self.uiGanttVIEW.scene().sceneRect()
        sbar_max = self.uiGanttTREE.verticalScrollBar().maximum()
        sbar_max += self.uiGanttTREE.viewport().height() + header_h
        widget_max = self.uiGanttVIEW.height()
        widget_max -= (self.uiGanttVIEW.horizontalScrollBar().height() + 10)

        rect.setHeight(max(widget_max, sbar_max))
        self.uiGanttVIEW.scene().setSceneRect(rect)

    def addTopLevelItem(self, item):
        """
        Adds the inputed item to the gantt widget.
        
        :param      item | <XGanttWidgetItem>
        """
        vitem = item.viewItem()

        self.treeWidget().addTopLevelItem(item)
        self.viewWidget().scene().addItem(vitem)

        item._viewItem = weakref.ref(vitem)

        if self.updatesEnabled():
            try:
                item.sync(recursive=True)
            except AttributeError:
                pass

    def alternateBrush(self):
        """
        Returns the alternate brush to be used for the grid view.
        
        :return     <QBrush>
        """
        return self._alternateBrush

    def alternatingRowColors(self):
        """
        Returns whether or not this widget should show alternating row colors.
        
        :return     <bool>
        """
        return self._alternatingRowColors

    def blockSignals(self, state):
        """
        Sets whether or not updates will be enabled.
        
        :param      state | <bool>
        """
        super(XGanttWidget, self).blockSignals(state)
        self.treeWidget().blockSignals(state)
        self.viewWidget().blockSignals(state)

    def brush(self):
        """
        Returns the background brush to be used for the grid view.
        
        :return     <QBrush>
        """
        return self._brush

    def centerOnDateTime(self, dtime):
        """
        Centers the view on a given datetime for the gantt widget.
        
        :param      dtime | <QDateTime>
        """
        view = self.uiGanttVIEW
        scene = view.scene()
        point = view.mapToScene(0, 0)
        x = scene.datetimeXPos(dtime)
        y = point.y()
        view.centerOn(x, y)

    def cellHeight(self):
        """
        Returns the height for the cells in this gantt's views.
        
        :return     <int>
        """
        return self._cellHeight

    def cellWidth(self):
        """
        Returns the width for the cells in this gantt's views.
        
        :return     <int>
        """
        return self._cellWidth

    def clear(self):
        """
        Clears all the gantt widget items for this widget.
        """
        self.uiGanttTREE.clear()
        self.uiGanttVIEW.scene().clear()

    def columns(self):
        """
        Returns a list of the columns being used in the treewidget of this gantt
        chart.
        
        :return     [<str>, ..]
        """
        return self.treeWidget().columns()

    def currentDateTime(self):
        """
        Returns the current date time for this widget.
        
        :return     <datetime.datetime>
        """
        view = self.uiGanttVIEW
        scene = view.scene()
        point = view.mapToScene(0, 0)
        return scene.datetimeAt(point.x())

    def dateEnd(self):
        """
        Returns the date end for this date range of this gantt widget.
        
        :return     <QDate>
        """
        return self._dateEnd

    def dateFormat(self):
        """
        Returns the date format that will be used when rendering items in the
        view.
        
        :return     <str>
        """
        return self._dateFormat

    def dateTimeEnd(self):
        """
        Returns the end date time for this gantt chart.
        
        :return     <QDateTime>
        """
        return QDateTime(self.dateEnd(), self.timeEnd())

    def dateTimeStart(self):
        """
        Returns the start date time for this gantt chart.
        
        :return     <QDateTime>
        """
        return QDateTime(self.dateStart(), self.timeStart())

    def dateStart(self):
        """
        Returns the date start for the date range of this gantt widget.
        
        :return     <QDate>
        """
        return self._dateStart

    def emitDateRangeChanged(self):
        """
        Emits the date range changed signal provided signals aren't being
        blocked.
        """
        if not self.signalsBlocked():
            self.dateRangeChanged.emit()

    def gridPen(self):
        """
        Returns the pen that this widget uses to draw in the view.
        
        :return     <QPen>
        """
        return self._gridPen

    def indexOfTopLevelItem(self, item):
        """
        Returns the index for the inputed item from the tree.
        
        :return     <int>
        """
        return self.treeWidget().indexOfTopLevelItem(item)

    def insertTopLevelItem(self, index, item):
        """
        Inserts the inputed item at the given index in the tree.
        
        :param      index   | <int>
                    item    | <XGanttWidgetItem>
        """
        self.treeWidget().insertTopLevelItem(index, item)

        if self.updatesEnabled():
            try:
                item.sync(recursive=True)
            except AttributeError:
                pass

    def rebuild(self):
        self.uiGanttVIEW.scene().rebuild()

    def requestTreeMenu(self, point):
        """
        Emits the itemMenuRequested and treeMenuRequested signals
        for the given item.
        
        :param      point | <QPoint>
        """
        item = self.uiGanttTREE.itemAt(point)
        if item:
            glbl_pos = self.uiGanttTREE.viewport().mapToGlobal(point)
            self.treeMenuRequested.emit(item, glbl_pos)
            self.itemMenuRequested.emit(item, glbl_pos)

    def requestViewMenu(self, point):
        """
        Emits the itemMenuRequested and viewMenuRequested signals
        for the given item.
        
        :param      point | <QPoint>
        """
        vitem = self.uiGanttVIEW.itemAt(point)
        if vitem:
            glbl_pos = self.uiGanttVIEW.mapToGlobal(point)
            item = vitem.treeItem()
            self.viewMenuRequested.emit(vitem, glbl_pos)
            self.itemMenuRequested.emit(item, glbl_pos)

    def setAlternateBrush(self, brush):
        """
        Sets the alternating brush used for this widget to the inputed brush.
        
        :param      brush | <QBrush> || <QColor>
        """
        self._alternateBrush = QBrush(brush)

    def setAlternatingRowColors(self, state):
        """
        Sets the alternating row colors state for this widget.
        
        :param      state | <bool>
        """
        self._alternatingRowColors = state

        self.treeWidget().setAlternatingRowColors(state)

    def setBrush(self, brush):
        """
        Sets the main background brush used for this widget to the inputed
        brush.
        
        :param      brush | <QBrush> || <QColor>
        """
        self._brush = QBrush(brush)

    def setCellHeight(self, cellHeight):
        """
        Sets the height for the cells in this gantt's views.
        
        :param      cellHeight | <int>
        """
        self._cellHeight = cellHeight

    def setCellWidth(self, cellWidth):
        """
        Sets the width for the cells in this gantt's views.
        
        :param      cellWidth | <int>
        """
        self._cellWidth = cellWidth

    def setColumns(self, columns):
        """
        Sets the columns for this gantt widget's tree to the inputed list of
        columns.
        
        :param      columns | {<str>, ..]
        """
        self.treeWidget().setColumns(columns)
        item = self.treeWidget().headerItem()
        for i in range(item.columnCount()):
            item.setTextAlignment(i, Qt.AlignBottom | Qt.AlignHCenter)

    def setDateEnd(self, dateEnd):
        """
        Sets the end date for the range of this gantt widget.
        
        :param      dateEnd | <QDate>
        """
        self._dateEnd = dateEnd
        self.emitDateRangeChanged()

    def setDateFormat(self, format):
        """
        Sets the date format that will be used when rendering in the views.
        
        :return     <str>
        """
        return self._dateFormat

    def setDateStart(self, dateStart):
        """
        Sets the start date for the range of this gantt widget.
        
        :param      dateStart | <QDate>
        """
        self._dateStart = dateStart
        self.emitDateRangeChanged()

    def setDateTimeEnd(self, dtime):
        """
        Sets the endiing date time for this gantt chart.
        
        :param      dtime | <QDateTime>
        """
        self._dateEnd = dtime.date()

        if self.timescale() in (self.Timescale.Minute, self.Timescale.Hour):
            self._timeEnd = dtime.time()
        else:
            self._timeEnd = QTime(23, 59, 59)

    def setDateTimeStart(self, dtime):
        """
        Sets the starting date time for this gantt chart.
        
        :param      dtime | <QDateTime>
        """
        self._dateStart = dtime.date()
        if self.timescale() in (self.Timescale.Minute, self.Timescale.Hour):
            self._timeStart = dtime.time()
        else:
            self._timeStart = QTime(0, 0, 0)

    def setCurrentDateTime(self, dtime):
        """
        Sets the current date time for this widget.
        
        :param      dtime | <datetime.datetime>
        """
        view = self.uiGanttVIEW
        scene = view.scene()
        point = view.mapToScene(0, 0)
        x = scene.datetimeXPos(dtime)
        y = point.y()
        view.ensureVisible(x, y, 1, 1)

    def setGridPen(self, pen):
        """
        Sets the pen used to draw the grid lines for the view.
        
        :param      pen | <QPen> || <QColor>
        """
        self._gridPen = QPen(pen)

    def setTimescale(self, timescale):
        """
        Sets the timescale value for this widget to the inputed value.
        
        :param      timescale | <XGanttWidget.Timescale>
        """
        self._timescale = timescale

        # show hour/minute scale
        if timescale == XGanttWidget.Timescale.Minute:
            self._cellWidth = 60  # (60 seconds)
            self._dateStart = QDate.currentDate()
            self._timeStart = QTime(0, 0, 0)

            self._dateEnd = QDate.currentDate()
            self._timeEnd = QTime(23, 59, 59)

        elif timescale == XGanttWidget.Timescale.Hour:
            self._cellWidth = 30  # (60 seconds / 2.0)

            self._dateStart = QDate.currentDate()
            self._timeStart = QTime(0, 0, 0)

            self._dateEnd = QDate.currentDate()
            self._timeEnd = QTime(23, 59, 59)

        # show day/hour scale
        elif timescale == XGanttWidget.Timescale.Day:
            self._cellWidth = 30  # (60 minutes / 2.0)

            self._dateStart = QDate.currentDate().addDays(-7)
            self._timeStart = QTime(0, 0, 0)

            self._dateEnd = QDate.currentDate().addDays(7)
            self._timeEnd = QTime(23, 59, 59)

    def setTimeEnd(self, time):
        """
        Sets the ending time for this gantt chart.
        
        :param      time | <QTime>
        """
        self._timeEnd = time

    def setTimeStart(self, time):
        """
        Sets the starting time for this gantt chart.
        
        :param      time | <QTime>
        """
        self._timeStart = time

    def setUpdatesEnabled(self, state):
        """
        Sets whether or not updates will be enabled.
        
        :param      state | <bool>
        """
        super(XGanttWidget, self).setUpdatesEnabled(state)
        self.treeWidget().setUpdatesEnabled(state)
        self.viewWidget().setUpdatesEnabled(state)

        if state:
            self._updateViewRect()
            for i in range(self.topLevelItemCount()):
                item = self.topLevelItem(i)
                try:
                    item.sync(recursive=True)
                except AttributeError:
                    continue

    def setWeekendBrush(self, brush):
        """
        Sets the brush to be used when coloring weekend columns.
        
        :param      brush | <QBrush> || <QColor>
        """
        self._weekendBrush = QBrush(brush)

    def syncView(self):
        """
        Syncs all the items to the view.
        """
        if not self.updatesEnabled():
            return

        for item in self.topLevelItems():
            try:
                item.syncView(recursive=True)
            except AttributeError:
                continue

    def takeTopLevelItem(self, index):
        """
        Removes the top level item at the inputed index from the widget.
        
        :param      index | <int>
        
        :return     <XGanttWidgetItem> || None
        """
        item = self.topLevelItem(index)
        if item:
            self.viewWidget().scene().removeItem(item.viewItem())
            self.treeWidget().takeTopLevelItem(index)

            return item
        return None

    def timescale(self):
        """
        Returns the timescale that is being used for this widget.
        
        :return     <XGanttWidget.Timescale>
        """
        return self._timescale

    def timeEnd(self):
        """
        Returns the ending time for this gantt chart.  Default value
        will be QTime(0, 0, 0)
        
        :return     <QTime>
        """
        return self._timeEnd

    def timeStart(self):
        """
        Returns the starting time for this gantt chart.  Default value
        will be QTime(0, 0, 0)
        
        :return     <QTime>
        """
        return self._timeStart

    def topLevelItems(self):
        """
        Return the top level item generator.
        
        :return     <generator [<QTreeWidgetItem>, ..]>
        """
        return self.treeWidget().topLevelItems()

    def topLevelItem(self, index):
        """
        Returns the top level item at the inputed index.
        
        :return     <QTreeWidgetItem>
        """
        return self.treeWidget().topLevelItem(index)

    def topLevelItemCount(self):
        """
        Returns the number of top level items for this widget.
        
        :return     <int>
        """
        return self.treeWidget().topLevelItemCount()

    def treeWidget(self):
        """
        Returns the tree widget for this gantt widget.
        
        :return     <QTreeWidget>
        """
        return self.uiGanttTREE

    def updateItemData(self, item, index):
        """
        Updates the item information from the tree.
        
        :param      item    | <XGanttWidgetItem>
                    index   | <int>
        """
        from projexui.widgets.xganttwidget.xganttwidgetitem import XGanttWidgetItem
        if not isinstance(item, XGanttWidgetItem):
            return

        value = unwrapVariant(item.data(index, Qt.EditRole))

        if type(value) == QDateTime:
            value = value.date()
            item.setData(index, Qt.EditRole, wrapVariant(value))

        if type(value) == QDate:
            value = value.toPython()

        columnName = self.treeWidget().columnOf(index)
        item.setProperty(columnName, value)
        item.sync()

    def viewWidget(self):
        """
        Returns the view widget for this gantt widget.
        
        :return     <QGraphicsView>
        """
        return self.uiGanttVIEW

    def weekendBrush(self):
        """
        Returns the weekend brush to be used for coloring in weekends.
        
        :return     <QBrush>
        """
        return self._weekendBrush
Exemple #20
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()
Exemple #21
0
class Plugin(object):
    """
    Defines the base class for generating a plugin based system in Python.
    This class is just an abstract framework to provide an interface to
    managing multiple objects of a particular plugin type through a simple
    registration system.  To use, you will need to subclass this object and
    implement your own abstract and virtual methods to provide an interface
    to your application, however it will handle all the common methods such
    as registration, enabling, etc. from here.
    
    :usage      |from projex.plugin import Plugin
                |from projex.decorators import abstractmethod
                |
                |class MyPlugin(Plugin):
                |   @abstractmethod
                |   def convert(self, value):
                |       pass
                |
                |class StringPlugin(MyPlugin):
                |   def __init__( self ):
                |       super(StringPlugin, self).__init__('string')
                |
                |   def convert(self, value):
                |       return str(value)
                |
                |class BoolPlugin(MyPlugin):
                |   def __init__( self ):
                |       super(StringPlugin, self).__init__('bool')
                |
                |   def convert(self, value):
                |       return bool(value)
                |
                |MyPlugin.register(StringPlugin())
                |MyPlugin.register(BoolPlugin())
                |
                |for plugin in MyPlugin.plugins():
                |   print plugin.instance().convert(1)
                |
                |print MyPlugin.plugin('string').instance().convert(1)
                
    """
    # define enums
    Type = enum('Module', 'Package', 'RegistryFile')

    def __init__(self, name, version=1.0):
        # define basic information
        self._name = str(name)
        self._version = version

        # define advanced information
        self._enabled = True
        self._filepath = ''
        self._icon = ''
        self._description = ''

        # define error information
        self._loaded = True
        self._instance = self
        self._error = None

        # define authorship information
        self._author = ''
        self._email = ''
        self._url = ''

        # define the filepath loading information
        mod = sys.modules.get(self.__class__.__module__)
        if (mod):
            self._filepath = os.path.abspath(mod.__file__)

    def author(self):
        """
        Returns the author information for this plugin.
        
        :return     <str>
        """
        return self._author

    def description(self):
        """
        Returns the description for this plugin.
        
        :return     <str>
        """
        return self._description

    def disable(self):
        """
        Disables this plugin.
        
        :sa     setEnabled
        """
        self.setEnabled(False)

    def email(self):
        """
        Returns the email information for this plugin.
        
        :return     <str>
        """
        return self._email

    def enable(self):
        """
        Enables this plugin.
        
        :sa     setEnabled
        """
        self.setEnabled(True)

    def error(self):
        """
        Returns the error from this plugin.
        
        :return     <Exception> || None
        """
        return self._error

    def filepath(self):
        """
        Returns the filepath from which this plugin was loaded.
        
        :return     <str>
        """
        return self._filepath

    def icon(self):
        """
        Returns the icon filepath for this plugin.
        
        :return     <str>
        """
        path = self._icon
        if (not path):
            return ''

        path = os.path.expandvars(os.path.expanduser(path))
        if (path.startswith('.')):
            base_path = os.path.dirname(self.filepath())
            path = os.path.abspath(os.path.join(base_path, path))

        return path

    def instance(self):
        """
        Returns the main instance of this plugin.  When wanting to use
        custom methods from a plugin class, call this method first, as plugins
        may decide to generate their instances in different ways.  By default,
        this will just return the self value.  
        
        For instance, a PluginProxy class will load its plugin registration 
        information during the initialization stage, but not load the actual
        class until the instance method is called, delaying the load of the 
        actual module until later.
        """
        return self._instance

    def isEnabled(self):
        """
        Returns whether or not this plugin is enabled.
        
        :return     <bool>
        """
        return self._enabled and not self.isErrored()

    def isErrored(self):
        """
        Returns whether or not the plugin ran into errors.
        
        :return     <bool>
        """
        return self._error is not None

    def isLoaded(self):
        """
        Returns whether or not this plugin has been loaded or not.
        
        :return     <bool>
        """
        return self._loaded

    def name(self):
        """
        Returns the name for this plugin.
        
        :return     <str>
        """
        return self._name

    def setAuthor(self, author):
        """
        Sets the author information for this plugin.
        
        :param      author | <str>
        """
        self._author = str(author)

    def setDescription(self, description):
        """
        Sets the description for this plugin to the inputed plugin.
        
        :param      description | <str>
        """
        self._description = description

    def setEmail(self, email):
        """
        Sets the email information for this plugin.
        
        :param      email | <str>
        """
        self._email = str(email)

    def setEnabled(self, state):
        """
        Sets whether or not this plugin is enabled.
        
        :param      state | <bool>
        """
        self._enabled = state

    def setError(self, error):
        """
        Sets the error that this plugin ran into last.
        
        :param      error | <Exception> || None
        """
        self._error = error

    def setFilepath(self, filepath):
        """
        Sets the filepath location for this plugin.
        
        :param      filepath | <str>
        """
        self._filepath = filepath

    def setIcon(self, filepath):
        """
        Sets the icon filepath for this plugin to the inputed path.  This can
        be either an absolute path, or a relative path from the location this
        plugin was loaded from.
        
        :param      filepath | <str>
        """
        self._icon = str(filepath)

    def setUrl(self, url):
        """
        Sets the url information for this plugin.
        
        :param      url | <str>
        """
        self._url = str(url)

    def url(self):
        """
        Returns the URL information for this plugin.
        
        :return     <str>
        """
        return self._url

    def version(self):
        """
        Returns the version for this plugin.
        
        :return     <float>
        """
        return self._version

    @classmethod
    def addPluginPath(cls, pluginpath):
        """
        Adds the plugin path for this class to the given path.  The inputed
        pluginpath value can either be a list of strings, or a string
        containing paths separated by the OS specific path separator (':' on
        Mac & Linux, ';' on Windows)
        
        :param      pluginpath | [<str>, ..] || <str>
        """
        prop_key = '_%s__pluginpath' % cls.__name__
        if not hasattr(cls, prop_key):
            setattr(cls, prop_key, [])

        curr_path = getattr(cls, prop_key)

        if isinstance(pluginpath, basestring):
            pluginpath = pluginpath.split(os.path.pathsep)

        for path in pluginpath:
            if not path:
                continue

            path = os.path.expanduser(os.path.expandvars(path))
            paths = path.split(os.path.pathsep)

            if len(paths) > 1:
                cls.addPluginPath(paths)
            else:
                curr_path.append(path)

    @classmethod
    def pluginRegisterType(cls):
        """
        Returns the register type for this plugin class.
        
        :return     <Plugin.RegisterType>
        """
        default = Plugin.Type.Module
        default |= Plugin.Type.Package
        default |= Plugin.Type.RegistryFile

        return getattr(cls, '_%s__pluginRegisterType', default)

    @classmethod
    def loadPlugins(cls):
        """
        Initializes the plugins by loading modules from the inputed paths.
        """
        plugs = getattr(cls, '_%s__plugins' % cls.__name__, None)
        if plugs is not None:
            return

        plugs = {}
        setattr(cls, '_%s__plugins' % cls.__name__, plugs)
        typ = cls.pluginRegisterType()

        for path in cls.pluginPath():
            base_package = projex.packageFromPath(path)
            base_path = os.path.normpath(projex.packageRootPath(path))

            # make sure it is at the front of the path
            if base_path in sys.path:
                sys.path.remove(base_path)

            sys.path.insert(0, base_path)
            processed = ['__init__']

            # load support for registries
            if typ & Plugin.Type.RegistryFile:
                files = glob.glob(os.path.join(path, '*/register.xml'))
                for file in files:
                    name = os.path.normpath(file).split(os.path.sep)[-2]
                    processed.append(name)

                    try:
                        proxy = PluginProxy.fromFile(cls, file)
                        cls.register(proxy)

                    except Exception, e:
                        name = projex.text.pretty(name)
                        err = Plugin(name)
                        err.setError(e)
                        err.setFilepath(file)

                        cls.register(err)

                        # log the error
                        msg = "%s.plugin('%s') failed to load from %s."
                        logger.warning(msg % (cls.__name__, name, file))
                        logger.error(e)

            # load support for packages
            if typ & Plugin.Type.Package:
                files = glob.glob(os.path.join(path, '*/__init__.py'))
                for file in files:
                    name = os.path.normpath(file).split(os.path.sep)[-2]
                    if (name in processed):
                        continue

                    processed.append(name)
                    package = '.'.join([base_package, name]).strip('.')
                    if (not package):
                        continue

                    try:
                        __import__(package)

                    except Exception, e:
                        name = projex.text.pretty(name)
                        err = Plugin(name)
                        err.setError(e)
                        err.setFilepath(file)

                        cls.register(err)

                        # log the error
                        msg = "%s.plugin('%s') failed to load from %s."
                        logger.warning(msg % (cls.__name__, name, file))
                        logger.error(e)

            # load support for modules
            if typ & Plugin.Type.Module:
                files = glob.glob(os.path.join(path, '*.py'))
                for file in files:
                    name = os.path.basename(file).split('.')[0]
                    if (name in processed):
                        continue

                    processed.append(name)
                    package = '.'.join([base_package, name]).strip('.')
                    if (not package):
                        continue

                    try:
                        __import__(package)

                    except Exception, e:
                        name = projex.text.pretty(name)
                        err = Plugin(name)
                        err.setError(e)
                        err.setFilepath(file)

                        cls.register(err)

                        # log the error
                        msg = "%s.plugin('%s') failed to load from %s."
                        logger.warning(msg % (cls.__name__, name, file))
                        logger.error(e)
Exemple #22
0
class XOrbBrowserWidget(QWidget):
    """ """
    __designer_group__ = 'ProjexUI - ORB'

    currentRecordChanged = Signal()
    queryChanged = Signal(PyObject)  # orb.Query
    recordDoubleClicked = Signal(PyObject)  # orb.Table

    GroupByAdvancedKey = '__ADVANCED__'
    Mode = enum('Detail', 'Card', 'Thumbnail')

    def __init__(self, parent=None):
        super(XOrbBrowserWidget, self).__init__(parent)

        # load the user interface
        projexui.loadUi(__file__, self)

        # define custom properties
        self._hint = ''
        self._query = Q()
        self._advancedGrouping = []
        self._records = RecordSet()
        self._groupBy = XOrbBrowserWidget.GroupByAdvancedKey
        self._factory = XOrbBrowserFactory()
        self._queryWidget = XOrbQueryWidget(self, self._factory)
        self._thumbnailSize = QSize(128, 128)

        # set default properties
        self.uiSearchTXT.addButton(self.uiQueryBTN)
        self.uiQueryBTN.setCentralWidget(self._queryWidget)
        self.uiThumbLIST.installEventFilter(self)

        self.uiQueryACT.setShortcutContext(Qt.WidgetWithChildrenShortcut)
        self.uiQueryBTN.setDefaultAction(self.uiQueryACT)

        self.uiViewModeWGT.addAction(self.uiDetailsACT)
        self.uiViewModeWGT.addAction(self.uiCardACT)
        self.uiViewModeWGT.addAction(self.uiThumbnailACT)

        # create connections
        self.uiGroupOptionsBTN.clicked.connect(self.showGroupMenu)
        self.uiSearchTXT.returnPressed.connect(self.refresh)
        self.queryChanged.connect(self.refresh)
        self.uiGroupBTN.toggled.connect(self.refreshResults)

        self.uiDetailsACT.triggered.connect(self.setDetailMode)
        self.uiCardACT.triggered.connect(self.setCardMode)
        self.uiThumbnailACT.triggered.connect(self.setThumbnailMode)

        self.uiQueryBTN.popupAboutToShow.connect(self.prepareQuery)
        self.uiQueryBTN.popupAccepted.connect(self.acceptQuery)
        self.uiQueryBTN.popupReset.connect(self.resetQuery)

        self.uiRefreshBTN.clicked.connect(self.refresh)

        self.uiRecordsTREE.itemDoubleClicked.connect(self.handleDetailDblClick)
        self.uiRecordsTREE.currentItemChanged.connect(
            self.emitCurrentRecordChanged)

        self.uiThumbLIST.itemDoubleClicked.connect(self.handleThumbDblClick)
        self.uiThumbLIST.currentItemChanged.connect(
            self.emitCurrentRecordChanged)

        self.uiCardTREE.itemDoubleClicked.connect(self.handleCardDblClick)
        self.uiCardTREE.currentItemChanged.connect(
            self.emitCurrentRecordChanged)

    def _loadCardGroup(self, groupName, records, parent=None):
        if (not groupName):
            groupName = 'None'

        cards = self.cardWidget()
        factory = self.factory()

        # create the group item
        group_item = QTreeWidgetItem(parent, [groupName])
        font = group_item.font(0)
        font.setBold(True)
        font.setPointSize(font.pointSize() + 2)
        group_item.setFont(0, font)
        group_item.setFlags(Qt.ItemIsEnabled)

        # load sub-groups
        if (type(records) == dict):
            for subgroup, records in sorted(records.items()):
                self._loadCardGroup(subgroup, records, group_item)
        else:
            for record in records:
                widget = factory.createCard(cards, record)
                if (not widget):
                    continue

                widget.adjustSize()

                # create the card item
                item = QTreeWidgetItem(group_item)
                item.setSizeHint(0, QSize(0, widget.height()))
                cards.setItemWidget(item, 0, widget)

        group_item.setExpanded(True)

    def _loadThumbnailGroup(self, groupName, records):
        if (not groupName):
            groupName = 'None'

        widget = self.thumbnailWidget()
        factory = self.factory()

        # create the group item
        GroupListWidgetItem(groupName, widget)

        # load sub-groups
        if (type(records) == dict):
            for subgroup, records in sorted(records.items()):
                self._loadThumbnailGroup(subgroup, records)
        else:
            # create the record items
            for record in records:
                thumbnail = factory.thumbnail(record)
                text = factory.thumbnailText(record)
                RecordListWidgetItem(thumbnail, text, record, widget)

    def acceptQuery(self):
        """
        Accepts the changes made from the query widget to the browser.
        """
        self.setQuery(self._queryWidget.query())

    def advancedGrouping(self):
        """
        Returns the advanced grouping options for this widget.
        
        :return     [<str> group level, ..]
        """
        return ['[lastName::slice(0, 1)]']
        return self._advancedGrouping

    def cardWidget(self):
        """
        Returns the card widget for this browser.
        
        :return     <QTreeWidget>
        """
        return self.uiCardTREE

    def controlsWidget(self):
        """
        Returns the controls widget for this browser.  This is the widget that
        contains the various control mechanisms.
        
        :return     <QWidget>
        """
        return self._controlsWidget

    def currentGrouping(self):
        """
        Returns the current grouping for this widget.
        
        :return     [<str> group level, ..]
        """
        groupBy = self.groupBy()
        if (groupBy == XOrbBrowserWidget.GroupByAdvancedKey):
            return self.advancedGrouping()
        else:
            table = self.tableType()
            if (not table):
                return []

            for column in table.schema().columns():
                if (column.displayName() == groupBy):
                    return [column.name()]

            return []

    def currentRecord(self):
        """
        Returns the current record from this browser.
        
        :return     <orb.Table> || None
        """
        if (self.currentMode() == XOrbBrowserWidget.Mode.Detail):
            return self.detailWidget().currentRecord()

        elif (self.currentMode() == XOrbBrowserWidget.Mode.Thumbnail):
            item = self.thumbnailWidget().currentItem()
            if (isinstance(item, RecordListWidgetItem)):
                return item.record()
            return None

        else:
            item = self.uiCardTREE.currentItem()
            widget = self.uiCardTREE.itemWidget(item, 0)
            if (isinstance(widget, XAbstractCardWidget)):
                return widget.record()

            return None

    def currentMode(self):
        """
        Returns the current mode for this widget.
        
        :return     <XOrbBrowserWidget.Mode>
        """
        if (self.uiCardACT.isChecked()):
            return XOrbBrowserWidget.Mode.Card
        elif (self.uiDetailsACT.isChecked()):
            return XOrbBrowserWidget.Mode.Detail
        else:
            return XOrbBrowserWidget.Mode.Thumbnail

    def detailWidget(self):
        """
        Returns the tree widget used by this browser.
        
        :return     <XOrbTreeWidget>
        """
        return self.uiRecordsTREE

    def emitCurrentRecordChanged(self):
        """
        Emits the current record changed signal.
        """
        if (not self.signalsBlocked()):
            self.currentRecordChanged.emit()

    def emitRecordDoubleClicked(self, record):
        """
        Emits the record double clicked signal.
        
        :param      record | <orb.Table>
        """
        if (not self.signalsBlocked()):
            self.recordDoubleClicked.emit(record)

    def enabledModes(self):
        """
        Returns the binary value of the enabled modes.
        
        :return     <XOrbBrowserWidget.Mode>
        """
        output = 0
        for i, action in enumerate(
            (self.uiDetailsACT, self.uiCardACT, self.uiThumbnailACT)):
            if (action.isEnabled()):
                output |= int(math.pow(2, i))
        return output

    def eventFilter(self, object, event):
        """
        Processes resize events on the thumbnail widget to update the group
        items to force a proper sizing.
        
        :param      object | <QObject>
                    event  | <QEvent>
        
        :return     <bool> | consumed
        """
        if ( event.type() == event.Resize and \
             self.currentMode() == XOrbBrowserWidget.Mode.Thumbnail and \
             self.isGroupingActive() ):
            size = QSize(event.size().width() - 20, 22)
            for row in range(object.count()):
                item = object.item(row)
                if (isinstance(item, GroupListWidgetItem)):
                    item.setSizeHint(size)
        return False

    def factory(self):
        """
        Returns the factory assigned to this browser for generating card and
        thumbnail information for records.
        
        :return     <XOrbBrowserFactory>
        """
        return self._factory

    def groupBy(self):
        """
        Returns the group by key for this widget.  If GroupByAdvancedKey
        is returned, then the advanced grouping options will be used.  
        Otherwise, a column will be used for grouping.
        
        :return     <str>
        """
        return self._groupBy

    def handleCardDblClick(self, item):
        """
        Handles when a card item is double clicked on.
        
        :param      item | <QTreeWidgetItem>
        """
        widget = self.uiCardTREE.itemWidget(item, 0)
        if (isinstance(widget, XAbstractCardWidget)):
            self.emitRecordDoubleClicked(widget.record())

    def handleDetailDblClick(self, item):
        """
        Handles when a detail item is double clicked on.
        
        :param      item | <QTreeWidgetItem>
        """
        if (isinstance(item, XOrbRecordItem)):
            self.emitRecordDoubleClicked(item.record())

    def handleThumbDblClick(self, item):
        """
        Handles when a thumbnail item is double clicked on.
        
        :param      item | <QListWidgetItem>
        """
        if (isinstance(item, RecordListWidgetItem)):
            self.emitRecordDoubleClicked(item.record())

    def hint(self):
        """
        Returns the hint for this widget.
        
        :return     <str>
        """
        return self._hint

    def isGroupingActive(self):
        """
        Returns if the grouping is currently on or not.
        
        :return     <bool>
        """
        return self.uiGroupBTN.isChecked()

    def isModeEnabled(self, mode):
        """
        Returns whether or not the inputed mode is enabled.
        
        :param      mode | <XOrbBrowserWidget.Mode>
        
        :return     <bool>
        """
        return (self.enabledModes() & mode) != 0

    def modeWidget(self):
        """
        Returns the mode widget for this instance.
        
        :return     <projexui.widgets.xactiongroupwidget.XActionGroupWidget>
        """
        return self.uiViewModeWGT

    def prepareQuery(self):
        """
        Prepares the popup widget with the query data.
        """
        self._queryWidget.setQuery(self.query())

    def query(self):
        """
        Returns the fixed query that is assigned via programmatic means.
        
        :return     <orb.Query> || None
        """
        return self._query

    def queryWidget(self):
        """
        Returns the query building widget.
        
        :return     <XOrbQueryWidget>
        """
        return self._queryWidget

    def records(self):
        """
        Returns the record set for the current settings of this browser.
        
        :return     <orb.RecordSet>
        """
        if (self.isGroupingActive()):
            self._records.setGroupBy(self.currentGrouping())
        else:
            self._records.setGroupBy(None)
        return self._records

    def refresh(self):
        """
        Refreshes the interface fully.
        """
        self.refreshRecords()
        self.refreshResults()

    def refreshRecords(self):
        """
        Refreshes the records being loaded by this browser.
        """
        table_type = self.tableType()
        if (not table_type):
            self._records = RecordSet()
            return False

        search = str(self.uiSearchTXT.text())

        query = self.query().copy()
        terms, search_query = Q.fromSearch(search)

        if (search_query):
            query &= search_query

        self._records = table_type.select(where=query).search(terms)
        return True

    def refreshResults(self):
        """
        Joins together the queries from the fixed system, the search, and the
        query builder to generate a query for the browser to display.
        """
        if (self.currentMode() == XOrbBrowserWidget.Mode.Detail):
            self.refreshDetails()
        elif (self.currentMode() == XOrbBrowserWidget.Mode.Card):
            self.refreshCards()
        else:
            self.refreshThumbnails()

    def refreshCards(self):
        """
        Refreshes the results for the cards view of the browser.
        """
        cards = self.cardWidget()
        factory = self.factory()

        self.setUpdatesEnabled(False)
        self.blockSignals(True)

        cards.setUpdatesEnabled(False)
        cards.blockSignals(True)

        cards.clear()
        QApplication.instance().processEvents()

        if (self.isGroupingActive()):
            grouping = self.records().grouped()
            for groupName, records in sorted(grouping.items()):
                self._loadCardGroup(groupName, records, cards)

        else:
            for record in self.records():
                widget = factory.createCard(cards, record)
                if (not widget):
                    continue

                widget.adjustSize()

                # create the card item
                item = QTreeWidgetItem(cards)
                item.setSizeHint(0, QSize(0, widget.height()))
                cards.setItemWidget(item, 0, widget)

        cards.setUpdatesEnabled(True)
        cards.blockSignals(False)

        self.setUpdatesEnabled(True)
        self.blockSignals(False)

    def refreshDetails(self):
        """
        Refreshes the results for the details view of the browser.
        """
        # start off by filtering based on the group selection
        tree = self.uiRecordsTREE
        tree.blockSignals(True)
        tree.setRecordSet(self.records())
        tree.blockSignals(False)

    def refreshThumbnails(self):
        """
        Refreshes the thumbnails view of the browser.
        """
        # clear existing items
        widget = self.thumbnailWidget()
        widget.setUpdatesEnabled(False)
        widget.blockSignals(True)

        widget.clear()
        widget.setIconSize(self.thumbnailSize())

        factory = self.factory()

        # load grouped thumbnails (only allow 1 level of grouping)
        if (self.isGroupingActive()):
            grouping = self.records().grouped()
            for groupName, records in sorted(grouping.items()):
                self._loadThumbnailGroup(groupName, records)

        # load ungrouped thumbnails
        else:
            # load the records into the thumbnail
            for record in self.records():
                thumbnail = factory.thumbnail(record)
                text = factory.thumbnailText(record)
                RecordListWidgetItem(thumbnail, text, record, widget)

        widget.setUpdatesEnabled(True)
        widget.blockSignals(False)

    def resetQuery(self):
        """
        Resets the popup query widget's query information
        """
        self._queryWidget.clear()

    def setCardMode(self):
        """
        Sets the mode for this widget to the Card mode.
        """
        self.setCurrentMode(XOrbBrowserWidget.Mode.Card)

    def setCurrentMode(self, mode):
        """
        Sets the current mode for this widget to the inputed mode.  This will
        check against the valid modes for this browser and return success.
        
        :param      mode | <XOrbBrowserWidget.Mode>
        
        :return     <bool> | success
        """
        if (not self.isModeEnabled(mode)):
            return False

        if (mode == XOrbBrowserWidget.Mode.Detail):
            self.uiModeSTACK.setCurrentIndex(0)
            self.uiDetailsACT.setChecked(True)
        elif (mode == XOrbBrowserWidget.Mode.Card):
            self.uiModeSTACK.setCurrentIndex(1)
            self.uiCardACT.setChecked(True)
        else:
            self.uiModeSTACK.setCurrentIndex(2)
            self.uiThumbnailACT.setChecked(True)

        self.refreshResults()

        return True

    def setCurrentRecord(self, record):
        """
        Sets the current record for this browser to the inputed record.
        
        :param      record | <orb.Table> || None
        """
        mode = self.currentMode()
        if (mode == XOrbBrowserWidget.Mode.Detail):
            self.detailWidget().setCurrentRecord(record)

        elif (mode == XOrbBrowserWidget.Mode.Thumbnail):
            thumbs = self.thumbnailWidget()
            for row in range(thumbs.count()):
                item = thumbs.item(row)
                if ( isinstance(item, RecordListWidgetItem) and \
                     item.record() == item ):
                    thumbs.setCurrentItem(item)
                    break

    def setDetailMode(self):
        """
        Sets the mode for this widget to the Detail mode.
        """
        self.setCurrentMode(XOrbBrowserWidget.Mode.Detail)

    def setFactory(self, factory):
        """
        Sets the factory assigned to this browser for generating card and
        thumbnail information for records.
        
        :param      factory | <XOrbBrowserFactory>
        """
        self._factory = factory
        self._queryWidget.setFactory(factory)

    def setGroupByAdvanced(self):
        """
        Sets the groupBy key for this widget to GroupByAdvancedKey signaling 
        that the advanced user grouping should be used.
        """
        self.setGroupBy(XOrbBrowserWidget.GroupByAdvancedKey)

    def setGroupBy(self, groupBy):
        """
        Sets the group by key for this widget.  This should correspond to a 
        display name for the columns, or the GroupByAdvancedKey keyword.  It is
        recommended to use setGroupByAdvanced for setting advanced groupings.
        
        :param      groupBy | <str>
        """
        self._groupBy = groupBy

    def setGroupingActive(self, state):
        """
        Sets whether or not the grouping should be enabled for the widget.
        
        :param      state | <bool>
        """
        self.uiGroupBTN.setChecked(state)

    def setHint(self, hint):
        """
        Sets the hint for this widget.
        
        :param      hint | <str>
        """
        self._hint = hint
        self.detailWidget().setHint(hint)

    def setModeEnabled(self, mode, state):
        """
        Sets whether or not the mode should be enabled.
        
        :param      mode  | <XOrbBrowserWidget.Mode>
                    state | <bool>
        """
        if (mode == XOrbBrowserWidget.Mode.Detail):
            self.uiDetailsACT.setEnabled(state)
        elif (mode == XOrbBrowserWidget.Mode.Card):
            self.uiCardACT.setEnabled(state)
        else:
            self.uiThumbnailACT.setEnabled(state)

    def setQuery(self, query):
        """
        Sets the fixed lookup query for this widget to the inputed query.
        
        :param      query | <orb.Query>
        """
        self._query = query
        if (not self.signalsBlocked()):
            self.queryChanged.emit(query)

    def setTableType(self, tableType):
        """
        Sets the table type for this widget to the inputed type.
        
        :param      tableType | <orb.Table>
        """
        self.detailWidget().setTableType(tableType)
        self.queryWidget().setTableType(tableType)

    def setThumbnailMode(self):
        """
        Sets the mode for this widget to the thumbnail mode.
        """
        self.setCurrentMode(XOrbBrowserWidget.Mode.Thumbnail)

    def setThumbnailSize(self, size):
        """
        Sets the size that will be used for the thumbnails in this widget.
        
        :param      size | <QSize>
        """
        self._thumbnailSize = QSize(size)

    def showGroupMenu(self):
        """
        Displays the group menu to the user for modification.
        """
        group_active = self.isGroupingActive()
        group_by = self.groupBy()

        menu = XMenu(self)
        menu.setTitle('Grouping Options')
        menu.setShowTitle(True)
        menu.addAction('Edit Advanced Grouping')

        menu.addSeparator()

        action = menu.addAction('No Grouping')
        action.setCheckable(True)
        action.setChecked(not group_active)

        action = menu.addAction('Advanced')
        action.setCheckable(True)
        action.setChecked(group_by == self.GroupByAdvancedKey and group_active)
        if (group_by == self.GroupByAdvancedKey):
            font = action.font()
            font.setBold(True)
            action.setFont(font)

        menu.addSeparator()

        # add dynamic options from the table schema
        tableType = self.tableType()
        if (tableType):
            columns = tableType.schema().columns()
            columns.sort(key=lambda x: x.displayName())
            for column in columns:
                action = menu.addAction(column.displayName())
                action.setCheckable(True)
                action.setChecked(group_by == column.displayName()
                                  and group_active)

                if (column.displayName() == group_by):
                    font = action.font()
                    font.setBold(True)
                    action.setFont(font)

        point = QPoint(0, self.uiGroupOptionsBTN.height())
        action = menu.exec_(self.uiGroupOptionsBTN.mapToGlobal(point))

        if (not action):
            return
        elif (action.text() == 'Edit Advanced Grouping'):
            print 'edit advanced grouping options'
        elif (action.text() == 'No Grouping'):
            self.setGroupingActive(False)

        elif (action.text() == 'Advanced'):
            self.uiGroupBTN.blockSignals(True)
            self.setGroupBy(self.GroupByAdvancedKey)
            self.setGroupingActive(True)
            self.uiGroupBTN.blockSignals(False)

            self.refreshResults()

        else:
            self.uiGroupBTN.blockSignals(True)
            self.setGroupBy(str(action.text()))
            self.setGroupingActive(True)
            self.uiGroupBTN.blockSignals(False)

            self.refreshResults()

    def stackWidget(self):
        """
        Returns the stack widget linked with this browser.  This contains the
        different views linked with the view mode.
        
        :return     <QStackWidget>
        """
        return self.uiModeSTACK

    def tableType(self):
        """
        Returns the table type for this widget.
        
        :return     <orb.Table>
        """
        return self.detailWidget().tableType()

    def thumbnailSize(self):
        """
        Returns the size that will be used for displaying thumbnails for this
        widget.
        
        :return     <QSize>
        """
        return self._thumbnailSize

    def thumbnailWidget(self):
        """
        Returns the thumbnail widget for this widget.
        
        :return     <QListWidget>
        """
        return self.uiThumbLIST

    x_hint = Property(str, hint, setHint)
Exemple #23
0
class DoxSource(object):
    Type = enum('Wiki', 'Module', 'Group')

    def __init__(self, dox, typ, name, filepath):
        self._dox = dox
        self._type = typ
        self._name = name
        self._filepath = filepath
        self._entries = []

    def addEntry(self, entry):
        """
        Adds a new entry to this entry.  Entries can be grouped
        together to create custom hierarchies.
        
        :param      entry | <DoxEntry>
        """
        entry._order = len(self._entries)
        self._entries.append(entry)

    def entries(self):
        """
        Returns the list of entries associated with this dox file.
        
        :return     [<DoxEntry>, ..]
        """
        return self._entries

    def export(self, outpath, breadcrumbs=None):
        """
        Exports this source to the given output path location.
        
        :param      outpath | <str>
        """
        if breadcrumbs is None:
            breadcrumbs = ['Home']

        # save the toc
        xtoc = ElementTree.Element('toc')
        xtoc.set('name', self.name())
        for entry in sorted(self.entries()):
            entry.toXml(xtoc)

        projex.text.xmlindent(xtoc)
        toc_file = open(os.path.join(outpath, 'toc.xml'), 'w')
        toc_file.write(ElementTree.tostring(xtoc))
        toc_file.close()

        # generate an arbitrary group
        if self.type() == DoxSource.Type.Group:
            key = self.name().replace(' ', '_').lower()
            target = os.path.join(outpath, key)
            if not os.path.exists(target):
                os.mkdir(target)

            breadcrumbs.append(self.name())
            for source in self.sources():
                source.export(target, breadcrumbs)

        # generate user docs
        elif self.type() == DoxSource.Type.Wiki:
            for entry in sorted(self._entries):
                self.exportEntry(self.filepath(), outpath, entry, breadcrumbs)

        # generate module docs
        elif self.type() == DoxSource.Type.Module:
            pass

    def exportEntry(self, src, target, entry, breadcrumbs):
        """
        Exports the entry to HTML.
        
        :param      srcpath     | <str>
                    targetpath  | <str>
                    entry       | <DoxEntry>
        """
        src_path = os.path.join(src, entry.basename())
        target_path = os.path.join(target, entry.basename())

        # export a folder
        if os.path.isdir(src_path):
            os.makedirs(target_path)
            crumbs = breadcrumbs + [entry.title()]
            for child in entry.children():
                self.exportEntry(src_path, target_path, child, crumbs)

        # export a wiki file
        elif entry.basename().endswith('.wiki'):
            count = len(breadcrumbs) - 1

            base_url = '.' + '/..' * count
            static_url = base_url + '/_static'

            # extract wiki information
            wiki_file = open(src_path, 'r')
            wiki_contents = wiki_file.read()
            wiki_file.close()

            # generate the contents

            # generate the breadcrumbs for this file
            incl = entry.basename() != 'index.wiki'
            html = self._dox.render(entry.title(),
                                    wiki_contents,
                                    breadcrumbs,
                                    filepath=src_path,
                                    baseUrl=base_url,
                                    staticUrl=static_url,
                                    includeTitleInCrumbs=incl)

            # generate the new html file
            target_path = target_path.replace('.wiki', '.html')

            html_file = open(target_path, 'w')
            html_file.write(html)
            html_file.close()

    def filepath(self):
        """
        Returns the filepath for the source of this source.
        
        :return     <str>
        """
        path = os.path.expandvars(self._filepath)
        if os.path.isabs(path):
            return path

        elif self._dox.filename():
            basepath = os.path.dirname(self._dox.filename())
            basepath = os.path.join(basepath, path)

        else:
            basepath = path

        return os.path.abspath(basepath)

    def name(self):
        """
        Returns the name for this source.
        
        :return     <str>
        """
        return self._name

    def sources(self):
        """
        Returns a list of the sub-sources this instance holds.  Sources
        can be nested together in groups to create custom hierarchies.
        
        :return     [<DoxSource>, ..]
        """
        return self._sources

    def sync(self):
        """
        Syncs the contents of this entry based on the given filepath.
        
        :param      filepath | <str>
        """
        if self.type() != DoxSource.Type.Wiki:
            return

        filepath = self.filepath()
        entry_names = [entry.basename() for entry in self.entries()]
        filenames = list(os.listdir(str(filepath)))

        rem_paths = set(entry_names).difference(filenames)
        new_paths = set(filenames).difference(entry_names)

        # remove the deleted filepaths
        if rem_paths:
            kids = [c for c in self.entries() if c.basename() not in rem_paths]
            for i, kid in enumerate(kids):
                kid.setOrder(i)
        else:
            kids = self.entries()[:]

        # add new paths
        for path in sorted(new_paths):
            entrypath = os.path.join(filepath, path)
            if os.path.isdir(entrypath):
                title = projex.text.pretty(path)
            else:
                title = projex.text.pretty(os.path.splitext(path)[0])

            order = len(kids)
            entry = DoxEntry(title=title, basename=path, order=order)
            kids.append(entry)

        self._entries = kids

        # sync sub-folders
        for entry in self.entries():
            entrypath = os.path.join(filepath, entry.basename())
            if os.path.isdir(entrypath):
                entry.sync(entrypath)

    def toXml(self, xml):
        """
        Converts this source information to xml data.
        
        :param      xml | <ElementTree.Element>
        
        :return     <ElementTree.SubElement>
        """
        xsource = ElementTree.SubElement(xml, 'source')
        xsource.set('type', DoxSource.Type[self.type()])
        xsource.set('name', self.name())
        xsource.set('path', self._filepath)

        for source in sorted(self._entries):
            source.toXml(xsource)

        return xsource

    def type(self):
        """
        Returns the type of source this source is.
        
        :return     <DoxSource.Type>
        """
        return self._type

    @staticmethod
    def fromXml(dox, xml):
        """
        Generates a new source based on the inputed xml node.
        
        :param      xml | <ElementTree.Element>
        
        :return     <DoxSource>
        """
        typ = DoxSource.Type[xml.get('type')]
        rsc = DoxSource(dox, typ, xml.get('name', ''), xml.get('path', ''))

        # load entries
        for xentry in xml:
            rsc.addEntry(DoxEntry.fromXml(xentry))

        return rsc
Exemple #24
0
__maintainer__ = 'Projex Software, LLC'
__email__ = '*****@*****.**'

import datetime
import projex.text

from projex.enum import enum

# define global enumerations
RepeatFlags = enum(  # Weekly Flags
    'EveryMonday',
    'EveryTuesday',
    'EveryWednesday',
    'EveryThursday',
    'EveryFriday',
    'EverySaturday',
    'EverySunday',

    # Repeating Flags
    'DayOfTheMonth',
    'DayOfTheWeek')

RepeatMode = enum(
    Weekly=2,  # removed the 'Daily' key, was 1
    Monthly=4,
    Yearly=8)

Names = enum(  # default
    'Sometime',

    # Preset Names
Exemple #25
0
class Collector(object):
    Flags = enum('Unique', 'Private', 'ReadOnly', 'Virtual', 'Static',
                 'AutoExpand')

    def __json__(self):
        try:
            model = self.model()
        except orb.errors.ModelNotFound:
            model_name = None
        else:
            model_name = model.schema().name() if model else None

        output = {
            'name': self.__name,
            'model': model_name,
            'flags': {k: True
                      for k in self.Flags.toSet(self.__flags)}
        }
        return output

    def __init__(self,
                 name='',
                 flags=0,
                 getter=None,
                 setter=None,
                 model=None,
                 queryFilter=None):
        self.__name = self.__name__ = name
        self.__model = model
        self.__schema = None
        self.__preload = None
        self.__getter = getter
        self.__setter = setter
        self.__query_filter = queryFilter
        self.__flags = self.Flags.fromSet(flags) if isinstance(flags,
                                                               set) else flags

    def __call__(self, record, useMethod=True, **context):
        if self.__getter and useMethod:
            return self.__getter(record, **context)
        else:
            if not record.isRecord():
                return orb.Collection()
            else:
                collection = self.collect(record, **context)

                # preload the results
                if isinstance(collection, orb.Collection):
                    cache = record.preload(projex.text.underscore(self.name()))
                    collection.preload(cache, **context)

                    if self.testFlag(self.Flags.Unique):
                        return collection.first()
                    else:
                        return collection
                else:
                    return collection

    def copy(self):
        out = type(self)(name=self.__name, flags=self.__flags)
        out.setSchema(self.schema())
        return out

    def collect(self, record, **context):
        if self.__getter:
            return self.__getter(record, **context)
        raise NotImplementedError

    def collectExpand(self, query, parts, **context):
        raise NotImplementedError

    def queryFilter(self, function=None):
        """
        Defines a decorator that can be used to filter
        queries.  It will assume the function being associated
        with the decorator will take a query as an input and
        return a modified query to use.

        :usage

            class MyModel(orb.Model):
                objects = orb.ReverseLookup('Object')

                @classmethod
                @objects.queryFilter()
                def objectsFilter(cls, query, **context):
                    return orb.Query()

        :param function: <callable>

        :return: <wrapper>
        """
        if function is not None:
            self.__query_filter = function
            return function

        def wrapper(func):
            self.__query_filter = func
            return func

        return wrapper

    def queryFilterMethod(self):
        """
        Returns the actual query filter method, if any,
        that is associated with this collector.

        :return: <callable>
        """
        return self.__query_filter

    def getter(self, function=None):
        if function is not None:
            self.__getter = function
            return function

        def wrapper(func):
            self.__getter = func
            return func

        return wrapper

    def gettermethod(self):
        return self.__getter

    def flags(self):
        return self.__flags

    def model(self):
        if isinstance(self.__model, (str, unicode)):
            schema = orb.system.schema(self.__model)
            if schema is not None:
                return schema.model()
            else:
                raise orb.errors.ModelNotFound(self.__model)
        else:
            return self.__model

    def name(self):
        return self.__name

    def schema(self):
        return self.__schema

    def setter(self, function=None):
        if function is not None:
            self.__setter = function
            return function

        def wrapper(func):
            self.__setter = func
            return func

        return wrapper

    def settermethod(self):
        return self.__setter

    def setName(self, name):
        self.__name = self.__name__ = name

    def setSchema(self, schema):
        self.__schema = schema

    def setFlags(self, flags):
        self.__flags = flags

    def testFlag(self, flag):
        return (self.__flags & flag) > 0
Exemple #26
0
class XSplitterHandle(QSplitterHandle):
    CollapseDirection = enum('After', 'Before')

    def __init__(self, orientation, parent):
        super(XSplitterHandle, self).__init__(orientation, parent)

        # create a layout for the different buttons
        self._collapsed = False
        self._storedSizes = None

        self._collapseBefore = QToolButton(self)
        self._resizeGrip = QLabel(self)
        self._collapseAfter = QToolButton(self)

        self._collapseBefore.setAutoRaise(True)
        self._collapseAfter.setAutoRaise(True)

        self._collapseBefore.setCursor(Qt.ArrowCursor)
        self._collapseAfter.setCursor(Qt.ArrowCursor)

        # define the layout
        layout = QBoxLayout(QBoxLayout.LeftToRight, self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addStretch(1)
        layout.addWidget(self._collapseBefore)
        layout.addWidget(self._resizeGrip)
        layout.addWidget(self._collapseAfter)
        layout.addStretch(1)
        self.setLayout(layout)

        # set the orientation to start with
        self.setOrientation(orientation)

        # create connections
        self._collapseAfter.clicked.connect(self.toggleCollapseAfter)
        self._collapseBefore.clicked.connect(self.toggleCollapseBefore)

    def collapse(self, direction):
        """
        Collapses this splitter handle before or after other widgets based on \
        the inputed CollapseDirection.
        
        :param      direction | <XSplitterHandle.CollapseDirection>
        
        :return     <bool> | success
        """
        if (self.isCollapsed()):
            return False

        splitter = self.parent()
        if (not splitter):
            return False

        sizes = splitter.sizes()
        handles = [splitter.handle(i) for i in range(len(sizes))]
        index = handles.index(self)

        self.markCollapsed(direction, sizes)

        # determine the sizes to use based on the direction
        if (direction == XSplitterHandle.CollapseDirection.Before):
            sizes = [0 for i in range(i)] + sizes[i + 1:]
        else:
            sizes = sizes[:i] + [0 for i in range(i, len(sizes))]

        splitter.setSizes(sizes)
        return True

    def collapseAfter(self, handle):
        """
        Collapses the splitter after the inputed handle.
        
        :param      handle | <XSplitterHandle>
        """
        self.setUpdatesEnabled(False)

        # collapse all items after the current handle
        if (handle.isCollapsed()):
            self.setSizes(handle.restoreSizes())

        found = False
        sizes = self.sizes()

        handle.storeSizes(sizes)

        for c in range(self.count()):
            if (self.handle(c) == handle):
                found = True

            if (found):
                sizes[c] = 0

        self.setSizes(sizes)
        self.update()

        self.setUpdatesEnabled(True)

    def collapseBefore(self, handle):
        """
        Collapses the splitter before the inputed handle.
        
        :param      handle | <XSplitterHandle>
        """
        self.setUpdatesEnabled(False)

        # collapse all items after the current handle
        if (handle.isCollapsed()):
            self.setSizes(handle.restoreSizes())

        # collapse all items before the current handle
        found = False
        sizes = self.sizes()

        handle.storeSizes(sizes)

        for c in range(self.count()):
            if (self.handle(c) == handle):
                break

            sizes[c] = 0

        self.setSizes(sizes)
        self.setUpdatesEnabled(True)

    def isCollapsed(self):
        """
        Returns whether or not this widget is collapsed.
        
        :return     <bool>
        """
        return self._collapsed

    def markCollapsed(self, direction, sizes):
        """
        Updates the interface to reflect that the splitter is collapsed.
        
        :param      direction | <XSplitterHandle.CollapseDirection>
                    sizes     | [<int>, ..]
        """
        self._collapsed = True
        self._storedSizes = sizes[:]

        if (direction == XSplitterHandle.CollapseDirection.Before):
            if (self.orientation() == Qt.Horizontal):
                self._collapseAfter.setArrowType(Qt.RightArrow)
                self._collapseBefore.setArrowType(Qt.RightArrow)
            else:
                self._collapseAfter.setArrowType(Qt.DownArrow)
                self._collapseBefore.setArrowType(Qt.DownArrow)
        else:
            if (self.orientation() == Qt.Horizontal):
                self._collapseAfter.setArrowType(Qt.LeftArrow)
                self._collapseBefore.setArrowType(Qt.LeftArrow)
            else:
                self._collapseAfter.setArrowType(Qt.UpArrow)
                self._collapseAfter.setArrowType(Qt.UpArrow)

    def paintEvent(self, event):
        """
        Overloads the paint event to handle drawing the splitter lines.
        
        :param      event | <QPaintEvent>
        """
        lines = []
        count = 20

        # calculate the lines
        if (self.orientation() == Qt.Vertical):
            x = self._resizeGrip.pos().x()
            h = self.height()
            spacing = int(float(self._resizeGrip.width()) / count)

            for i in range(count):
                lines.append(QLine(x, 0, x, h))
                x += spacing
        else:
            y = self._resizeGrip.pos().y()
            w = self.width()
            spacing = int(float(self._resizeGrip.height()) / count)

            for i in range(count):
                lines.append(QLine(0, y, w, y))
                y += spacing

        # draw the lines
        with XPainter(self) as painter:
            pal = self.palette()
            painter.setPen(pal.color(pal.Window).darker(120))
            painter.drawLines(lines)

    def setOrientation(self, orientation):
        """
        Sets the orientation for this handle and updates the widgets linked \
        with it.
        
        :param      orientation | <Qt.Orientation>
        """
        super(XSplitterHandle, self).setOrientation(orientation)

        if (orientation == Qt.Vertical):
            self.layout().setDirection(QBoxLayout.LeftToRight)

            # update the widgets
            self._collapseBefore.setFixedSize(30, 10)
            self._collapseAfter.setFixedSize(30, 10)
            self._resizeGrip.setFixedSize(60, 10)

            self._collapseBefore.setArrowType(Qt.UpArrow)
            self._collapseAfter.setArrowType(Qt.DownArrow)

        elif (orientation == Qt.Horizontal):
            self.layout().setDirection(QBoxLayout.TopToBottom)

            # update the widgets
            self._collapseBefore.setFixedSize(10, 30)
            self._collapseAfter.setFixedSize(10, 30)
            self._resizeGrip.setFixedSize(10, 60)

            self._collapseBefore.setArrowType(Qt.LeftArrow)
            self._collapseAfter.setArrowType(Qt.RightArrow)

    def uncollapse(self):
        """
        Uncollapses the splitter this handle is associated with by restoring \
        its sizes from before the collapse occurred.
        
        :return     <bool> | changed
        """
        if (not self.isCollapsed()):
            return False

        self.parent().setSizes(self._storedSizes)
        self.unmarkCollapsed()

        return True

    def unmarkCollapsed(self):
        """
        Unmarks this splitter as being in a collapsed state, clearing any \
        collapsed information.
        """
        if (not self.isCollapsed()):
            return

        self._collapsed = False
        self._storedSizes = None

        if (self.orientation() == Qt.Vertical):
            self._collapseBefore.setArrowType(Qt.UpArrow)
            self._collapseAfter.setArrowType(Qt.DownArrow)
        else:
            self._collapseBefore.setArrowType(Qt.LeftArrow)
            self._collapseAfter.setArrowType(Qt.RightArrow)

    def toggleCollapseAfter(self):
        """
        Collapses the splitter after this handle.
        """
        if (self.isCollapsed()):
            self.uncollapse()
        else:
            self.collapse(XSplitterHandle.CollapseDirection.After)

    def toggleCollapseBefore(self):
        """
        Collapses the splitter before this handle.
        """
        if (self.isCollapsed()):
            self.uncollapse()
        else:
            self.collapse(XSplitterHandle.CollapseDirection.Before)
class XCodeHighlighter(QtGui.QSyntaxHighlighter, AddonManager):
    Style = enum('Keyword',
                 'Comment',
                 'String',
                 'Class',
                 'Function')
    
    Theme = enum('Default',
                 'Dark')
    
    def __init__(self, parent=None):
        super(XCodeHighlighter, self).__init__(parent)
        
        # define the format options
        self._formats = {}
        self._patterns = []
        self._multiline = []
        self._theme = None
        
        self.setTheme(XCodeHighlighter.Theme.Default)

    def definePattern(self, style, pattern):
        """
        Defines a pattern for the given style.
        
        :param      style   | <XCodeHighlighter.Style>
                    pattern | <str>
        """
        self._patterns.append((style, pattern))
    
    def defineMultiline(self, style, openPattern, closePattern):
        """
        Defines a pattern that can span multiple styles.
        
        :param      style        | <XCodeHighlighter.Style>
                    openPattern  | <str>
                    closePattern | <str>
        """
        self._multiline.append((style, openPattern, closePattern))
    
    def highlightBlock(self, text):
        """
        Highlights the given text format based on this highlighters syntax
        rules.
        
        :param      text | <str>
        """
        text = nativestring(text)
        for pattern, format in self.patterns():
            for result in re.finditer(pattern, text):
                grps = result.groups()
                if grps:
                    for i in range(len(grps)):
                        start, end = result.span(i+1)
                        self.setFormat(start, end - start, format)
                else:
                    self.setFormat(result.start(),
                                   result.end() - result.start(),
                                   format)
        
        self.setCurrentBlockState(0)
        if self.previousBlockState() == 1:
            return
        
        for form, open, close in self._multiline:
            open = QtCore.QRegExp(open)
            close = QtCore.QRegExp(close)
            
            start = open.indexIn(text)
            processed = False
            
            while start >= 0:
                processed = True
                end = close.indexIn(text, start)

                if end == -1:
                    self.setCurrentBlockState(1)
                    length = len(text) - start
                else:
                    length = end - start + close.matchedLength()

                self.setFormat(start, length, form)
                start = open.indexIn(text, start + length)
            
            if processed:
                break

    def keywords(self):
        """
        Returns the list of keywords associated with this highlighter.
        
        :return     [<str>, ..]
        """
        return self._keywords
    
    def patterns(self):
        """
        Returns a list of highlighting rules for this highlighter.  This will
        join the list of keywords as a regular expression with the keyword
        format as well as any custom patterns that have been defined.
        
        :return     [(<str>, <QtGui.QTextCharFormat>), ..]
        """
        patterns = []
        
        # add keyword patterns
        form = self.styleFormat(XCodeHighlighter.Style.Keyword)
        for kwd in self.keywords():
            patterns.append((r'\b{0}\b'.format(kwd), form))
        
        # add additional patterns
        for style, pattern in self._patterns:
            form = self.styleFormat(style)
            if not form:
                continue
            
            patterns.append((pattern, form))
        
        return patterns

    def setStyleFormat(self, style, format):
        """
        Sets the character format for the given style for this highlighter.
        
        :param      style   | <XCodeHighlighter.Style>
                    format  | <QtGui.QTextCharFormat>
        """
        self._formats[style] = format
    
    def setKeywords(self, keywords):
        """
        Sets the keywords for this highlighter to the inputed list of keywords.
        
        :param      keywords | [<str>, ..]
        """
        self._keywords = keywords
    
    def setTheme(self, theme):
        if theme == XCodeHighlighter.Theme.Default:
            # create the default keyword format
            form = QtGui.QTextCharFormat()
            form.setForeground(QtGui.QColor('blue'))
            self._formats[XCodeHighlighter.Style.Keyword] = form
            
            # create the default comment format
            form = QtGui.QTextCharFormat()
            form.setForeground(QtGui.QColor('green'))
            self._formats[XCodeHighlighter.Style.Comment] = form
            
            # create the default string format
            form = QtGui.QTextCharFormat()
            form.setForeground(QtGui.QColor('brown'))
            self._formats[XCodeHighlighter.Style.String] = form
            
            # create the class format
            form = QtGui.QTextCharFormat()
            form.setForeground(QtGui.QColor('darkMagenta'))
            form.setFontWeight(QtGui.QFont.Bold)
            self._formats[XCodeHighlighter.Style.Class] = form
            
            # create the function format
            form = QtGui.QTextCharFormat()
            form.setForeground(QtGui.QColor('darkMagenta'))
            form.setFontWeight(QtGui.QFont.Bold)
            self._formats[XCodeHighlighter.Style.Function] = form
    
        elif theme == XCodeHighlighter.Theme.Dark:
            opts = []
            opts.append((self.Style.Keyword, '#2da4ff', False))
            opts.append((self.Style.String, 'orange', False))
            opts.append((self.Style.Comment, '#10ff00', False))
            opts.append((self.Style.Class, '#f7ffc1', True))
            opts.append((self.Style.Function, '#f7ffc1', True))
            
            for style, clr, bold in opts:
                form = QtGui.QTextCharFormat()
                form.setForeground(QtGui.QColor(clr))
                
                if bold:
                    form.setFontWeight(QtGui.QFont.Bold)
                
                self._formats[style] = form
    
    def styleFormat(self, style):
        """
        Returns the character format for the given style for this highlighter.
        
        :param      style | <XCodeHighlighter.Style>
        
        :return     <QtGui.QTextCharFormat> || None
        """
        return self._formats.get(style, None)

    @classmethod
    def fileTypes(cls):
        prop = '_{0}__filetypes'.format(cls.__name__)
        return getattr(cls, prop, '')

    @classmethod
    def hasFileType(cls, filetype):
        return filetype in cls.fileTypes().split(',')

    @classmethod
    def setFileTypes(cls, filetypes):
        prop = '_{0}__filetypes'.format(cls.__name__)
        setattr(cls, prop, filetypes)
Exemple #28
0
class XPopupWidget(QWidget):
    """ """
    Direction = enum('North', 'South', 'East', 'West')
    Mode = enum('Popup', 'Dialog', 'ToolTip')
    Anchor = enum('TopLeft', 'TopCenter', 'TopRight', 'LeftTop', 'LeftCenter',
                  'LeftBottom', 'RightTop', 'RightCenter', 'RightBottom',
                  'BottomLeft', 'BottomCenter', 'BottomRight')

    aboutToShow = Signal()
    accepted = Signal()
    closed = Signal()
    rejected = Signal()
    resetRequested = Signal()
    shown = Signal()
    buttonClicked = Signal(QAbstractButton)

    def __init__(self, parent=None, buttons=None):
        super(XPopupWidget, self).__init__(parent)

        # define custom properties
        self._anchor = XPopupWidget.Anchor.TopCenter
        self._autoCalculateAnchor = False
        self._autoCloseOnAccept = True
        self._autoCloseOnReject = True
        self._autoCloseOnFocusOut = False
        self._autoDefault = True
        self._first = True
        self._animated = False
        self._currentMode = None
        self._positionLinkedTo = []
        self._possibleAnchors = XPopupWidget.Anchor.all()

        # define controls
        self._result = 0
        self._resizable = True
        self._popupPadding = 10
        self._titleBarVisible = True
        self._buttonBoxVisible = True
        self._dialogButton = QToolButton(self)
        self._closeButton = QToolButton(self)
        self._scrollArea = QScrollArea(self)
        self._sizeGrip = QSizeGrip(self)
        self._sizeGrip.setFixedWidth(12)
        self._sizeGrip.setFixedHeight(12)

        self._leftSizeGrip = QSizeGrip(self)
        self._leftSizeGrip.setFixedWidth(12)
        self._leftSizeGrip.setFixedHeight(12)

        if buttons is None:
            buttons = QDialogButtonBox.NoButton

        self._buttonBox = QDialogButtonBox(buttons, Qt.Horizontal, self)
        self._buttonBox.setContentsMargins(3, 0, 3, 9)

        self._scrollArea.setWidgetResizable(True)
        self._scrollArea.setFrameShape(QScrollArea.NoFrame)
        self._scrollArea.setSizePolicy(QSizePolicy.Expanding,
                                       QSizePolicy.Expanding)

        palette = self.palette()
        self._scrollArea.setPalette(palette)

        self._dialogButton.setToolTip('Popout to Dialog')
        self._closeButton.setToolTip('Close Popup')

        for btn in (self._dialogButton, self._closeButton):
            btn.setAutoRaise(True)
            btn.setIconSize(QSize(14, 14))
            btn.setMaximumSize(16, 16)

        # setup the icons
        icon = QIcon(projexui.resources.find('img/dialog.png'))
        self._dialogButton.setIcon(icon)

        icon = QIcon(projexui.resources.find('img/close.png'))
        self._closeButton.setIcon(icon)

        # define the ui
        hlayout = QHBoxLayout()
        hlayout.setSpacing(0)
        hlayout.addStretch(1)
        hlayout.addWidget(self._dialogButton)
        hlayout.addWidget(self._closeButton)
        hlayout.setContentsMargins(0, 0, 0, 0)

        hlayout2 = QHBoxLayout()
        hlayout2.addWidget(self._buttonBox)
        hlayout2.setContentsMargins(0, 0, 3, 0)

        vlayout = QVBoxLayout()
        vlayout.addLayout(hlayout)
        vlayout.addWidget(self._scrollArea)
        vlayout.addLayout(hlayout2)
        vlayout.setContentsMargins(3, 2, 3, 2)
        vlayout.setSpacing(0)

        self.setLayout(vlayout)
        self.setPositionLinkedTo(parent)

        # set default properties
        self.setAutoFillBackground(True)
        self.setBackgroundRole(QPalette.Window)
        self.setWindowTitle('Popup')
        self.setFocusPolicy(Qt.StrongFocus)
        self.setCurrentMode(XPopupWidget.Mode.Popup)

        # create connections
        self._dialogButton.clicked.connect(self.setDialogMode)
        self._closeButton.clicked.connect(self.reject)
        self._buttonBox.accepted.connect(self.accept)
        self._buttonBox.rejected.connect(self.reject)
        self._buttonBox.clicked.connect(self.handleButtonClick)

    def addButton(self, button, role=QDialogButtonBox.ActionRole):
        """
        Adds a custom button to the button box for this popup widget.
        
        :param      button | <QAbstractButton> || <str>
        
        :return     <button> || None (based on if a button or string was given)
        """
        return self._buttonBox.addButton(button, role)

    def adjustContentsMargins(self):
        """
        Adjusts the contents for this widget based on the anchor and \
        mode.
        """
        anchor = self.anchor()
        mode = self.currentMode()

        # margins for a dialog
        if (mode == XPopupWidget.Mode.Dialog):
            self.setContentsMargins(0, 0, 0, 0)

        # margins for a top anchor point
        elif (anchor &
              (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter
               | XPopupWidget.Anchor.TopRight)):
            self.setContentsMargins(0, self.popupPadding() + 5, 0, 0)

        # margins for a bottom anchor point
        elif (
                anchor &
            (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter
             | XPopupWidget.Anchor.BottomRight)):
            self.setContentsMargins(0, 0, 0, self.popupPadding())

        # margins for a left anchor point
        elif (anchor &
              (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter
               | XPopupWidget.Anchor.LeftBottom)):
            self.setContentsMargins(self.popupPadding(), 0, 0, 0)

        # margins for a right anchor point
        else:
            self.setContentsMargins(0, 0, self.popupPadding(), 0)

        self.adjustMask()

    def adjustMask(self):
        """
        Updates the alpha mask for this popup widget.
        """
        if self.currentMode() == XPopupWidget.Mode.Dialog:
            self.clearMask()
            return

        path = self.borderPath()
        bitmap = QBitmap(self.width(), self.height())
        bitmap.fill(QColor('white'))

        with XPainter(bitmap) as painter:
            painter.setRenderHint(XPainter.Antialiasing)
            pen = QPen(QColor('black'))
            pen.setWidthF(0.75)
            painter.setPen(pen)
            painter.setBrush(QColor('black'))
            painter.drawPath(path)

        self.setMask(bitmap)

    def adjustSize(self):
        """
        Adjusts the size of this popup to best fit the new widget size.
        """
        widget = self.centralWidget()
        if widget is None:
            super(XPopupWidget, self).adjustSize()
            return

        widget.adjustSize()
        hint = widget.minimumSizeHint()
        size = widget.minimumSize()

        width = max(size.width(), hint.width())
        height = max(size.height(), hint.height())

        width += 20
        height += 20

        if self._buttonBoxVisible:
            height += self.buttonBox().height() + 10

        if self._titleBarVisible:
            height += max(self._dialogButton.height(),
                          self._closeButton.height()) + 10

        curr_w = self.width()
        curr_h = self.height()

        # determine if we need to move based on our anchor
        anchor = self.anchor()
        if anchor & (self.Anchor.LeftBottom | self.Anchor.RightBottom | \
                       self.Anchor.BottomLeft | self.Anchor.BottomCenter | \
                       self.Anchor.BottomRight):
            delta_y = height - curr_h

        elif anchor & (self.Anchor.LeftCenter | self.Anchor.RightCenter):
            delta_y = (height - curr_h) / 2

        else:
            delta_y = 0

        if anchor & (self.Anchor.RightTop | self.Anchor.RightCenter | \
                       self.Anchor.RightTop | self.Anchor.TopRight):
            delta_x = width - curr_w

        elif anchor & (self.Anchor.TopCenter | self.Anchor.BottomCenter):
            delta_x = (width - curr_w) / 2

        else:
            delta_x = 0

        self.setMinimumSize(width, height)
        self.resize(width, height)

        pos = self.pos()
        pos.setX(pos.x() - delta_x)
        pos.setY(pos.y() - delta_y)

        self.move(pos)

    @Slot()
    def accept(self):
        """
        Emits the accepted signal and closes the popup.
        """
        self._result = 1

        if not self.signalsBlocked():
            self.accepted.emit()

        if self.autoCloseOnAccept():
            self.close()

    def anchor(self):
        """
        Returns the anchor point for this popup widget.
        
        :return     <XPopupWidget.Anchor>
        """
        return self._anchor

    def autoCalculateAnchor(self):
        """
        Returns whether or not this popup should calculate the anchor point
        on popup based on the parent widget and the popup point.
        
        :return     <bool>
        """
        return self._autoCalculateAnchor

    def autoCloseOnAccept(self):
        """
        Returns whether or not this popup widget manages its own close on accept
        behavior.
        
        :return     <bool>
        """
        return self._autoCloseOnAccept

    def autoCloseOnReject(self):
        """
        Returns whether or not this popup widget manages its own close on reject
        behavior.
        
        :return     <bool>
        """
        return self._autoCloseOnReject

    def autoCloseOnFocusOut(self):
        """
        Returns whether or not this popup widget should auto-close when the user
        clicks off the view.
        
        :return     <bool>
        """
        return self._autoCloseOnFocusOut

    def autoDefault(self):
        """
        Returns whether or not clicking enter should default to the accept key.
        
        :return     <bool>
        """
        return self._autoDefault

    def borderPath(self):
        """
        Returns the border path that will be drawn for this widget.
        
        :return     <QPainterPath>
        """

        path = QPainterPath()

        x = 1
        y = 1
        w = self.width() - 2
        h = self.height() - 2
        pad = self.popupPadding()
        anchor = self.anchor()

        # create a path for a top-center based popup
        if anchor == XPopupWidget.Anchor.TopCenter:
            path.moveTo(x, y + pad)
            path.lineTo(x + ((w / 2) - pad), y + pad)
            path.lineTo(x + (w / 2), y)
            path.lineTo(x + ((w / 2) + pad), y + pad)
            path.lineTo(x + w, y + pad)
            path.lineTo(x + w, y + h)
            path.lineTo(x, y + h)
            path.lineTo(x, y + pad)

        # create a path for a top-left based popup
        elif anchor == XPopupWidget.Anchor.TopLeft:
            path.moveTo(x, y + pad)
            path.lineTo(x + pad, y)
            path.lineTo(x + 2 * pad, y + pad)
            path.lineTo(x + w, y + pad)
            path.lineTo(x + w, y + h)
            path.lineTo(x, y + h)
            path.lineTo(x, y + pad)

        # create a path for a top-right based popup
        elif anchor == XPopupWidget.Anchor.TopRight:
            path.moveTo(x, y + pad)
            path.lineTo(x + w - 2 * pad, y + pad)
            path.lineTo(x + w - pad, y)
            path.lineTo(x + w, y + pad)
            path.lineTo(x + w, y + h)
            path.lineTo(x, y + h)
            path.lineTo(x, y + pad)

        # create a path for a bottom-left based popup
        elif anchor == XPopupWidget.Anchor.BottomLeft:
            path.moveTo(x, y)
            path.lineTo(x + w, y)
            path.lineTo(x + w, y + h - pad)
            path.lineTo(x + 2 * pad, y + h - pad)
            path.lineTo(x + pad, y + h)
            path.lineTo(x, y + h - pad)
            path.lineTo(x, y)

        # create a path for a south based popup
        elif anchor == XPopupWidget.Anchor.BottomCenter:
            path.moveTo(x, y)
            path.lineTo(x + w, y)
            path.lineTo(x + w, y + h - pad)
            path.lineTo(x + ((w / 2) + pad), y + h - pad)
            path.lineTo(x + (w / 2), y + h)
            path.lineTo(x + ((w / 2) - pad), y + h - pad)
            path.lineTo(x, y + h - pad)
            path.lineTo(x, y)

        # create a path for a bottom-right based popup
        elif anchor == XPopupWidget.Anchor.BottomRight:
            path.moveTo(x, y)
            path.lineTo(x + w, y)
            path.lineTo(x + w, y + h - pad)
            path.lineTo(x + w - pad, y + h)
            path.lineTo(x + w - 2 * pad, y + h - pad)
            path.lineTo(x, y + h - pad)
            path.lineTo(x, y)

        # create a path for a right-top based popup
        elif anchor == XPopupWidget.Anchor.RightTop:
            path.moveTo(x, y)
            path.lineTo(x + w - pad, y)
            path.lineTo(x + w, y + pad)
            path.lineTo(x + w - pad, y + 2 * pad)
            path.lineTo(x + w - pad, y + h)
            path.lineTo(x, y + h)
            path.lineTo(x, y)

        # create a path for a right-center based popup
        elif anchor == XPopupWidget.Anchor.RightCenter:
            path.moveTo(x, y)
            path.lineTo(x + w - pad, y)
            path.lineTo(x + w - pad, y + ((h / 2) - pad))
            path.lineTo(x + w, y + (h / 2))
            path.lineTo(x + w - pad, y + ((h / 2) + pad))
            path.lineTo(x + w - pad, y + h)
            path.lineTo(x, y + h)
            path.lineTo(x, y)

        # create a path for a right-bottom based popup
        elif anchor == XPopupWidget.Anchor.RightBottom:
            path.moveTo(x, y)
            path.lineTo(x + w - pad, y)
            path.lineTo(x + w - pad, y + h - 2 * pad)
            path.lineTo(x + w, y + h - pad)
            path.lineTo(x + w - pad, y + h)
            path.lineTo(x, y + h)
            path.lineTo(x, y)

        # create a path for a left-top based popup
        elif anchor == XPopupWidget.Anchor.LeftTop:
            path.moveTo(x + pad, y)
            path.lineTo(x + w, y)
            path.lineTo(x + w, y + h)
            path.lineTo(x + pad, y + h)
            path.lineTo(x + pad, y + 2 * pad)
            path.lineTo(x, y + pad)
            path.lineTo(x + pad, y)

        # create a path for an left-center based popup
        elif anchor == XPopupWidget.Anchor.LeftCenter:
            path.moveTo(x + pad, y)
            path.lineTo(x + w, y)
            path.lineTo(x + w, y + h)
            path.lineTo(x + pad, y + h)
            path.lineTo(x + pad, y + ((h / 2) + pad))
            path.lineTo(x, y + (h / 2))
            path.lineTo(x + pad, y + ((h / 2) - pad))
            path.lineTo(x + pad, y)

        # create a path for a left-bottom based popup
        elif anchor == XPopupWidget.Anchor.LeftBottom:
            path.moveTo(x + pad, y)
            path.lineTo(x + w, y)
            path.lineTo(x + w, y + h)
            path.lineTo(x + pad, y + h)
            path.lineTo(x, y + h - pad)
            path.lineTo(x + pad, y + h - 2 * pad)
            path.lineTo(x + pad, y)

        return path

    def buttonBox(self):
        """
        Returns the button box that is used to control this popup widget.
        
        :return     <QDialogButtonBox>
        """
        return self._buttonBox

    def centralWidget(self):
        """
        Returns the central widget that is being used by this popup.
        
        :return     <QWidget>
        """
        return self._scrollArea.widget()

    def close(self):
        """
        Closes the popup widget and central widget.
        """
        widget = self.centralWidget()
        if widget and not widget.close():
            return

        super(XPopupWidget, self).close()

    def closeEvent(self, event):
        widget = self.centralWidget()
        if widget and not widget.close() and \
           self.currentMode() != XPopupWidget.Mode.ToolTip:
            event.ignore()
        else:
            super(XPopupWidget, self).closeEvent(event)

        self.closed.emit()

    def currentMode(self):
        """
        Returns the current mode for this widget.
        
        :return     <XPopupWidget.Mode>
        """
        return self._currentMode

    @deprecatedmethod('XPopupWidget',
                      'Direction is no longer used, use anchor instead')
    def direction(self):
        """
        Returns the current direction parameter for this widget.
        
        :return     <XPopupWidget.Direction>
        """
        anchor = self.anchor()
        if (anchor &
            (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter
             | XPopupWidget.Anchor.TopRight)):
            return XPopupWidget.Direction.North

        elif (
                anchor &
            (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter
             | XPopupWidget.Anchor.BottomRight)):
            return XPopupWidget.Direction.South

        elif (anchor &
              (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter
               | XPopupWidget.Anchor.LeftBottom)):
            return XPopupWidget.Direction.East

        else:
            return XPopupWidget.Direction.West

    def exec_(self, pos=None):
        self._result = 0
        self.setWindowModality(Qt.ApplicationModal)
        self.popup(pos)
        while self.isVisible():
            QApplication.processEvents()

        return self.result()

    def eventFilter(self, object, event):
        """
        Processes when the window is moving to update the position for the
        popup if in popup mode.
        
        :param      object | <QObject>
                    event  | <QEvent>
        """
        if not self.isVisible():
            return False

        links = self.positionLinkedTo()
        is_dialog = self.currentMode() == self.Mode.Dialog
        if object not in links:
            return False

        if event.type() == event.Close:
            self.close()
            return False

        if event.type() == event.Hide and not is_dialog:
            self.hide()
            return False

        if event.type() == event.Move and not is_dialog:
            deltaPos = event.pos() - event.oldPos()
            self.move(self.pos() + deltaPos)
            return False

        if self.currentMode() != self.Mode.ToolTip:
            return False

        if event.type() == event.Leave:
            pos = object.mapFromGlobal(QCursor.pos())
            if (not object.rect().contains(pos)):
                self.close()
                event.accept()
                return True

        if event.type() in (event.MouseButtonPress, event.MouseButtonDblClick):
            self.close()
            event.accept()
            return True

        return False

    @Slot(QAbstractButton)
    def handleButtonClick(self, button):
        """
        Handles the button click for this widget.  If the Reset button was
        clicked, then the resetRequested signal will be emitted.  All buttons
        will emit the buttonClicked signal.
        
        :param      button | <QAbstractButton>
        """
        if (self.signalsBlocked()):
            return

        if (button == self._buttonBox.button(QDialogButtonBox.Reset)):
            self.resetRequested.emit()

        self.buttonClicked.emit(button)

    def isAnimated(self):
        """
        Returns whether or not the popup widget should animate its opacity
        when it is shown.
        
        :return     <bool>
        """
        return self._animated

    def isPossibleAnchor(self, anchor):
        return bool(anchor & self._possibleAnchors)

    def isResizable(self):
        """
        Returns if this popup is resizable or not.
        
        :return     <bool>
        """
        return self._resizable

    def keyPressEvent(self, event):
        """
        Looks for the Esc key to close the popup.
        
        :param      event | <QKeyEvent>
        """
        if (event.key() == Qt.Key_Escape):
            self.reject()
            event.accept()
            return

        elif (event.key() in (Qt.Key_Return, Qt.Key_Enter)):
            if self._autoDefault:
                self.accept()
                event.accept()
            return

        super(XPopupWidget, self).keyPressEvent(event)

    def mapAnchorFrom(self, widget, point):
        """
        Returns the anchor point that best fits within the given widget from
        the inputed global position.
        
        :param      widget      | <QWidget>
                    point       | <QPoint>
        
        :return     <XPopupWidget.Anchor>
        """
        screen_geom = QtGui.QDesktopWidget(self).screenGeometry()

        # calculate the end rect for each position
        Anchor = self.Anchor
        w = self.width()
        h = self.height()

        possible_rects = {
            # top anchors
            Anchor.TopLeft:
            QtCore.QRect(point.x(), point.y(), w, h),
            Anchor.TopCenter:
            QtCore.QRect(point.x() - w / 2, point.y(), w, h),
            Anchor.TopRight:
            QtCore.QRect(point.x() - w, point.y(), w, h),

            # left anchors
            Anchor.LeftTop:
            QtCore.QRect(point.x(), point.y(), w, h),
            Anchor.LeftCenter:
            QtCore.QRect(point.x(),
                         point.y() - h / 2, w, h),
            Anchor.LeftBottom:
            QtCore.QRect(point.x(),
                         point.y() - h, w, h),

            # bottom anchors
            Anchor.BottomLeft:
            QtCore.QRect(point.x(),
                         point.y() - h, w, h),
            Anchor.BottomCenter:
            QtCore.QRect(point.x() - w / 2,
                         point.y() - h, w, h),
            Anchor.BottomRight:
            QtCore.QRect(point.x() - w,
                         point.y() - h, w, h),

            # right anchors
            Anchor.RightTop:
            QtCore.QRect(point.x() - self.width(), point.y(), w, h),
            Anchor.RightCenter:
            QtCore.QRect(point.x() - self.width(),
                         point.y() - h / 2, w, h),
            Anchor.RightBottom:
            QtCore.QRect(point.x() - self.width(),
                         point.y() - h, w, h)
        }

        for anchor in (Anchor.TopCenter, Anchor.BottomCenter,
                       Anchor.LeftCenter, Anchor.RightCenter, Anchor.TopLeft,
                       Anchor.LeftTop, Anchor.BottomLeft, Anchor.LeftBottom,
                       Anchor.TopRight, Anchor.RightTop, Anchor.BottomRight,
                       Anchor.RightBottom):

            if not self.isPossibleAnchor(anchor):
                continue

            rect = possible_rects[anchor]
            if screen_geom.contains(rect):
                return anchor

        return self.anchor()

    def popup(self, pos=None):
        """
        Pops up this widget at the inputed position.  The inputed point should \
        be in global space.
        
        :param      pos | <QPoint>
        
        :return     <bool> success
        """
        if self._first and self.centralWidget() is not None:
            self.adjustSize()
            self._first = False

        if not self.signalsBlocked():
            self.aboutToShow.emit()

        if not pos:
            pos = QCursor.pos()

        if self.currentMode() == XPopupWidget.Mode.Dialog and \
             self.isVisible():
            return False

        elif self.currentMode() == XPopupWidget.Mode.Dialog:
            self.setPopupMode()

        # auto-calculate the point
        if self.autoCalculateAnchor():
            self.setAnchor(self.mapAnchorFrom(self.parent(), pos))

        pad = self.popupPadding()

        # determine where to move based on the anchor
        anchor = self.anchor()

        # MODIFY X POSITION
        # align x-left
        if (anchor &
            (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.BottomLeft)):
            pos.setX(pos.x() - pad)

        # align x-center
        elif (anchor & (XPopupWidget.Anchor.TopCenter
                        | XPopupWidget.Anchor.BottomCenter)):
            pos.setX(pos.x() - self.width() / 2)

        # align x-right
        elif (
                anchor &
            (XPopupWidget.Anchor.TopRight | XPopupWidget.Anchor.BottomRight)):
            pos.setX(pos.x() - self.width() + pad)

        # align x-padded
        elif (anchor &
              (XPopupWidget.Anchor.RightTop | XPopupWidget.Anchor.RightCenter
               | XPopupWidget.Anchor.RightBottom)):
            pos.setX(pos.x() - self.width())

        # MODIFY Y POSITION
        # align y-top
        if (anchor &
            (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.RightTop)):
            pos.setY(pos.y() - pad)

        # align y-center
        elif (anchor & (XPopupWidget.Anchor.LeftCenter
                        | XPopupWidget.Anchor.RightCenter)):
            pos.setY(pos.y() - self.height() / 2)

        # align y-bottom
        elif (anchor & (XPopupWidget.Anchor.LeftBottom
                        | XPopupWidget.Anchor.RightBottom)):
            pos.setY(pos.y() - self.height() + pad)

        # align y-padded
        elif (
                anchor &
            (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter
             | XPopupWidget.Anchor.BottomRight)):
            pos.setY(pos.y() - self.height())

        self.adjustMask()
        self.move(pos)
        self.update()
        self.setUpdatesEnabled(True)

        if self.isAnimated():
            anim = QPropertyAnimation(self, 'windowOpacity')
            anim.setParent(self)
            anim.setStartValue(0.0)
            anim.setEndValue(self.windowOpacity())
            anim.setDuration(500)
            anim.finished.connect(anim.deleteLater)
            self.setWindowOpacity(0.0)
        else:
            anim = None

        self.show()

        if self.currentMode() != XPopupWidget.Mode.ToolTip:
            self.activateWindow()

            widget = self.centralWidget()
            if widget:
                self.centralWidget().setFocus()

        if anim:
            anim.start()

        if not self.signalsBlocked():
            self.shown.emit()

        return True

    def paintEvent(self, event):
        """
        Overloads the paint event to handle painting pointers for the popup \
        mode.
        
        :param      event | <QPaintEvent>
        """
        # use the base technique for the dialog mode
        if self.currentMode() == XPopupWidget.Mode.Dialog:
            super(XPopupWidget, self).paintEvent(event)
            return

        # setup the coloring options
        palette = self.palette()

        with XPainter(self) as painter:
            pen = QPen(palette.color(palette.Window).darker(130))
            pen.setWidthF(1.75)
            painter.setPen(pen)
            painter.setRenderHint(painter.Antialiasing)
            painter.setBrush(palette.color(palette.Window))
            painter.drawPath(self.borderPath())

    def popupPadding(self):
        """
        Returns the amount of pixels to pad the popup arrow for this widget.
        
        :return     <int>
        """
        return self._popupPadding

    def possibleAnchors(self):
        return self._possibleAnchors

    def positionLinkedTo(self):
        """
        Returns the widget that this popup is linked to for positional changes.
        
        :return     [<QWidget>, ..]
        """
        return self._positionLinkedTo

    @Slot()
    def reject(self):
        """
        Emits the accepted signal and closes the popup.
        """
        self._result = 0
        if not self.signalsBlocked():
            self.rejected.emit()

        if self.autoCloseOnReject():
            self.close()

    def result(self):
        return self._result

    def resizeEvent(self, event):
        """
        Resizes this widget and updates the mask.
        
        :param      event | <QResizeEvent>
        """
        self.setUpdatesEnabled(False)
        super(XPopupWidget, self).resizeEvent(event)

        self.adjustMask()
        self.setUpdatesEnabled(True)

        x = self.width() - self._sizeGrip.width()
        y = self.height() - self._sizeGrip.height()

        self._leftSizeGrip.move(0, y)
        self._sizeGrip.move(x, y)

    def scrollArea(self):
        """
        Returns the scroll area widget for this popup.
        
        :return     <QScrollArea>
        """
        return self._scrollArea

    def setAnimated(self, state):
        """
        Sets whether or not the popup widget should animate its opacity
        when it is shown.
        
        :param      state | <bool>
        """
        self._animated = state
        self.setAttribute(Qt.WA_TranslucentBackground, state)

    def setAutoCloseOnAccept(self, state):
        """
        Sets whether or not the popup handles closing for accepting states.
        
        :param      state | <bool>
        """
        self._autoCloseOnAccept = state

    def setAutoCloseOnReject(self, state):
        """
        Sets whether or not the popup handles closing for rejecting states.
        
        :param      state | <bool>
        """
        self._autoCloseOnReject = state

    def setAutoDefault(self, state):
        """
        Sets whether or not the buttons should respond to defaulting options
        when the user is interacting with it.
        
        :param      state | <bool>
        """
        self._autoDefault = state
        for button in self.buttonBox().buttons():
            button.setAutoDefault(state)
            button.setDefault(state)

    def setAnchor(self, anchor):
        """
        Sets the anchor position for this popup widget to the inputed point.
        
        :param      anchor | <XPopupWidget.Anchor>
        """
        self._anchor = anchor
        self.adjustContentsMargins()

    def setAutoCalculateAnchor(self, state=True):
        """
        Sets whether or not this widget should auto-calculate the anchor point
        based on the parent position when the popup is triggered.
        
        :param      state | <bool>
        """
        self._autoCalculateAnchor = state

    def setAutoCloseOnFocusOut(self, state):
        """
        Sets whether or not this popup widget should auto-close when the user
        clicks off the view.
        
        :param     state | <bool>
        """
        self._autoCloseOnFocusOut = state
        self.updateModeSettings()

    def setCentralWidget(self, widget):
        """
        Sets the central widget that will be used by this popup.
        
        :param      widget | <QWidget> || None
        """
        self._scrollArea.takeWidget()
        self._scrollArea.setWidget(widget)

        self.adjustSize()

    def setCurrentMode(self, mode):
        """
        Sets the current mode for this dialog to the inputed mode.
        
        :param      mode | <XPopupWidget.Mode>
        """
        if (self._currentMode == mode):
            return

        self._currentMode = mode
        self.updateModeSettings()

    @Slot()
    def setDialogMode(self):
        """
        Sets the current mode value to Dialog.
        """
        self.setCurrentMode(XPopupWidget.Mode.Dialog)

    @deprecatedmethod('XPopupWidget',
                      'Direction is no longer used, use setAnchor instead')
    def setDirection(self, direction):
        """
        Sets the direction for this widget to the inputed direction.
        
        :param      direction | <XPopupWidget.Direction>
        """
        if (direction == XPopupWidget.Direction.North):
            self.setAnchor(XPopupWidget.Anchor.TopCenter)

        elif (direction == XPopupWidget.Direction.South):
            self.setAnchor(XPopupWidget.Anchor.BottomCenter)

        elif (direction == XPopupWidget.Direction.East):
            self.setAnchor(XPopupWidget.Anchor.LeftCenter)

        else:
            self.setAnchor(XPopupWidget.Anchor.RightCenter)

    def setPalette(self, palette):
        """
        Sets the palette for this widget and the scroll area.
        
        :param      palette | <QPalette>
        """
        super(XPopupWidget, self).setPalette(palette)
        self._scrollArea.setPalette(palette)

    def setPopupMode(self):
        """
        Sets the current mode value to Popup.
        """
        self.setCurrentMode(XPopupWidget.Mode.Popup)

    def setPopupPadding(self, padding):
        """
        Sets the amount to pad the popup area when displaying this widget.
        
        :param      padding | <int>
        """
        self._popupPadding = padding
        self.adjustContentsMargins()

    def setPossibleAnchors(self, anchors):
        self._possibleAnchors = anchors

    def setPositionLinkedTo(self, widgets):
        """
        Sets the widget that this popup will be linked to for positional
        changes.
        
        :param      widgets | <QWidget> || [<QWidget>, ..]
        """
        if type(widgets) in (list, set, tuple):
            new_widgets = list(widgets)
        else:
            new_widgets = []
            widget = widgets
            while widget:
                widget.installEventFilter(self)
                new_widgets.append(widget)
                widget = widget.parent()

        self._positionLinkedTo = new_widgets

    def setResizable(self, state):
        self._resizable = state
        self._sizeGrip.setVisible(state)
        self._leftSizeGrip.setVisible(state)

    def setShowButtonBox(self, state):
        self._buttonBoxVisible = state
        self.buttonBox().setVisible(state)

    def setShowTitleBar(self, state):
        self._titleBarVisible = state
        self._dialogButton.setVisible(state)
        self._closeButton.setVisible(state)

    def setToolTipMode(self):
        """
        Sets the mode for this popup widget to ToolTip
        """
        self.setCurrentMode(XPopupWidget.Mode.ToolTip)

    def setVisible(self, state):
        super(XPopupWidget, self).setVisible(state)
        widget = self.centralWidget()
        if widget:
            widget.setVisible(state)

    def timerEvent(self, event):
        """
        When the timer finishes, hide the tooltip popup widget.
        
        :param      event | <QEvent>
        """
        if self.currentMode() == XPopupWidget.Mode.ToolTip:
            self.killTimer(event.timerId())
            event.accept()
            self.close()
        else:
            super(XPopupWidget, self).timerEvent(event)

    def updateModeSettings(self):
        mode = self.currentMode()
        is_visible = self.isVisible()

        # display as a floating dialog
        if mode == XPopupWidget.Mode.Dialog:
            self.setWindowFlags(Qt.Dialog | Qt.Tool)
            self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
            self._closeButton.setVisible(False)
            self._dialogButton.setVisible(False)

        # display as a user tooltip
        elif mode == XPopupWidget.Mode.ToolTip:
            flags = Qt.Popup | Qt.FramelessWindowHint

            self.setWindowFlags(flags)
            self.setBackgroundRole(QPalette.Window)
            self.setAttribute(Qt.WA_TransparentForMouseEvents)
            self.setShowTitleBar(False)
            self.setShowButtonBox(False)
            self.setFocusPolicy(Qt.NoFocus)

            # hide the scrollbars
            policy = Qt.ScrollBarAlwaysOff
            self._scrollArea.setVerticalScrollBarPolicy(policy)
            self._scrollArea.setHorizontalScrollBarPolicy(policy)

        # display as a popup widget
        else:
            flags = Qt.Popup | Qt.FramelessWindowHint

            if not self.autoCloseOnFocusOut():
                flags |= Qt.Tool

            self.setWindowFlags(flags)
            self._closeButton.setVisible(self._titleBarVisible)
            self._dialogButton.setVisible(self._titleBarVisible)
            self.setBackgroundRole(QPalette.Window)

        self.adjustContentsMargins()

        if (is_visible):
            self.show()

    @staticmethod
    @deprecatedmethod('XPopupWidget',
                      'This method no longer has an effect as we are not '\
                      'storing references to the tooltip.')
    def hideToolTip(key=None):
        """
        Hides any existing tooltip popup widgets.
        
        :warning    This method is deprecated!
        """
        pass

    @staticmethod
    def showToolTip(text,
                    point=None,
                    anchor=None,
                    parent=None,
                    background=None,
                    foreground=None,
                    key=None,
                    seconds=5):
        """
        Displays a popup widget as a tooltip bubble.
        
        :param      text        | <str>
                    point       | <QPoint> || None
                    anchor      | <XPopupWidget.Mode.Anchor> || None
                    parent      | <QWidget> || None
                    background  | <QColor> || None
                    foreground  | <QColor> || None
                    key         | <str> || None
                    seconds     | <int>
        """
        if point is None:
            point = QCursor.pos()

        if parent is None:
            parent = QApplication.activeWindow()

        if anchor is None and parent is None:
            anchor = XPopupWidget.Anchor.TopCenter

        # create a new tooltip widget
        widget = XPopupWidget(parent)
        widget.setToolTipMode()
        widget.setResizable(False)

        # create the tooltip label
        label = QLabel(text, widget)
        label.setOpenExternalLinks(True)
        label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        label.setMargin(3)
        label.setIndent(3)
        label.adjustSize()

        widget.setCentralWidget(label)

        # update the tip
        label.adjustSize()
        widget.adjustSize()

        palette = widget.palette()
        if not background:
            background = palette.color(palette.ToolTipBase)
        if not foreground:
            foreground = palette.color(palette.ToolTipText)

        palette.setColor(palette.Window, QColor(background))
        palette.setColor(palette.WindowText, QColor(foreground))
        widget.setPalette(palette)
        widget.centralWidget().setPalette(palette)

        if anchor is None:
            widget.setAutoCalculateAnchor(True)
        else:
            widget.setAnchor(anchor)

        widget.setAutoCloseOnFocusOut(True)
        widget.setAttribute(Qt.WA_DeleteOnClose)
        widget.popup(point)
        widget.startTimer(1000 * seconds)

        return widget
Exemple #29
0
class XGanttWidgetItem(XTreeWidgetItem):
    """ 
    Defines the main widget item class that contains information for both the
    tree and view widget items.
    """

    ItemStyle = enum('Normal', 'Group', 'Milestone')

    def __init__(self, ganttWidget):
        super(XGanttWidgetItem, self).__init__()

        # set default properties
        self.setFixedHeight(ganttWidget.cellHeight())
        for i in range(1, 20):
            self.setTextAlignment(i, Qt.AlignCenter)

        # define custom properties
        self._blockedAdjustments = {}
        self._viewItem = self.createViewItem()
        self._dateStart = QDate.currentDate()
        self._dateEnd = QDate.currentDate()
        self._allDay = True
        self._timeStart = QTime(0, 0, 0)
        self._timeEnd = QTime(23, 59, 59)
        self._name = ''
        self._properties = {}
        self._itemStyle = XGanttWidgetItem.ItemStyle.Normal
        self._useGroupStyleWithChildren = True
        self._dependencies = {}
        self._reverseDependencies = {}

    def addChild(self, item):
        """
        Adds a new child item to this item.
        
        :param      item | <XGanttWidgetItem>
        """
        super(XGanttWidgetItem, self).addChild(item)

        item.sync()

    def addDependency(self, item):
        """
        Creates a dependency for this item to the next item.  This item will
        be treated as the source, the other as the target.
        
        :param      item | <QGanttWidgetItem>
        """
        if item in self._dependencies:
            return

        viewItem = XGanttDepItem(self, item)

        self._dependencies[item] = viewItem
        item._reverseDependencies[self] = viewItem

        self.syncDependencies()

    def adjustmentsBlocked(self, key):
        """
        Returns whether or not hierarchy adjustments are being blocked.
        
        :param      key | <str>
        
        :return     <bool>
        """
        return self._blockedAdjustments.get(nativestring(key), False)

    def adjustChildren(self, delta, secs=False):
        """
        Shifts the children for this item by the inputed number of days.
        
        :param      delta | <int>
        """
        if self.adjustmentsBlocked('children'):
            return

        if self.itemStyle() != self.ItemStyle.Group:
            return

        if not delta:
            return

        for c in range(self.childCount()):
            child = self.child(c)
            child.blockAdjustments('range', True)
            if secs:
                dstart = child.dateTimeStart()
                dstart = dstart.addSecs(delta)
                child.setDateStart(dstart.date())
                child.setTimeStart(dstart.time())
            else:
                child.setDateStart(child.dateStart().addDays(delta))
            child.blockAdjustments('range', False)

    def adjustRange(self, recursive=True):
        """
        Adjust the start and end ranges for this item based on the limits from
        its children.  This method will only apply to group items.
        
        :param      recursive | <bool>
        """
        if (self.adjustmentsBlocked('range')):
            return

        if (self.itemStyle() == self.ItemStyle.Group):
            dateStart = self.dateStart()
            dateEnd = self.dateEnd()
            first = True

            for c in range(self.childCount()):
                child = self.child(c)

                if (first):
                    dateStart = child.dateStart()
                    dateEnd = child.dateEnd()
                    first = False
                else:
                    dateStart = min(child.dateStart(), dateStart)
                    dateEnd = max(child.dateEnd(), dateEnd)

            self._dateStart = dateStart
            self._dateEnd = dateEnd

            self.sync()

        if (self.parent() and recursive):
            self.parent().adjustRange(True)

    def blockAdjustments(self, key, state):
        """
        Blocks the inputed adjustments for the given key type.
        
        :param      key     | <str>
                    state   | <bool>
        """
        self._blockedAdjustments[nativestring(key)] = state

    def clearDependencies(self):
        """
        Clears out all the dependencies from the scene.
        """
        gantt = self.ganttWidget()
        if (not gantt):
            return

        scene = gantt.viewWidget().scene()

        for target, viewItem in self._dependencies.items():
            target._reverseDependencies.pop(self)
            scene.removeItem(viewItem)

        self._dependencies.clear()

    def createViewItem(self):
        """
        Returns a new XGanttViewItem to use with this item.
        
        :return     <XGanttViewItem>
        """
        return XGanttViewItem(self)

    def dateEnd(self):
        """
        Return the end date for this gantt item.
        
        :return     <QDate>
        """
        return self._dateEnd

    def dateStart(self):
        """
        Return the start date for this gantt item.
        
        :return     <QDate>
        """
        return self._dateStart

    def dateTimeEnd(self):
        """
        Returns a merging of data from the date end with the time end.
        
        :return     <QDateTime>
        """
        return QDateTime(self.dateEnd(), self.timeEnd())

    def dateTimeStart(self):
        """
        Returns a merging of data from the date end with the date start.
        
        :return     <QDateTime>
        """
        return QDateTime(self.dateStart(), self.timeStart())

    def dependencies(self):
        """
        Returns a list of all the dependencies linked with this item.
        
        :return     [<XGanttWidgetItem>, ..]
        """
        return self._dependencies.keys()

    def duration(self):
        """
        Returns the number of days this gantt item represents.
        
        :return     <int>
        """
        return 1 + self.dateStart().daysTo(self.dateEnd())

    def ganttWidget(self):
        """
        Returns the gantt widget that this item is linked to.
        
        :return     <XGanttWidget> || None
        """
        tree = self.treeWidget()
        if (not tree):
            return None

        from projexui.widgets.xganttwidget import XGanttWidget
        return projexui.ancestor(tree, XGanttWidget)

    def insertChild(self, index, item):
        """
        Inserts a new item in the given index.
        
        :param      index | <int>
                    item  | <XGanttWidgetItem>
        """
        super(XGanttWidgetItem, self).insertChild(index, item)
        item.sync()

    def isAllDay(self):
        """
        Returns whehter or not this item reflects an all day event.
        
        :return     <bool>
        """
        return self._allDay

    def itemStyle(self):
        """
        Returns the item style information for this item.
        
        :return     <XGanttWidgetItem.ItemStyle>
        """
        if (self.useGroupStyleWithChildren() and self.childCount()):
            return XGanttWidgetItem.ItemStyle.Group

        return self._itemStyle

    def name(self):
        """
        Returns the name for this gantt widget item.
        
        :return     <str>
        """
        return self._name

    def property(self, key, default=None):
        """
        Returns the custom data that is stored on this object.
        
        :param      key     | <str>
                    default | <variant>
        
        :return     <variant>
        """
        if key == 'Name':
            return self.name()
        elif key == 'Start':
            return self.dateStart()
        elif key == 'End':
            return self.dateEnd()
        elif key == 'Calendar Days':
            return self.duration()
        elif key == 'Work Days':
            return self.weekdays()
        elif key == 'Time Start':
            return self.timeStart()
        elif key == 'Time End':
            return self.timeEnd()
        elif key == 'All Day':
            return self.isAllDay()
        else:
            return self._properties.get(nativestring(key), default)

    def removeFromScene(self):
        """
        Removes this item from the view scene.
        """
        gantt = self.ganttWidget()
        if not gantt:
            return

        scene = gantt.viewWidget().scene()

        scene.removeItem(self.viewItem())
        for target, viewItem in self._dependencies.items():
            target._reverseDependencies.pop(self)
            scene.removeItem(viewItem)

    def setAllDay(self, state):
        """
        Sets whether or not this item is an all day event.
        
        :param      state | <bool>
        """
        self._allDay = state

    def setDateEnd(self, date):
        """
        Sets the date start value for this item.
        
        :param      dateStart | <QDate>
        """
        self._dateEnd = date

    def setDateStart(self, date):
        """
        Sets the date start value for this item.
        
        :param      dateStart | <QDate>
        """
        self._dateStart = date

    def setDateTimeEnd(self, dtime):
        """
        Sets the endiing date time for this gantt chart.
        
        :param      dtime | <QDateTime>
        """
        self._dateEnd = dtime.date()
        self._timeEnd = dtime.time()
        self._allDay = False

    def setDateTimeStart(self, dtime):
        """
        Sets the starting date time for this gantt chart.
        
        :param      dtime | <QDateTime>
        """
        self._dateStart = dtime.date()
        self._timeStart = dtime.time()
        self._allDay = False

    def setDuration(self, duration):
        """
        Sets the duration for this item to the inputed duration.
        
        :param      duration | <int>
        """
        if duration < 1:
            return False

        self.setDateEnd(self.dateStart().addDays(duration - 1))
        return True

    def setItemStyle(self, itemStyle):
        """
        Sets the item style that will be used for this widget.  If you are
        trying to set a style on an item that has children, make sure to turn 
        off the useGroupStyleWithChildren option, or it will always display as
        a group.
        
        :param      itemStyle | <XGanttWidgetItem.ItemStyle>
        """
        self._itemStyle = itemStyle

        # initialize the group icon for group style
        if itemStyle == XGanttWidgetItem.ItemStyle.Group and \
           self.icon(0).isNull():
            ico = projexui.resources.find('img/folder_close.png')
            expand_ico = projexui.resources.find('img/folder_open.png')
            self.setIcon(0, QIcon(ico))
            self.setExpandedIcon(0, QIcon(expand_ico))

    def setName(self, name):
        """
        Sets the name of this widget item to the inputed name.
        
        :param      name | <str>
        """
        self._name = name

        tree = self.treeWidget()
        if tree:
            col = tree.column('Name')
            if col != -1:
                self.setData(col, Qt.EditRole, wrapVariant(name))

    def setProperty(self, key, value):
        """
        Sets the custom property for this item's key to the inputed value.  If
        the widget has a column that matches the inputed key, then the value 
        will be added to the tree widget as well.
        
        :param      key     | <str>
                    value   | <variant>
        """
        if key == 'Name':
            self.setName(value)
        elif key == 'Start':
            self.setDateStart(value)
        elif key == 'End':
            self.setDateEnd(value)
        elif key == 'Calendar Days':
            self.setDuration(value)
        elif key == 'Time Start':
            self.setTimeStart(value)
        elif key == 'Time End':
            self.setTimeEnd(value)
        elif key == 'All Day':
            self.setAllDay(value)
        elif key == 'Workadys':
            pass
        else:
            self._properties[nativestring(key)] = value

            tree = self.treeWidget()
            if tree:
                col = tree.column(key)
                if col != -1:
                    self.setData(col, Qt.EditRole, wrapVariant(value))

    def setTimeEnd(self, time):
        """
        Sets the ending time that this item will use.  To properly use a timed
        item, you need to also set this item's all day property to False.
        
        :sa         setAllDay
        
        :param      time | <QTime>
        """
        self._timeEnd = time
        self._allDay = False

    def setTimeStart(self, time):
        """
        Sets the starting time that this item will use.  To properly use a timed
        item, you need to also set this item's all day property to False.
        
        :sa         setAllDay
        
        :param      time | <QTime>
        """
        self._timeStart = time
        self._allDay = False

    def setUseGroupStyleWithChildren(self, state):
        """
        Sets whether or not this item should display as group style when 
        it has children.  This will override whatever is set in the style
        property for the item.
        
        :return     <bool>
        """
        self._useGroupStyleWithChildren = state

    def sync(self, recursive=False):
        """
        Syncs the information from this item to the tree and view.
        """
        self.syncTree(recursive=recursive)
        self.syncView(recursive=recursive)

    def syncDependencies(self, recursive=False):
        """
        Syncs the dependencies for this item to the view.
        
        :param      recurisve | <bool>
        """
        scene = self.viewItem().scene()
        if not scene:
            return

        visible = self.viewItem().isVisible()
        depViewItems = self._dependencies.values()
        depViewItems += self._reverseDependencies.values()

        for depViewItem in depViewItems:
            if not depViewItem.scene():
                scene.addItem(depViewItem)

            depViewItem.rebuild()
            depViewItem.setVisible(visible)

        if recursive:
            for c in range(self.childCount()):
                self.child(c).syncDependencies(recursive=True)

    def syncTree(self, recursive=False, blockSignals=True):
        """
        Syncs the information from this item to the tree.
        """
        tree = self.treeWidget()

        # sync the tree information
        if not tree:
            return

        items = [self]
        if recursive:
            items += list(self.children(recursive=True))

        if blockSignals and not tree.signalsBlocked():
            blocked = True
            tree.blockSignals(True)
        else:
            blocked = False

        date_format = self.ganttWidget().dateFormat()

        for item in items:
            for c, col in enumerate(tree.columns()):
                value = item.property(col, '')
                item.setData(c, Qt.EditRole, wrapVariant(value))

        if blocked:
            tree.blockSignals(False)

    def syncView(self, recursive=False):
        """
        Syncs the information from this item to the view.
        """
        # update the view widget
        gantt = self.ganttWidget()
        tree = self.treeWidget()

        if not gantt:
            return

        vwidget = gantt.viewWidget()
        scene = vwidget.scene()
        cell_w = gantt.cellWidth()

        tree_offset_y = tree.header().height() + 1
        tree_offset_y += tree.verticalScrollBar().value()

        # collect the items to work on
        items = [self]
        if recursive:
            items += list(self.children(recursive=True))

        for item in items:
            # grab the view item from the gantt item
            vitem = item.viewItem()
            if not vitem.scene():
                scene.addItem(vitem)

            # make sure the item should be visible
            if item.isHidden() or not tree:
                vitem.hide()
                continue

            vitem.show()
            tree_rect = tree.visualItemRect(item)

            tree_y = tree_rect.y() + tree_offset_y
            tree_h = tree_rect.height()

            # check to see if this item is hidden
            if tree_rect.height() == 0:
                vitem.hide()
                continue

            if gantt.timescale() in (gantt.Timescale.Minute,
                                     gantt.Timescale.Hour,
                                     gantt.Timescale.Day):
                dstart = item.dateTimeStart()
                dend = item.dateTimeEnd()
                view_x = scene.datetimeXPos(dstart)
                view_r = scene.datetimeXPos(dend)
                view_w = view_r - view_x
            else:
                view_x = scene.dateXPos(item.dateStart())
                view_w = item.duration() * cell_w

                # determine the % off from the length based on this items time
                if not item.isAllDay():
                    full_day = 24 * 60 * 60  # full days worth of seconds

                    # determine the start offset
                    start = item.timeStart()
                    start_day = (start.hour() * 60 * 60)
                    start_day += (start.minute() * 60)
                    start_day += (start.second())

                    offset_start = (start_day / float(full_day)) * cell_w

                    # determine the end offset
                    end = item.timeEnd()
                    end_day = (end.hour() * 60 * 60)
                    end_day += (start.minute() * 60)
                    end_day += (start.second() + 1)  # forces at least 1 sec

                    offset_end = ((full_day - end_day) / float(full_day))
                    offset_end *= cell_w

                    # update the xpos and widths
                    view_x += offset_start
                    view_w -= (offset_start + offset_end)

            view_w = max(view_w, 5)

            vitem.setSyncing(True)
            vitem.setPos(view_x, tree_y)
            vitem.setRect(0, 0, view_w, tree_h)
            vitem.setSyncing(False)

            # setup standard properties
            flags = vitem.ItemIsSelectable
            flags |= vitem.ItemIsFocusable

            if item.flags() & Qt.ItemIsEditable:
                flags |= vitem.ItemIsMovable

            vitem.setFlags(flags)
            item.syncDependencies()

    def takeChild(self, index):
        """
        Removes the child at the given index from this item.
        
        :param      index | <int>
        """
        item = super(XGanttWidgetItem, self).takeChild(index)

        if item:
            item.removeFromScene()

        return item

    def takeDependency(self, item):
        """
        Removes the dependency between the this item and the inputed target.
        
        :param      item | <XGanttWidgetItem>
        """
        if (not item in self._dependencies):
            return

        item._reverseDependencies.pop(self)
        viewItem = self._dependencies.pop(item)

        scene = viewItem.scene()
        if (scene):
            scene.removeItem(viewItem)

    def timeEnd(self):
        """
        Returns the ending time that will be used for this item.  If it is an
        all day event, then the time returned will be 23:59:59.
        
        :return     <QTime>
        """
        if (self.isAllDay()):
            return QTime(23, 59, 59)
        return self._timeEnd

    def timeStart(self):
        """
        Returns the starting time that will be used for this item.  If it is
        an all day event, then the time returned will be 0:0:0
        
        :return     <QTime>
        """
        if (self.isAllDay()):
            return QTime(0, 0, 0)

        return self._timeStart

    def useGroupStyleWithChildren(self):
        """
        Returns whether or not this item should display as group style when 
        it has children.  This will override whatever is set in the style
        property for the item.
        
        :return     <bool>
        """
        return self._useGroupStyleWithChildren

    def viewChanged(self, start, end):
        """
        Called when the view item is changed by the user.
        
        :param      start | <QDate> || <QDateTime>
                    end   | <QDate> || <QDateTime>
        """
        if type(start) == QDate:
            delta = self._dateStart.daysTo(start)
            self._dateStart = start
            self._dateEnd = end
            self.adjustChildren(delta)

        else:
            delta = self._dateStart.secsTo(start)
            self._dateStart = start.date()
            self._timeStart = start.time()

            self._dateEnd = end.date()
            self._timeEnd = end.time()

            self.adjustChildren(delta, secs=True)

        self.adjustRange()

        self.syncDependencies()
        self.syncTree()

    def viewItem(self):
        """
        Returns the view item that is linked with this item.
        
        :return     <XGanttViewItem>
        """
        if type(self._viewItem).__name__ == 'weakref':
            return self._viewItem()
        return self._viewItem

    def weekdays(self):
        """
        Returns the number of weekdays this item has.
        
        :return     <int>
        """
        if self.itemStyle() == self.ItemStyle.Group:
            out = 0
            for i in range(self.childCount()):
                out += self.child(i).weekdays()
            return out
        else:
            dstart = self.dateStart().toPyDate()
            dend = self.dateEnd().toPyDate()

            return projex.dates.weekdays(dstart, dend)
Exemple #30
0
class SearchThesaurus(AddonManager):
    """
    Defines a global thesaurus system for searching.  This will allow
    additional keyword lookups based on synonyms.  Thesaurus can be
    define on a per API and per Table system.
    """
    Flags = enum('FindSingular', 'FindPlural', 'FindInherited')
    
    def __init__(self, wordsets=None, parent=None):
        self._wordsets = []
        self._phrases = set()
        self._parent = parent
        
        if wordsets is not None:
            self.update(wordsets)

    def add(self, word, synonym):
        """
        Adds a new synonym for a given word.
        
        :param      word    | <str>
                    synonym | <str>
        """
        word = nstr(word)
        synonym = nstr(synonym)
        
        # lookup wordsets 
        for wordset in self._wordsets:
            if word in wordset:
                wordset.add(synonym)
        
        # define new wordset
        self._wordsets.append({word, synonym})

    def addset(self, wordset):
        """
        Defines a wordset that will be used as a grouping of synonyms
        within the thesaurus.  A wordset is a comma separated list of
        synonyms.
        
        :param      wordset | <str> || <list> || <set>
        """
        if type(wordset) in (list, tuple, set):
            wordset = set([nstr(word) for word in wordset])
        else:
            wordset = set(nstr(wordset).split(','))

        self._wordsets.append(wordset)

    def addPhrase(self, pattern):
        """
        Phrases define groups of words that will create a singular
        search term within a search pattern.  For instance, "is not" will be 
        treated as single term, so instead of looking for "is" and "not", the
        phrase "is not" will be matched.  The inputted pattern can be a regular
        expression, or hard set of terms.
        
        :param      pattern | <str>
        """
        self._phrases.add(pattern)

    def clear(self):
        """
        Clears out the data for this thesaurus.
        """
        self._phrases = set()
        self._wordsets = []

    def expand(self, wordset, flags=Flags.all()):
        """
        Expands a given wordset based on the synonyms per word in the set.
        
        :param      wordset | <list> || <tuple> || <set>
        """
        output = set()
        for word in wordset:
            output = output.union(self.synonyms(word, flags))
        return output

    def parent(self):
        """
        Returns the parent thesaurus for this instance if any is defined.
        
        :return     <orb.SearchThesaurus>
        """
        return self._parent

    def phrases(self):
        """
        Returns a list of phrases for the searching pattern.
        
        :return     [<str>, ..]
        """
        out = set(self._phrases)
        if self.parent():
            out = out.union(self.parent().phrases())
        return out

    def load(self, xml):
        """
        Loads the thesaurus information from the inputted XML file.
        
        :param      filename | <str>
        """
        try:
            xroot = ElementTree.parse(xml).getroot()
        except StandardError:
            try:
                xroot = ElementTree.fromstring(xml)
            except StandardError:
                return False

        # load wordsets
        xwordsets = xroot.find('wordsets')
        if xwordsets is not None:
            for xwordset in xwordsets:
                self.addset(xwordset.text)
        
        # load patterns
        xphrases = xroot.find('phrases')
        if xphrases is not None:
            for xphrase in xphrases:
                self.addPhrase(xphrase.text)

        return True

    def remove(self, word, synonym):
        """
        Removes a given synonym from the inputted word in a wordset.
        
        :param      word    | <str>
                    synonym | <str>
        """
        word = nstr(word)
        synonym = nstr(synonym)
        
        for wordset in self._wordsets:
            if word in wordset:
                try:
                    wordset.remove(synonym)
                except KeyError:
                    pass
                return

    def removePhrase(self, pattern):
        """
        Removes the given phrasing pattern for this search term.
        
        :param      pattern | <str>
        """
        try:
            self._phrases.remove(pattern)
        except KeyError:
            pass

    def setParent(self, parent):
        """
        Sets the parent thesaurus for this instance if any is defined.
        
        :param      parent | <orb.SearchThesaurus> || None
        """
        self._parent = parent

    def splitterms(self, text):
        """
        Splits the inputted search text into search terms.  This will use the
        phrasing patterns within this thesaurus to determine groups of words.
        
        :param      text | <str>
        
        :return     [<str>, ..]
        """
        text = nstr(text)
        repl = []
        
        # pre-process all phrases into their own groups
        for phrase in self.phrases():
            grp = re.search(phrase, text)
            while grp and grp.group():
                result = grp.group()
                text = text.replace(result, '`REGEXGRP{0}`'.format(len(repl)))
                repl.append(result)
                grp = re.search(phrase, text)
        
        # split the terms into their own words, grouped together with phrases
        output = []
        for term in text.split():
            term = term.strip()
            grp = re.match('`REGEXGRP(\d+)`(.*)', term)
            if grp:
                index, remain = grp.groups()
                term = repl[int(index)] + remain
            
            output.append(term)
        
        return output

    def synonyms(self, word, flags=Flags.all()):
        """
        Looks up the synonyms for the given word within this thesaurus
        system.
        
        :param      word | <str>
        
        :return     set(<str>, ..)
        """
        word = nstr(word)
        output = {word}
        
        # find matching words
        for wordset in self._wordsets:
            if output.intersection(wordset):
                output = output.union(wordset)
        
        # lookup inherited synonyms
        if self.parent() and flags & SearchThesaurus.Flags.FindInherited:
            output = output.union(self.parent().synonyms(word, flags=flags))
        
        return output
    
    def update(self, wordsets):
        """
        Updates the records for this thesaurus' wordsets with the inputted
        list of sets.
        
        :param      wordsets | [<str>, ..]
        """
        for wordset in wordsets:
            self.addset(wordset)

    def updatePhrases(self, phrases):
        """
        Updates the phrase sets for this thesaurus with the inputted list
        of phrases.
        
        :param      phrases | [<str>, ..]
        """
        for phrase in phrases:
            self.addPhrase(phrase)
Exemple #31
0
ColumnType = enum(
    # simple types
    'Bool',
    'Decimal',
    'Double',
    'Integer',  # 32-bit integer
    'BigInt',  # 64-bit integer
    'Enum',  # equates to an integer in databases, but links to an enum

    # string types
    'String',  # used for limited char sets
    'Text',  # used for larger string data
    'Url',  # similar to String, but uses url regex validation
    'Email',  # similar to String, but uses email regex validation
    'Password',  # similar to String, but uses password regex validation
    'Filepath',  # similar to String, but uses filepath regex validation
    'Directory',  # similar to String, but uses directory regex validation
    'Xml',  # similar to Text,   but auto-escape data stored
    'Html',  # similar to Text,   but store rich text data & auto-escape
    'Color',  # similar to String, stores the HEX value of a color (#ff00)

    # date/time types
    'Datetime',
    'Date',
    'Interval',
    'Time',
    'DatetimeWithTimezone',
    'Timestamp',
    'Timestamp_UTC',

    # data types
    'Image',  # stores images in the database as binary
    'ByteArray',  # stores additional binary information
    'Dict',  # stores python dictionary types
    'Pickle',  # stores python pickle data
    'Yaml',  # stores python data as yaml (requires PyYaml)
    'JSON', # stores python data as JSON
    'Query',  # stores an orb.Query class as xml

    # relation types
    'ForeignKey',  # tells the system to use the relation's information
)
                                  QGraphicsPathItem, \
                                  QPainterPath, \
                                  QPen, \
                                  QPolygonF, \
                                  QTransform,\
                                  QApplication,\
                                  QGraphicsTextItem,\
                                  QFontMetrics

from projex.enum    import enum

from projexui.widgets.xnodewidget.xnodelayer      import XNodeLayer

# create the common enumerated types needed
XConnectionStyle        = enum( 'Linear',
                                'Block',
                                'Smooth')
                                
XConnectionLocation     = enum( 'Top',
                                'Bottom',
                                'Left',
                                'Right' )

class XNodeConnection( QGraphicsPathItem ):
    """ 
    Defines the base graphics item class that is used to draw a connection
    between two nodes.
    """
    def __init__( self, scene ):
        self._visible               = True
        
Exemple #33
0
class QueryCompound(object):
    """ Defines combinations of queries via either the AND or OR mechanism. """
    Op = enum('And', 'Or')

    def __contains__(self, value):
        """
        Returns whether or not the query compound contains a query for the
        inputted column name.
        
        :param      value | <variant>
        
        :return     <bool>
        
        :usage      |>>> from orb import Query as Q
                    |>>> q = Q('testing') == True
                    |>>> 'testing' in q
                    |True
                    |>>> 'name' in q
                    |False
        """
        for query in self._queries:
            if value in query:
                return True
        return False

    def __nonzero__(self):
        return not self.isNull()

    def __str__(self):
        """
        Returns the string representation for this query instance
        
        :sa         toString
        """
        return self.toString()

    def __init__(self, *queries, **options):
        self._queries = queries
        self._op = options.get('op', QueryCompound.Op.And)
        self._name = nstr(options.get('name', ''))

    def __and__(self, other):
        """
        Creates a new compound query using the 
        QueryCompound.Op.And type.
        
        :param      other   <Query> || <QueryCompound>
        
        :return     <QueryCompound>
        
        :sa         and_
        
        :usage      |>>> from orb import Query as Q
                    |>>> query = (Q('test') != 1) & (Q('name') == 'Eric')
                    |>>> print query
                    |(test does_not_equal 1 and name is Eric)
        """
        return self.and_(other)

    def __hash__(self):
        return hash(self.toXmlString())

    def __neg__(self):
        """
        Negates the current state of the query.
        
        :sa     negate
        
        :return     self
        
        :usage      |>>> from orb import Query as Q
                    |>>> query = (Q('test') == 1) & (Q('name') == 'Eric')
                    |>>> print -query
                    |NOT (test is  1 and name is Eric)
        """
        return self.negated()

    def __or__(self, other):
        """
        Creates a new compound query using the 
        QueryCompound.Op.Or type.
        
        :param      other   <Query> || <QueryCompound>
        
        :return     <QueryCompound>
        
        :sa         or_
        
        :usage      |>>> from orb import Query as Q
                    |>>> query = (Q('test') != 1) | (Q('name') == 'Eric')
                    |>>> print query
                    |(test isNot 1 or name is Eric)
        """
        return self.or_(other)

    def and_(self, other):
        """
        Creates a new compound query using the 
        QueryCompound.Op.And type.
        
        :param      other   <Query> || <QueryCompound>
        
        :return     <QueryCompound>
        
        :sa         __and__
        
        :usage      |>>> from orb import Query as Q
                    |>>> query = (Q('test') != 1).and_((Q('name') == 'Eric')
                    |>>> print query
                    |(test isNot 1 and name is Eric)
        """
        if other is None or other.isNull():
            return self

        elif self.isNull():
            return other

        # grow this objects list if the operator types are the same
        if self.operatorType() == QueryCompound.Op.And:
            queries = list(self._queries)
            queries.append(other)
            opts = {'op': QueryCompound.Op.And}

            return QueryCompound(*queries, **opts)

        # create a new compound
        return QueryCompound(self, other, op=QueryCompound.Op.And)

    def copy(self):
        """
        Returns a copy of this query compound.
        
        :return     <QueryCompound>
        """
        out = QueryCompound()
        out._queries = [q.copy() for q in self._queries]
        out._op = self._op
        return out

    def columns(self, schema=None):
        """
        Returns any columns used within this query.
        
        :return     [<orb.Column>, ..]
        """
        output = []
        for query in self.queries():
            output += query.columns(schema=schema)
        return list(set(output))

    def expandShortcuts(self, basetable=None):
        """
        Expands any shortcuts that were created for this query.  Shortcuts
        provide the user access to joined methods using the '.' accessor to
        access individual columns for referenced tables.
        
        :param      basetable | <orb.Table> || None
        
        :usage      |>>> from orb import Query as Q
                    |>>> # lookup the 'username' of foreign key 'user'
                    |>>> Q('user.username') == 'bob.smith'
        
        :return     <orb.Query> || <orb.QueryCompound>
        """
        output = self.copy()
        queries = []
        rset = None

        for query in output._queries:
            query = query.expandShortcuts(basetable)

            # chain together joins into sub-queries
            if isinstance(query, orb.Query) and \
               isinstance(query.value(), orb.Query) and \
               query.value().table(basetable) != query.table(basetable):
                columns = [query.value().columnName()
                           ] if query.value().columnName() else ['id']
                new_rset = query.value().table(basetable).select(
                    columns=columns)
                query = query.copy()
                query.setOperatorType(query.Op.IsIn)
                query.setValue(new_rset)

                if rset is not None and rset.table() == query.table(basetable):
                    rset.setQuery(query & rset.query())
                else:
                    queries.append(query)

                rset = new_rset

            # update the existing recordset in the chain
            elif rset is not None and \
                    ((isinstance(query, orb.Query) and rset.table() == query.table(basetable)) or
                     (isinstance(query, orb.QueryCompound) and rset.table() in query.tables(basetable))):
                rset.setQuery(query & rset.query())

            # clear out the chain and move on to the next query set
            else:
                rset = None
                queries.append(query)

        output._queries = queries
        return output

    def findValue(self, column, instance=1):
        """
        Looks up the value for the inputted column name for the given instance.
        If the instance == 1, then this result will return the value and a
        0 instance count, otherwise it will decrement the instance for a
        matching column to indicate it was found, but not at the desired
        instance.
        
        :param      column   | <str>
                    instance | <int>
        
        :return     (<bool> success, <variant> value, <int> instance)
        """
        for query in self.queries():
            success, value, instance = query.findValue(column, instance)
            if success:
                return success, value, 0
        return False, None, instance

    def isNull(self):
        """
        Returns whether or not this join is empty or not.
        
        :return     <bool>
        """
        am_null = True
        for query in self._queries:
            if not query.isNull():
                am_null = False
                break

        return am_null

    def name(self):
        return self._name

    def negated(self):
        """
        Negates this instance and returns it.
        
        :return     self
        """
        qcompound = QueryCompound(*self._queries)
        qcompound._op = QueryCompound.Op.And if self._op == QueryCompound.Op.Or else QueryCompound.Op.Or
        return qcompound

    def operatorType(self):
        """
        Returns the operator type for this compound.
        
        :return     <QueryCompound.Op>
        """
        return self._op

    def or_(self, other):
        """
        Creates a new compound query using the 
        QueryCompound.Op.Or type.
        
        :param      other   <Query> || <QueryCompound>
        
        :return     <QueryCompound>
        
        :sa         or_
        
        :usage      |>>> from orb import Query as Q
                    |>>> query = (Q('test') != 1).or_(Q('name') == 'Eric')
                    |>>> print query
                    |(test isNot 1 or name is Eric)
        """
        if other is None or other.isNull():
            return self

        elif self.isNull():
            return other

        # grow this objects list if the operator types are the same
        if self.operatorType() == QueryCompound.Op.Or:
            queries = list(self._queries)
            queries.append(other)
            opts = {'op': QueryCompound.Op.Or}

            return QueryCompound(*queries, **opts)

        return QueryCompound(self, other, op=QueryCompound.Op.Or)

    def queries(self):
        """
        Returns the list of queries that are associated with
        this compound.
        
        :return     <list> [ <Query> || <QueryCompound>, .. ]
        """
        return self._queries

    def removed(self, columnName):
        """
        Removes the query containing the inputted column name from this
        query set.
        
        :param      columnName | <str>
        
        :return     <QueryCompound>
        """
        out = self.copy()
        new_queries = []
        for query in out._queries:
            new_queries.append(query.removed(columnName))

        out._queries = new_queries
        return out

    def setName(self, name):
        self._name = nstr(name)

    def setOperatorType(self, op):
        """
        Sets the operator type that this compound that will be
        used when joining together its queries.
        
        :param      op      <QueryCompound.Op>
        """
        self._op = op

    def tables(self, base=None):
        """
        Returns the tables that this query is referencing.
        
        :return     [ <subclass of Table>, .. ]
        """
        output = []
        for query in self._queries:
            output += query.tables(base=base)

        return list(set(output))

    def toString(self):
        """
        Returns this query instance as a semi readable language
        query.
        
        :warning    This method will NOT return a valid SQL statement.  The
                    backend classes will determine how to convert the Query
                    instance to whatever lookup code they need it to be.
        
        :return     <str>
        """
        optypestr = QueryCompound.Op[self.operatorType()]
        op_type = ' %s ' % projex.text.underscore(optypestr)
        query = '(%s)' % op_type.join([q.toString() for q in self.queries()])
        return query

    def toDict(self):
        """
        Creates a dictionary representation of this query.
        
        :return     <dict>
        """
        output = {}

        if self.isNull():
            return output

        output['type'] = 'compound'
        output['name'] = self.name()
        output['op'] = self.operatorType()

        queries = []
        for query in self.queries():
            queries.append(query.toDict())

        output['queries'] = queries
        return output

    def toXml(self, xparent=None):
        """
        Returns this query as an XML value.
        
        :param      xparent | <xml.etree.ElementTree.Element> || None
        
        :return     <xml.etree.ElementTree.Element>
        """
        if self.isNull():
            return None

        if xparent is None:
            xquery = ElementTree.Element('compound')
        else:
            xquery = ElementTree.SubElement(xparent, 'compound')

        xquery.set('name', nstr(self.name()))
        xquery.set('op', nstr(self.operatorType()))

        for query in self.queries():
            query.toXml(xquery)

        return xquery

    def toXmlString(self, indented=False):
        """
        Returns this query as an XML string.
        
        :param      indented | <bool>
        
        :return     <str>
        """
        xml = self.toXml()
        if indented:
            projex.text.xmlindent(xml)

        return ElementTree.tostring(xml)

    def validate(self, record, table=None):
        """
        Validates the inputted record against this query compound.
        
        :param      record | <orb.Table>
        """
        op = self._op
        queries = self.queries()

        if not queries:
            return False

        for query in queries:
            valid = query.validate(record, table)

            if op == QueryCompound.Op.And and not valid:
                return False
            elif op == QueryCompound.Op.Or and valid:
                return True

        return op == QueryCompound.Op.And

    @staticmethod
    def build(compound, queries):
        """
        Builds a compound based on the inputted compound string.  This should
        look like: ((QUERY_1 and QUERY_2) or (QUERY_3 and QUERY_4)).  The 
        inputted query dictionary should correspond with the keys in the string.
        
        This method will be called as part of the Query.fromString method and 
        probably never really needs to be called otherwise.
        
        :param      compound | <str>
                    queries  | [<Query>, ..]
        
        :return     <Query> || <QueryCompound>
        """
        indexStack = []
        compounds = {}
        new_text = projex.text.decoded(compound)

        for index, char in enumerate(projex.text.decoded(compound)):
            # open a new compound
            if char == '(':
                indexStack.append(index)

            # close a compound
            elif char == ')' and indexStack:
                openIndex = indexStack.pop()
                match = compound[openIndex + 1:index]

                if not match:
                    continue

                # create the new compound
                new_compound = QueryCompound.build(match, queries)

                key = 'QCOMPOUND_%i' % (len(compounds) + 1)
                compounds[key] = new_compound

                new_text = new_text.replace('(' + match + ')', key)

        new_text = new_text.strip('()')
        query = orb.Query()
        last_op = 'and'
        for section in new_text.split():
            section = section.strip('()')

            # merge a compound
            if section in compounds:
                section_q = compounds[section]

            elif section in queries:
                section_q = queries[section]

            elif section in ('and', 'or'):
                last_op = section
                continue

            else:
                log.warning('Missing query section: %s', section)
                continue

            if query is None:
                query = section_q
            elif last_op == 'and':
                query &= section_q
            else:
                query |= section_q

        return query

    @staticmethod
    def fromDict(data):
        if data.get('type') != 'compound':
            return orb.Query.fromDict(data)

        compound = QueryCompound()
        compound.setName(data.get('name', ''))
        compound.setOperatorType(int(data.get('op', '1')))

        queries = []
        for subdata in data.get('queries', []):
            queries.append(orb.Query.fromDict(subdata))

        compound._queries = queries
        return compound

    @staticmethod
    def fromString(querystr):
        """
        Returns a new compound from the inputted query string.  This simply calls
        the Query.fromString method, as the two work the same.
        
        :param      querystr | <str>
        
        :return     <Query> || <QueryCompound> || None
        """
        return orb.Query.fromString(querystr)

    @staticmethod
    def fromXml(xquery):
        if xquery.tag == 'query':
            return orb.Query.fromXml(xquery)

        compound = QueryCompound()
        compound.setName(xquery.get('name', ''))
        compound.setOperatorType(int(xquery.get('op', '1')))

        queries = []
        for xsubquery in xquery:
            queries.append(orb.Query.fromXml(xsubquery))

        compound._queries = queries
        return compound

    @staticmethod
    def fromXmlString(xquery_str):
        """
        Returns a query from the XML string.
        
        :param      xquery_str | <str>
        
        :return     <orb.Query> || <orb.QueryCompound>
        """
        try:
            xml = ElementTree.fromstring(xquery_str)
        except ExpatError:
            return orb.Query()

        return orb.Query.fromXml(xml)

    @staticmethod
    def typecheck(obj):
        """
        Returns whether or not the inputted object is a QueryCompound object.
        
        :param      obj     <variant>
        
        :return     ,bool>
        """
        return isinstance(obj, QueryCompound)