Пример #1
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)
Пример #2
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 )
Пример #3
0
class XCommentEdit(XTextEdit):
    attachmentRequested = Signal()
    
    def __init__(self, parent=None):
        super(XCommentEdit, self).__init__(parent)
        
        # define custom properties
        self._attachments = {}
        self._showAttachments = True
        
        # create toolbar
        self._toolbar = QToolBar(self)
        self._toolbar.setMovable(False)
        self._toolbar.setFixedHeight(30)
        self._toolbar.setAutoFillBackground(True)
        self._toolbar.setFocusProxy(self)
        self._toolbar.hide()
        
        # create toolbar buttons
        self._attachButton = QToolButton(self)
        self._attachButton.setIcon(QIcon(resources.find('img/attach.png')))
        self._attachButton.setToolTip('Add Attachment')
        self._attachButton.setAutoRaise(True)
        self._attachButton.setIconSize(QSize(24, 24))
        self._attachButton.setFixedSize(26, 26)
        
        self._submitButton = QPushButton(self)
        self._submitButton.setText('Submit')
        self._submitButton.setFocusProxy(self)
        
        # create attachments widget
        self._attachmentsEdit = XMultiTagEdit(self)
        self._attachmentsEdit.setAutoResizeToContents(True)
        self._attachmentsEdit.setFrameShape(XMultiTagEdit.NoFrame)
        self._attachmentsEdit.setViewMode(XMultiTagEdit.ListMode)
        self._attachmentsEdit.setEditable(False)
        self._attachmentsEdit.setFocusProxy(self)
        self._attachmentsEdit.hide()
        
        # define toolbar layout
        spacer = QWidget(self)
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        
        self._attachAction = self._toolbar.addWidget(self._attachButton)
        self._toolbar.addWidget(spacer)
        self._toolbar.addWidget(self._submitButton)
        
        # set standard properties
        self.setAutoResizeToContents(True)
        self.setHint('add comment')
        self.setFocusPolicy(Qt.StrongFocus)
        self.setRequireShiftForNewLine(True)
        
        # create connections
        self._attachButton.clicked.connect(self.attachmentRequested)
        self._submitButton.clicked.connect(self.acceptText)
        self._attachmentsEdit.tagRemoved.connect(self.removeAttachment)
        self.focusChanged.connect(self.setToolbarVisible)
    
    def addAttachment(self, title, attachment):
        """
        Adds an attachment to this comment.
        
        :param      title      | <str>
                    attachment | <variant>
        """
        self._attachments[title] = attachment
        self.resizeToContents()
    
    def attachments(self):
        """
        Returns a list of attachments that have been linked to this widget.
        
        :return     {<str> title: <variant> attachment, ..}
        """
        return self._attachments.copy()
    
    def attachButton(self):
        """
        Returns the attach button from the toolbar for this widget.
        
        :return     <QToolButton>
        """
        return self._attachButton
    
    @Slot()
    def clear(self):
        """
        Clears out this widget and its attachments.
        """
        # clear the attachment list
        self._attachments.clear()
        
        super(XCommentEdit, self).clear()
    
    def isToolbarVisible(self):
        """
        Returns whether or not the toolbar for this comment edit is currently
        visible to the user.
        
        :return     <bool>
        """
        return self._toolbar.isVisible()
    
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.clear()
            event.accept()
        else:
            super(XCommentEdit, self).keyPressEvent(event)
    
    @Slot()
    def pickAttachment(self):
        """
        Prompts the user to select an attachment to add to this edit.
        """
        filename = QFileDialog.getOpenFileName(self.window(),
                                               'Select Attachment',
                                               '',
                                               'All Files (*.*)')
        
        if type(filename) == tuple:
            filename = nativestring(filename[0])
        
        filename = nativestring(filename)
        if filename:
            self.addAttachment(os.path.basename(filename), filename)
    
    def removeAttachment(self, title):
        """
        Removes the attachment from the given title.
        
        :param      title | <str>
        
        :return     <variant> | attachment
        """
        attachment = self._attachments.pop(nativestring(title), None)
        
        if attachment:
            self.resizeToContents()
        
        return attachment
    
    def resizeEvent(self, event):
        super(XCommentEdit, self).resizeEvent(event)
        
        self._toolbar.resize(self.width() - 4, 30)
        edit = self._attachmentsEdit
        edit.resize(self.width() - 4, edit.height())
    
    def resizeToContents(self):
        """
        Resizes this toolbar based on the contents of its text.
        """
        if self._toolbar.isVisible():
            doc = self.document()
            h = doc.documentLayout().documentSize().height()
            
            offset = 34
            
            # update the attachments edit
            edit = self._attachmentsEdit
            if self._attachments:
                edit.move(2, self.height() - edit.height() - 31)
                edit.setTags(sorted(self._attachments.keys()))
                edit.show()
                
                offset = 34 + edit.height()
            else:
                edit.hide()
                offset = 34
            
            self.setFixedHeight(h + offset)
            self._toolbar.move(2, self.height() - 32)
        else:
            super(XCommentEdit, self).resizeToContents()
    
    def setAttachments(self, attachments):
        """
        Sets the attachments for this widget to the inputed list of attachments.
        
        :param      attachments | {<str> title: <variant> attachment, ..}
        """
        self._attachments = attachments
        self.resizeToContents()
    
    def setSubmitText(self, text):
        """
        Sets the submission text for this edit.
        
        :param      text | <str>
        """
        self._submitButton.setText(text)

    def setShowAttachments(self, state):
        """
        Sets whether or not to show the attachments for this edit.
        
        :param      state | <bool>
        """
        self._showAttachments = state
        self._attachAction.setVisible(state)

    def setToolbarVisible(self, state):
        """
        Sets whether or not the toolbar is visible.
        
        :param      state | <bool>
        """
        self._toolbar.setVisible(state)
        
        self.resizeToContents()
    
    def showAttachments(self):
        """
        Returns whether or not to show the attachments for this edit.
        
        :return     <bool>
        """
        return self._showAttachments
    
    def submitButton(self):
        """
        Returns the submit button for this edit.
        
        :return     <QPushButton>
        """
        return self._submitButton
    
    def submitText(self):
        """
        Returns the submission text for this edit.
        
        :return     <str>
        """
        return self._submitButton.text()

    def toolbar(self):
        """
        Returns the toolbar widget for this comment edit.
        
        :return     <QToolBar>
        """
        return self._toolbar
    
    x_showAttachments = Property(bool, showAttachments, setShowAttachments)
    x_submitText = Property(str, submitText, setSubmitText)
Пример #4
0
class XCommentEdit(XTextEdit):
    attachmentRequested = Signal()

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

        # define custom properties
        self._attachments = {}
        self._showAttachments = True

        # create toolbar
        self._toolbar = QToolBar(self)
        self._toolbar.setMovable(False)
        self._toolbar.setFixedHeight(30)
        self._toolbar.setAutoFillBackground(True)
        self._toolbar.setFocusProxy(self)
        self._toolbar.hide()

        # create toolbar buttons
        self._attachButton = QToolButton(self)
        self._attachButton.setIcon(QIcon(resources.find('img/attach.png')))
        self._attachButton.setToolTip('Add Attachment')
        self._attachButton.setAutoRaise(True)
        self._attachButton.setIconSize(QSize(24, 24))
        self._attachButton.setFixedSize(26, 26)

        self._submitButton = QPushButton(self)
        self._submitButton.setText('Submit')
        self._submitButton.setFocusProxy(self)

        # create attachments widget
        self._attachmentsEdit = XMultiTagEdit(self)
        self._attachmentsEdit.setAutoResizeToContents(True)
        self._attachmentsEdit.setFrameShape(XMultiTagEdit.NoFrame)
        self._attachmentsEdit.setViewMode(XMultiTagEdit.ListMode)
        self._attachmentsEdit.setEditable(False)
        self._attachmentsEdit.setFocusProxy(self)
        self._attachmentsEdit.hide()

        # define toolbar layout
        spacer = QWidget(self)
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

        self._attachAction = self._toolbar.addWidget(self._attachButton)
        self._toolbar.addWidget(spacer)
        self._toolbar.addWidget(self._submitButton)

        # set standard properties
        self.setAutoResizeToContents(True)
        self.setHint('add comment')
        self.setFocusPolicy(Qt.StrongFocus)
        self.setRequireShiftForNewLine(True)

        # create connections
        self._attachButton.clicked.connect(self.attachmentRequested)
        self._submitButton.clicked.connect(self.acceptText)
        self._attachmentsEdit.tagRemoved.connect(self.removeAttachment)
        self.focusChanged.connect(self.setToolbarVisible)

    def addAttachment(self, title, attachment):
        """
        Adds an attachment to this comment.
        
        :param      title      | <str>
                    attachment | <variant>
        """
        self._attachments[title] = attachment
        self.resizeToContents()

    def attachments(self):
        """
        Returns a list of attachments that have been linked to this widget.
        
        :return     {<str> title: <variant> attachment, ..}
        """
        return self._attachments.copy()

    def attachButton(self):
        """
        Returns the attach button from the toolbar for this widget.
        
        :return     <QToolButton>
        """
        return self._attachButton

    @Slot()
    def clear(self):
        """
        Clears out this widget and its attachments.
        """
        # clear the attachment list
        self._attachments.clear()

        super(XCommentEdit, self).clear()

    def isToolbarVisible(self):
        """
        Returns whether or not the toolbar for this comment edit is currently
        visible to the user.
        
        :return     <bool>
        """
        return self._toolbar.isVisible()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.clear()
            event.accept()
        else:
            super(XCommentEdit, self).keyPressEvent(event)

    @Slot()
    def pickAttachment(self):
        """
        Prompts the user to select an attachment to add to this edit.
        """
        filename = QFileDialog.getOpenFileName(self.window(),
                                               'Select Attachment', '',
                                               'All Files (*.*)')

        if type(filename) == tuple:
            filename = nativestring(filename[0])

        filename = nativestring(filename)
        if filename:
            self.addAttachment(os.path.basename(filename), filename)

    def removeAttachment(self, title):
        """
        Removes the attachment from the given title.
        
        :param      title | <str>
        
        :return     <variant> | attachment
        """
        attachment = self._attachments.pop(nativestring(title), None)

        if attachment:
            self.resizeToContents()

        return attachment

    def resizeEvent(self, event):
        super(XCommentEdit, self).resizeEvent(event)

        self._toolbar.resize(self.width() - 4, 30)
        edit = self._attachmentsEdit
        edit.resize(self.width() - 4, edit.height())

    def resizeToContents(self):
        """
        Resizes this toolbar based on the contents of its text.
        """
        if self._toolbar.isVisible():
            doc = self.document()
            h = doc.documentLayout().documentSize().height()

            offset = 34

            # update the attachments edit
            edit = self._attachmentsEdit
            if self._attachments:
                edit.move(2, self.height() - edit.height() - 31)
                edit.setTags(sorted(self._attachments.keys()))
                edit.show()

                offset = 34 + edit.height()
            else:
                edit.hide()
                offset = 34

            self.setFixedHeight(h + offset)
            self._toolbar.move(2, self.height() - 32)
        else:
            super(XCommentEdit, self).resizeToContents()

    def setAttachments(self, attachments):
        """
        Sets the attachments for this widget to the inputed list of attachments.
        
        :param      attachments | {<str> title: <variant> attachment, ..}
        """
        self._attachments = attachments
        self.resizeToContents()

    def setSubmitText(self, text):
        """
        Sets the submission text for this edit.
        
        :param      text | <str>
        """
        self._submitButton.setText(text)

    def setShowAttachments(self, state):
        """
        Sets whether or not to show the attachments for this edit.
        
        :param      state | <bool>
        """
        self._showAttachments = state
        self._attachAction.setVisible(state)

    def setToolbarVisible(self, state):
        """
        Sets whether or not the toolbar is visible.
        
        :param      state | <bool>
        """
        self._toolbar.setVisible(state)

        self.resizeToContents()

    def showAttachments(self):
        """
        Returns whether or not to show the attachments for this edit.
        
        :return     <bool>
        """
        return self._showAttachments

    def submitButton(self):
        """
        Returns the submit button for this edit.
        
        :return     <QPushButton>
        """
        return self._submitButton

    def submitText(self):
        """
        Returns the submission text for this edit.
        
        :return     <str>
        """
        return self._submitButton.text()

    def toolbar(self):
        """
        Returns the toolbar widget for this comment edit.
        
        :return     <QToolBar>
        """
        return self._toolbar

    x_showAttachments = Property(bool, showAttachments, setShowAttachments)
    x_submitText = Property(str, submitText, setSubmitText)
Пример #5
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 = 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._filepathEdit.setSizePolicy(QSizePolicy.Expanding,
                                         QSizePolicy.Expanding)
        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()
    
    @Slot()
    def clearFilepath( self ):
        """
        Clears the filepath contents for this path.
        """
        self.setFilepath('')
    
    @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 nativestring(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 = nativestring(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 = nativestring(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(nativestring(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)
    
    @Slot(int)
    def setFilepathMode( self, mode ):
        """
        Sets the filepath mode for this widget to the inputed mode.
        
        :param      mode | <XFilepathEdit.Mode>
        """
        self._filepathMode = mode
    
    @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[nativestring(text)])
            return True
        except KeyError:
            return False
    
    @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
    
    @Slot(str)
    def setFilepath(self, filepath):
        """
        Sets the filepath text for this widget to the inputed path.
        
        :param      filepath | <str>
        """
        if not filepath:
            self._filepathEdit.setText('')
            return
        
        if self.normalizePath():
            filepath = os.path.normpath(nativestring(filepath))
        else:
            filepath = os.path.normpath(nativestring(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(nativestring(hint))
        else:
            filepath = os.path.normpath(nativestring(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
    
    @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)
    
    @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         = Property(bool, autoRaise,     setAutoRaise)
    x_filepathTypes     = Property(str,  filepathTypes, setFilepathTypes)
    x_filepath          = Property(str,  filepath,      setFilepath)
    x_readOnly          = Property(bool, isReadOnly,    setReadOnly)
    x_validated         = Property(bool, isValidated,   setValidated)
    x_hint              = Property(str,  hint,          setHint)
    x_icon              = Property('QIcon', icon,       setIcon)
    x_normalizePath     = Property(bool, normalizePath, setNormalizePath)
    
    x_invalidForeground = Property('QColor',
                                        invalidForeground,
                                        setInvalidForeground)
    
    x_invalidBackground = Property('QColor',
                                        invalidBackground,
                                        setInvalidBackground)
    
    x_validForeground   = Property('QColor',
                                        validForeground,
                                        setValidForeground)
    
    x_validBackground   = Property('QColor',
                                        validBackground,
                                        setValidBackground)
    
    x_filepathModeText  = Property(str, 
                                       filepathModeText, 
                                       setFilepathModeText)