Exemple #1
0
class XIOHook(QObject):
    _instance = None

    printed = Signal(str)
    errored = Signal(str)

    @staticmethod
    def cleanup():
        # destroy the global hook instance
        hook = XIOHook._instance
        if not hook:
            return

        XIOHook._instance = None

        # disconnect Qt hooks
        try:
            hook.printed.disconnect()
        except RuntimeError:
            pass  # no connections have been made

        try:
            hook.errored.disconnect()
        except RuntimeError:
            pass  # no connections have been made

        hook.deleteLater()

        # unregister from python hooks
        hooks.unregisterStdOut(XIOHook.stdout)
        hooks.unregisterStdErr(XIOHook.stderr)

    @staticmethod
    def stdout(text):
        XIOHook.instance().printed.emit(text)

    @staticmethod
    def stderr(text):
        XIOHook.instance().errored.emit(text)

    @staticmethod
    def instance():
        if XIOHook._instance is None:
            XIOHook._instance = XIOHook()

            # create the hook registration
            hooks.registerStdOut(XIOHook.stdout)
            hooks.registerStdErr(XIOHook.stderr)

            QApplication.instance().aboutToQuit.connect(XIOHook.cleanup)

        return XIOHook._instance
Exemple #2
0
class XdkWorker(QObject):
    loadingFinished = Signal(str)

    def loadFile(self, filename):
        # creates the new XdkItem
        filename = nativestring(filename)
        basename = os.path.basename(filename)
        name = os.path.splitext(basename)[0]

        temp_dir = nativestring(QDir.tempPath())
        temp_path = os.path.join(temp_dir, 'xdk/%s' % name)

        # remove existing files from the location
        if os.path.exists(temp_path):
            try:
                shutil.rmtree(temp_path, True)
            except:
                pass

        # make sure we have the temp location available
        if not os.path.exists(temp_path):
            try:
                os.makedirs(temp_path)
            except:
                pass

        # extract the zip files to the temp location
        zfile = zipfile.ZipFile(filename, 'r')
        zfile.extractall(temp_path)
        zfile.close()

        # load the table of contents
        self.loadingFinished.emit(filename)
Exemple #3
0
class XTabBar(QTabBar):
    resized = Signal()

    def resizeEvent(self, event):
        """
        Updates the position of the additional buttons when this widget \
        resizes.
        
        :param      event | <QResizeEvet>
        """
        super(XTabBar, self).resizeEvent(event)
        self.resized.emit()
class XOrbPopupButton(XPopupButton):
    __designer_group__ = 'ProjexUI - ORB'

    saved = Signal()

    def setCentralWidget(self, widget, createsNew=True, autoCommit=True):
        """
        Sets the central widget for this popup button.  If createsNew is set
        to True, then the about to show signal from the popup will be linked to
        the widget's reset slot.  If autoCommit is set to True, then the widget
        will commit it's information to the database.
        
        :param      widget | <prjexui.widgets.xorbrecordwidget.XOrbRecordWidget>
                    createsNew | <bool>
                    autoCommit | <boo>
        
        :return     <bool> | success
        """
        if not isinstance(widget, XOrbRecordWidget):
            return False

        # assign the widget
        super(XOrbPopupButton, self).setCentralWidget(widget)

        # setup widget options
        widget.setAutoCommitOnSave(autoCommit)

        # setup popup options
        popup = self.popupWidget()
        popup.setAutoCloseOnAccept(False)

        if createsNew and widget.multipleCreateEnabled():
            btn = popup.addButton('Save && Create Another')
            btn.clicked.connect(widget.saveSilent)

        # create connections
        popup.accepted.connect(widget.save)
        widget.saved.connect(popup.close)
        widget.saved.connect(self.saved)

        if createsNew:
            popup.aboutToShow.connect(widget.reset)

        return True
Exemple #5
0
class XAnimatedIconHelper(QObject):
    iconChanged = Signal(QIcon)

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

        self._movie = movie

    def movie(self):
        """
        Returns the movie linked with this icon.
        
        :return     <QMovie>
        """
        return self._movie

    def isNull(self):
        """
        Returns whether or not this icon is a null icon.
        
        :return     <bool>
        """
        if (self._movie):
            return False
        return True

    def setMovie(self, movie):
        """
        Sets the movie for this icon.
        
        :param      movie | <QMovie> || None
        """
        if (self._movie):
            self._movie.frameChanged.disconnect(self.updateIcon)

        self._movie = movie

        if (self._movie):
            self._movie.frameChanged.connect(self.updateIcon)

    def updateIcon(self):
        if (self._movie):
            icon = QIcon(self._movie.currentPixmap())
            self.iconChanged.emit(icon)
class XPopupButton(QToolButton):
    popupAboutToShow = Signal()
    popupAccepted = Signal()
    popupRejected = Signal()
    popupReset = Signal()

    Anchor = XPopupWidget.Anchor

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

        # define custom options
        if buttons is None:
            buttons = QDialogButtonBox.Reset
            buttons |= QDialogButtonBox.Save
            buttons |= QDialogButtonBox.Cancel

        self._popupWidget = XPopupWidget(self, buttons)
        self._defaultAnchor = 0
        self._popupShown = False

        self.setEnabled(False)

        # create connections
        self.clicked.connect(self.clickAction)
        self.triggered.connect(self.togglePopupOnAction)

        self._popupWidget.accepted.connect(self.popupAccepted)
        self._popupWidget.rejected.connect(self.popupRejected)
        self._popupWidget.resetRequested.connect(self.popupReset)

    def centralWidget(self):
        """
        Returns the central widget from this tool button
        """
        return self._popupWidget.centralWidget()

    def clickAction(self):
        """
        Calls the triggered signal if there is no action for this widget.
        """
        if not self.defaultAction():
            self.triggered.emit(None)

    def defaultAnchor(self):
        """
        Returns the default anchor for this popup widget.
        
        :return     <XPopupWidget.Anchor>
        """
        return self._defaultAnchor

    def hideEvent(self, event):
        super(XPopupButton, self).hideEvent(event)

        self._popupShown = self.popupWidget().isVisible()
        if self.popupWidget().currentMode() != XPopupWidget.Mode.Dialog:
            self.popupWidget().hide()

    def popupWidget(self):
        """
        Returns the popup widget for this button.
        
        :return     <XPopupWidget>
        """
        return self._popupWidget

    def setCentralWidget(self, widget):
        """
        Sets the central widget for this button.
        
        :param      widget | <QWidget>
        """
        self.setEnabled(widget is not None)
        self._popupWidget.setCentralWidget(widget)

    def setDefaultAnchor(self, anchor):
        """
        Sets the default anchor for the popup on this button.
        
        :param      anchor | <XPopupWidget.Anchor>
        """
        self._defaultAnchor = anchor

    def showEvent(self, event):
        super(XPopupButton, self).showEvent(event)

        if self._popupShown and \
           self.popupWidget().currentMode() != XPopupWidget.Mode.Dialog:
            self.showPopup()

    def showPopupOnAction(self, action):
        """
        Shows the popup if the action is the current default action.
        
        :param      action | <QAction>
        """
        if (action == self.defaultAction()):
            self.showPopup()

    def showPopup(self):
        """
        Shows the popup for this button.
        """
        as_dialog = QApplication.keyboardModifiers()

        anchor = self.defaultAnchor()
        if anchor:
            self.popupWidget().setAnchor(anchor)
        else:
            anchor = self.popupWidget().anchor()

        if (anchor &
            (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter
             | XPopupWidget.Anchor.BottomRight)):
            pos = QPoint(self.width() / 2, 0)
        else:
            pos = QPoint(self.width() / 2, self.height())

        pos = self.mapToGlobal(pos)

        if not self.signalsBlocked():
            self.popupAboutToShow.emit()
        self._popupWidget.popup(pos)

        if as_dialog:
            self._popupWidget.setCurrentMode(XPopupWidget.Mode.Dialog)

    def togglePopup(self):
        """
        Toggles whether or not the popup is visible.
        """
        if not self._popupWidget.isVisible():
            self.showPopup()
        elif self._popupWidget.currentMode() != self._popupWidget.Mode.Dialog:
            self._popupWidget.close()

    def togglePopupOnAction(self, action):
        """
        Toggles the popup if the action is the current default action.
        
        :param      action | <QAction>
        """
        if action in (None, self.defaultAction()):
            self.togglePopup()
Exemple #7
0
class XScintillaEdit(QsciScintilla):
    """ Creates some convenience methods on top of the QsciScintilla class. """
    
    __designer_icon__ = projexui.resources.find('img/ui/codeedit.png')
    
    breakpointsChanged = Signal()
    fontSizeChanged    = Signal(int)
    
    def __init__( self, parent, filename = '', lineno = 0 ):
        super(XScintillaEdit, self).__init__(parent)
        
        # create custom properties
        self._filename              = ''
        self._marginsFont           = QFont()
        self._saveOnClose           = True
        self._colorSet              = None
        self._language              = None
        
        # define markers
        breakpoint_ico = projexui.resources.find('img/debug/break.png')
        debug_ico      = projexui.resources.find('img/debug/current.png')
        
        self._breakpointMarker      = self.markerDefine(QPixmap(breakpoint_ico))
        self._currentDebugMarker    = self.markerDefine(QPixmap(debug_ico))
        
        # set one time properties
        self.setFolding(            QsciScintilla.BoxedTreeFoldStyle )
        self.setBraceMatching(      QsciScintilla.SloppyBraceMatch )
        self.setContextMenuPolicy(  Qt.NoContextMenu )
        
        # load the inputed filename
        self.initOptions(XScintillaEditOptions())
        self.load(filename, lineno)
    
    def addBreakpoint( self, lineno = -1 ):
        """
        Adds a breakpoint for the given line number to this edit.
        
        :note       The lineno is 0-based, while the editor displays lines as
                    a 1-based system.  So, if you want to put a breakpoint at
                    visual line 3, you would pass in lineno as 2
        
        :param      lineno | <int>
        """
        if ( lineno == -1 ):
            lineno, colno = self.getCursorPosition()
            
        self.markerAdd(lineno, self._breakpointMarker)
        
        if ( not self.signalsBlocked() ):
            self.breakpointsChanged.emit()
    
    def breakpoints( self ):
        """
        Returns a list of lines that have breakpoints for this edit.
        
        :return     [<int>, ..]
        """
        lines = []
        result = self.markerFindNext(0, self._breakpointMarker + 1)
        while ( result != -1 ):
            lines.append(result)
            result = self.markerFindNext(result + 1, self._breakpointMarker + 1)
        return lines
    
    def colorSet( self ):
        """
        Returns the color set for this edit.
        
        :return     <XScintillaEditColorSet> || None
        """
        return self._colorSet
    
    def clearBreakpoints( self ):
        """
        Clears the file of all the breakpoints.
        """
        self.markerDeleteAll(self._breakpointMarker)
        
        if ( not self.signalsBlocked() ):
            self.breakpointsChanged.emit()
    
    def clearCurrentDebugLine( self ):
        """
        Clears the current debug line for this edit.
        """
        self.markerDeleteAll(self._currentDebugMarker)
    
    def closeEvent( self, event ):
        """
        Overloads the close event for this widget to make sure that the data \
        is properly saved before exiting.
        
        :param      event | <QCloseEvent>
        """
        if ( not (self.saveOnClose() and self.checkForSave()) ):
            event.ignore()
        else:
            super(XScintillaEdit, self).closeEvent(event)
    
    def checkForSave( self ):
        """
        Checks to see if the current document has been modified and should \
        be saved.
        
        :return     <bool>
        """
        # if the file is not modified, then save is not needed
        if ( not self.isModified() ):
            return True
        
        options     = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
        question    = 'Would you like to save your changes to %s?' % \
                                            self.windowTitle()
                                            
        answer      = QMessageBox.question( None, 
                                            'Save Changes', 
                                            question, 
                                            options )
        
        if ( answer == QMessageBox.Yes ):
            return self.save()
        elif ( answer == QMessageBox.Cancel ):
            return False
        return True
    
    def currentLine( self ):
        """
        Returns the current line number.
        
        :return     <int>
        """
        return self.getCursorPosition()[0]
    
    def filename( self ):
        """
        Returns the filename that is associated with this edit.
        
        :return     <str>
        """
        return self._filename
    
    def findNext( self, 
                  text, 
                  wholeWords    = False, 
                  caseSensitive = False, 
                  regexed       = False,
                  wrap          = True ):
        """
        Looks up the next iteration fot the inputed search term.
        
        :param      text            | <str>
                    wholeWords      | <bool>
                    caseSensitive   | <bool>
                    regexed         | <bool>
        
        :return     <bool>
        """
        return self.findFirst( text,
                               regexed,
                               caseSensitive,
                               wholeWords,
                               wrap,
                               True )
    
    def findPrev( self,
                  text,
                  wholeWords    = False,
                  caseSensitive = False,
                  regexed       = False,
                  wrap          = True ):
        """
        Looks up the previous iteration for the inputed search term.
        
        :param      text            | <str>
                    wholeWords      | <bool>
                    caseSensitive   | <bool>
                    regexed         | <bool>
                    wrap            | <bool>
        
        :return     <bool>
        """
        lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
        
        return self.findFirst( text,
                               regexed,
                               caseSensitive,
                               wholeWords,
                               wrap,
                               False,
                               lineFrom,
                               indexFrom )
                               
    def findRepl( self, 
                  text,
                  repl,
                  caseSensitive = False,
                  replaceAll    = False ):
        """
        Looks for the inputed text and replaces it with the given replacement \
        text.
        
        :param      text            | <str>
                    repl            | <str>
                    caseSensitive   | <bool>
                    replaceAll      | <bool>
        
        :return     <int> number of items replace
        """
        # make sure something is selected
        if ( not text ):
            return 0
        
        # make sure we have some text selected to replace
        if ( self.selectedText() != text ):
            found = self.findNext( text,
                                   False,
                                   caseSensitive,
                                   False,
                                   True )
            
            if ( not found ):
                return 0
        
        sel     = self.getSelection()
        alltext = self.text()
        
        # replace all instances
        if ( replaceAll ):
            sensitivity = Qt.CaseInsensitive
            if ( caseSensitive ):
                sensitivity = Qt.CaseSensitive
                
            count = alltext.count(text, sensitivity)
            alltext.replace(text, repl, sensitivity)
            
        else:
            count    = 1
            startpos = self.positionFromLineIndex(sel[0], sel[1])
            alltext.replace(startpos, len(text), repl)
            
        self.setText(alltext)
        
        if ( count == 1 ):
            sel = list(sel)
            sel[3] += len(repl) - len(text)
            
            self.setSelection(*sel)
        else:
            self.findNext( repl,
                           False,
                           caseSensitive,
                           False,
                           True )
        
        return count
        
    def initOptions( self, options ):
        """
        Initializes the edit with the inputed options data set.
        
        :param      options | <XScintillaEditOptions>
        """
        self.setAutoIndent(             options.value('autoIndent'))
        self.setIndentationsUseTabs(    options.value('indentationsUseTabs'))
        self.setTabIndents(             options.value('tabIndents'))
        self.setTabWidth(               options.value('tabWidth'))
        
        self.setCaretLineVisible(       options.value('showCaretLine'))
        self.setShowWhitespaces(        options.value('showWhitespaces'))
        self.setMarginLineNumbers( 0,   options.value('showLineNumbers'))
        self.setIndentationGuides(      options.value('showIndentations'))
        self.setEolVisibility(          options.value('showEndlines'))
        
        if ( options.value('showLimitColumn') ):
            self.setEdgeMode(self.EdgeLine)
            self.setEdgeColumn(options.value('limitColumn'))
        else:
            self.setEdgeMode(self.EdgeNone)
        
        if ( options.value('showLineWrap') ):
            self.setWrapMode(self.WrapWord)
        else:
            self.setWrapMode(self.WrapNone)
        
        # set the autocompletion source
        if ( options.value('autoComplete') ):
            self.setAutoCompletionSource(QsciScintilla.AcsAll)
        else:
            self.setAutoCompletionSource(QsciScintilla.AcsNone)
        
        self.setAutoCompletionThreshold(options.value('autoCompleteThreshold'))
        
        # update the font information
        font = options.value('documentFont')
        font.setPointSize(options.value('documentFontSize'))
        self.setFont(font)
        
        # udpate the lexer
        lexer = self.lexer()
        if ( lexer ):
            lexer.setFont(font)
        
        # create the margin font option
        mfont = options.value('documentMarginFont')
        mfont.setPointSize(font.pointSize() - 2)
        self.setMarginsFont( mfont )
        self.setMarginWidth( 0, QFontMetrics(mfont).width('0000000') + 5 )
    
    def insertComments( self, comment = None ):
        """
        Inserts comments into the editor based on the current selection.\
        If no comment string is supplied, then the comment from the language \
        will be used.
        
        :param      comment | <str> || None
        
        :return     <bool> | success
        """
        if ( not comment ):
            lang = self.language()
            if ( lang ):
                comment = lang.lineComment()
        
        if ( not comment ):
            return False
        
        startline, startcol, endline, endcol = self.getSelection()
        line, col = self.getCursorPosition()
        
        for lineno in range(startline, endline+1 ):
            self.setCursorPosition(lineno, 0)
            self.insert(comment)
        
        self.setSelection(startline, startcol, endline, endcol)
        self.setCursorPosition(line, col)
        return True
    
    def keyPressEvent( self, event ):
        """
        Overloads the keyPressEvent method to support backtab operations.
        
        :param      event | <QKeyPressEvent>
        """
        if ( event.key() == Qt.Key_Backtab ):
            self.unindentSelection()
        else:
            super(XScintillaEdit, self).keyPressEvent(event)
    
    def language( self ):
        """
        Returns the language that this edit is being run in.
        
        :return     <XLanguage> || None
        """
        return self._language
    
    def load( self, filename, lineno = 0 ):
        """
        Loads the inputed filename as the current document for this edit.
        
        :param      filename | <str>
                    lineno   | <int>
        
        :return     <bool> | success
        """
        filename = str(filename)
        
        if ( not (filename and os.path.exists(filename)) ):
            return False
        
        # load the file
        docfile = QFile(filename)
        if ( not docfile.open(QFile.ReadOnly) ):
            return False
            
        success = self.read(docfile)
        docfile.close()
        
        if ( not success ):
            return False
        
        self._filename = str(filename)
        ext = os.path.splitext(filename)[1]
        self.setCurrentLine(lineno)
        
        lang = XLanguage.byFileType(ext)
        if ( lang != self.language() ):
            self.setLanguage(lang)
        
        self.setModified(False)
        
        return True
    
    def removeComments( self, comment = None ):
        """
        Inserts comments into the editor based on the current selection.\
        If no comment string is supplied, then the comment from the language \
        will be used.
        
        :param      comment | <str> || None
        
        :return     <bool> | success
        """
        if ( not comment ):
            lang = self.language()
            if ( lang ):
                comment = lang.lineComment()
        
        if ( not comment ):
            return False
        
        startline, startcol, endline, endcol = self.getSelection()
        len_comment = len(comment)
        line, col = self.getCursorPosition()
        
        for lineno in range(startline, endline+1 ):
            self.setSelection(lineno, 0, lineno, len_comment)
            if ( self.selectedText() == comment ):
                self.removeSelectedText()
        
        self.setSelection(startline, startcol, endline, endcol)    
        self.setCursorPosition(line, col)
        
        return True
    
    def removeBreakpoint( self, lineno = -1 ):
        """
        Removes the breakpoint at the inputed line number.  If the lineno is -1,
        then the current line number will be used
        
        :note       The lineno is 0-based, while the editor displays lines as
                    a 1-based system.  So, if you remove a breakpoint at
                    visual line 3, you would pass in lineno as 2
        
        :param      lineno | <int>
        """
        if ( lineno == -1 ):
            lineno, colno = self.getCursorPosition()
        
        self.markerDelete(lineno, self._breakpointMarker)
        
        if ( not self.signalsBlocked() ):
            self.breakpointsChanged.emit()
    
    def save( self ):
        """
        Saves the current document out to its filename.
        
        :sa     saveAs
        
        :return     <bool> | success
        """
        return self.saveAs( self.filename() )
    
    def saveAs( self, filename = '' ):
        """
        Saves the current document to the inputed filename.  If no filename \
        is supplied, then the user will be prompted to supply a filename.
        
        :param      filename | <str>
        
        :return     <bool> | success
        """
        if ( not (filename and isinstance(filename, basestring)) ):
            langTypes = XLanguage.pluginFileTypes()
            filename = QFileDialog.getSaveFileName( None,
                                                    'Save File As...',
                                                    QDir.currentPath(),
                                                    langTypes)
            
            if type(filename) == tuple:
                filename = str(filename[0])
        
        if ( not filename ):
            return False
        
        docfile = QFile(filename)
        if ( not docfile.open(QFile.WriteOnly) ):
            logger.warning('Could not open %s for writing.' % filename)
            return False
        
        success = self.write(docfile)
        docfile.close()
        
        if success:
            filename = str(filename)
            self._filename = filename
            
            self.setModified(False)
            
            # set the language
            lang = XLanguage.byFileType(os.path.splitext(filename)[1])
            if ( lang != self.language() ):
                self.setLanguage(lang)
        
        return success
    
    def saveOnClose( self ):
        """
        Returns whether or not this widget should check for save before \
        closing.
        
        :return     <bool>
        """
        return self._saveOnClose
    
    def setBreakpoints( self, breakpoints ):
        """
        Sets the breakpoints for this edit to the inputed list of breakpoints.
        
        :param      breakpoints | [<int>, ..]
        """
        self.clearBreakpoints()
        for breakpoint in breakpoints:
            self.addBreakpoint(breakpoint)
    
    def setColorSet( self, colorSet ):
        """
        Sets the color set for this edit to the inputed set.
        
        :param      colorSet | <XColorSet>
        """
        self._colorSet = colorSet
        
        if ( not colorSet ):
            return
        
        palette = self.palette()
        palette.setColor( palette.Base, colorSet.color('Background'))
        self.setPalette(palette)
        
        # set the global colors
        self.setCaretForegroundColor(
                                colorSet.color('CaretForeground'))
        self.setMarginsBackgroundColor(
                                colorSet.color('MarginsBackground'))
        self.setMarginsForegroundColor(
                                colorSet.color('MarginsForeground'))
        self.setPaper(          colorSet.color('Background'))
        self.setSelectionBackgroundColor(
                                colorSet.color('SelectionBackground'))
        self.setSelectionForegroundColor(
                                colorSet.color('SelectionForeground'))
        self.setFoldMarginColors(colorSet.color('FoldMarginsForeground'),
                                 colorSet.color('FoldMarginsBackground'))
        self.setCallTipsBackgroundColor(
                                colorSet.color('CallTipsBackground'))
        self.setCallTipsForegroundColor(
                                colorSet.color('CallTipsForeground'))
        self.setCallTipsHighlightColor(
                                colorSet.color('CallTipsHighlight'))
        self.setEdgeColor(      colorSet.color('Edge'))
        self.setMarkerBackgroundColor(
                                colorSet.color('MarkerBackground'))
        self.setMarkerForegroundColor(
                                colorSet.color('MarkerForeground'))
        self.setMatchedBraceBackgroundColor(
                                colorSet.color('MatchedBraceBackground'))
        self.setMatchedBraceForegroundColor(
                                colorSet.color('MatchedBraceForeground'))
        self.setUnmatchedBraceBackgroundColor(
                                colorSet.color('UnmatchedBraceBackground'))
        self.setUnmatchedBraceForegroundColor(
                                colorSet.color('UnmatchedBraceForeground'))
        self.setColor(          colorSet.color('Text'))
        
        self.setIndentationGuidesBackgroundColor(
                                    colorSet.color('IndentBackground'))
        self.setIndentationGuidesForegroundColor(
                                colorSet.color('IndentForeground'))
        
        # backwards support
        if ( hasattr(self, 'setCaretBackgroundColor') ):
            self.setCaretBackgroundColor(
                                    colorSet.color('CaretBackground'))
        elif ( hasattr(self, 'setCaretLineBackgroundColor') ):
            self.setCaretLineBackgroundColor(
                                    colorSet.color('CaretBackground'))
                                    
        
        # supported in newer QScintilla versions
        if ( hasattr(self, 'setIndicatorForegroundColor') ):
            self.setIndicatorForegroundColor(
                                    colorSet.color('IndicatorForeground'))
            self.setIndicatorOutlineColor(
                                    colorSet.color('IndicatorOutline'))
        
        # set the language and lexer colors
        lang  = self.language()
        lexer = self.lexer()
        
        if ( lang and lexer ):
            lang.setColorSet(lexer, colorSet)
    
    def setCurrentDebugLine( self, lineno ):
        """
        Returns the line number for the documents debug line.
        
        :param      lineno | <int>
        """
        self.markerDeleteAll(self._currentDebugMarker)
        self.markerAdd(lineno, self._currentDebugMarker)
        self.setCurrentLine(lineno)
        
    def setCurrentLine( self, lineno ):
        """
        Sets the current line number for the edit to the inputed line number.
        
        :param      lineno | <int>
        """
        self.setCursorPosition(lineno, 0)
        self.ensureLineVisible(lineno)
    
    def setLanguage( self, language ):
        self._language = language
        
        # grab the language from the lang module if it is a string
        if ( language and type(language) != XLanguage ):
            language = XLanguage.byName(language)
            
        # collect the language's lexer
        if ( language ):
            lexer = language.createLexer(self, self._colorSet)
        else:
            lexer = None
        
        if ( lexer ):
            self.setLexer(lexer)
            lexer.setFont(self.font())
            mfont = QFont(self.font())
            mfont.setPointSize(mfont.pointSize() - 2)
            self.setMarginsFont(mfont)
        else:
            self.setLexer(None)
        
        self.setColorSet(self._colorSet)
        
    def setLineMarginWidth( self, width ):
        self.setMarginWidth( self.SymbolMargin, width )
    
    def setSaveOnClose( self, state ):
        """
        Sets whether or not this widget should check for save before closing.
        
        :param      state | <bool>
        """
        self._saveOnClose = state
    
    def setShowFolding( self, state ):
        if ( state ):
            self.setFolding( self.BoxedTreeFoldStyle )
        else:
            self.setFolding( self.NoFoldStyle )
    
    def setShowLineNumbers( self, state ):
        self.setMarginLineNumbers( self.SymbolMargin, state )
    
    def setShowWhitespaces( self, state ):
        if ( state ):
            self.setWhitespaceVisibility( QsciScintilla.WsVisible )
        else:
            self.setWhitespaceVisibility( QsciScintilla.WsInvisible )
    
    def showFolding( self ):
        return self.folding() != self.NoFoldStyle
    
    def showLineNumbers( self ):
        return self.marginLineNumbers( self.SymbolMargin )
    
    def showWhitespaces( self ):
        return self.whitespaceVisibility() == QsciScintilla.WsVisible
    
    def unindentSelection( self ):
        """
        Unindents the current selected text.
        """
        sel = self.getSelection()
        
        for line in range(sel[0], sel[2] + 1):
            self.unindent(line)
    
    def wheelEvent( self, event ):
        # scroll the font up and down with the wheel event
        if ( event.modifiers() == Qt.ControlModifier ):
            font            = self.font()
            lexer           = self.lexer()
            if ( lexer ):
                font = lexer.font(0)
            
            pointSize = font.pointSize()
            
            if ( event.delta() > 0 ):
                pointSize += 1
            else:
                pointSize -= 1
            
            if ( pointSize < 5 ):
                pointSize = 5
            
            font.setPointSize( pointSize )
            mfont = QFont(font)
            mfont.setPointSize(pointSize - 2)
            
            # set the font size
            self.setMarginsFont(mfont)
            self.setFont(font)
            if ( lexer ):
                lexer.setFont(font)
            
            self.fontSizeChanged.emit(pointSize)
            event.accept()
        else:
            super(XScintillaEdit,self).wheelEvent(event)
    
    def windowTitle( self ):
        """
        Returns the window title for this edit based on its filename and \
        modified state.
        
        :return     <str>
        """
        output = os.path.basename(self._filename)
        if ( not output ):
            output = 'New Document'
        
        if ( self.isModified() ):
            output += '*'
        
        return output
Exemple #8
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 #9
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 #10
0
class XTextEdit(QTextEdit):
    focusEntered = Signal()
    focusChanged = Signal(bool)
    focusExited = Signal()
    returnPressed = Signal()
    textEntered = Signal(str)
    htmlEntered = Signal(str)
    
    def __init__(self, parent=None):
        super(XTextEdit, self).__init__(parent)
        
        # define custom properties
        self._autoResizeToContents = False
        self._hint = ''
        self._encoding = 'utf-8'
        self._tabsAsSpaces = False
        self._requireShiftForNewLine = False
        self._richTextEditEnabled = True
        
        palette = self.palette()
        self._hintColor = palette.color(palette.AlternateBase).darker(130)
    
    def acceptText(self):
        """
        Emits the editing finished signals for this widget.
        """
        if not self.signalsBlocked():
            self.textEntered.emit(self.toPlainText())
            self.htmlEntered.emit(self.toHtml())
            self.returnPressed.emit()
    
    def autoResizeToContents(self):
        """
        Returns whether or not this text edit should automatically resize
        itself to fit its contents.
        
        :return     <bool>
        """
        return self._autoResizeToContents
    
    @Slot()
    def clear(self):
        """
        Clears the text for this edit and resizes the toolbar information.
        """
        super(XTextEdit, self).clear()
        
        if self.autoResizeToContents():
            self.resizeToContents()
    
    def encoding(self):
        """
        Returns the encoding format that will be used for this text edit.  All
        text that is pasted into this edit will be automatically converted
        to this format.
        
        :return     <str>
        """
        return self._encoding
    
    def focusInEvent(self, event):
        """
        Processes when this widget recieves focus.
        
        :param      event | <QFocusEvent>
        """
        if not self.signalsBlocked():
            self.focusChanged.emit(True)
            self.focusEntered.emit()
        
        return super(XTextEdit, self).focusInEvent(event)
    
    def focusOutEvent(self, event):
        """
        Processes when this widget loses focus.
        
        :param      event | <QFocusEvent>
        """
        if not self.signalsBlocked():
            self.focusChanged.emit(False)
            self.focusExited.emit()
        
        return super(XTextEdit, self).focusOutEvent(event)
    
    def hint( self ):
        """
        Returns the hint that will be rendered for this tree if there are no
        items defined.
        
        :return     <str>
        """
        return self._hint
    
    def hintColor( self ):
        """
        Returns the color used for the hint rendering.
        
        :return     <QColor>
        """
        return self._hintColor
    
    def isRichTextEditEnabled(self):
        """
        Returns whether or not this widget should accept rich text or not.
        
        :return     <bool>
        """
        return self._richTextEditEnabled
    
    def keyPressEvent(self, event):
        """
        Processes user input when they enter a key.
        
        :param      event | <QKeyEvent>
        """
        # emit the return pressed signal for this widget
        if event.key() in (Qt.Key_Return, Qt.Key_Enter) and \
           event.modifiers() == Qt.ControlModifier:
            self.acceptText()
            event.accept()
            return
        
        elif event.key() == Qt.Key_Tab:
            if self.tabsAsSpaces():
                count = 4 - (self.textCursor().columnNumber() % 4)
                self.insertPlainText(' ' * count)
                event.accept()
                return
        
        elif event.key() == Qt.Key_V and event.modifiers() == Qt.ControlModifier:
            self.paste()
            event.accept()
            return
        
        super(XTextEdit, self).keyPressEvent(event)
        
        if self.autoResizeToContents():
            self.resizeToContents()
    
    def paintEvent(self, event):
        """
        Overloads the paint event to support rendering of hints if there are
        no items in the tree.
        
        :param      event | <QPaintEvent>
        """
        super(XTextEdit, self).paintEvent(event)
        
        if self.document().isEmpty() and self.hint():
            text    = self.hint()
            rect    = self.rect()
            
            # modify the padding on the rect
            rect.setX(4)
            rect.setY(4)
            align = int(Qt.AlignLeft | Qt.AlignTop)
            
            # setup the coloring options
            clr = self.hintColor()
            
            # paint the hint
            painter = QPainter(self.viewport())
            painter.setPen(clr)
            painter.drawText(rect, align | Qt.TextWordWrap, text)
    
    @Slot()
    def paste(self):
        """
        Pastes text from the clipboard into this edit.
        """
        html = QApplication.clipboard().text()
        if not self.isRichTextEditEnabled():
            self.insertPlainText(projex.text.toAscii(html))
        else:
            super(XTextEdit, self).paste()
    
    def requireShiftForNewLine(self):
        """
        Returns whether or not the shift modifier is required for new lines.
        When this is True, then Return/Enter key presses will not create
        new lines in the edit, but instead trigger the returnPressed,
        textEntered and htmlEntered signals.
        
        :return     <bool>
        """
        return self._requireShiftForNewLine
    
    def resizeEvent(self, event):
        """
        Processes when this edit has been resized.
        
        :param      event | <QResizeEvent>
        """
        super(XTextEdit, self).resizeEvent(event)
        
        if self.autoResizeToContents():
            self.resizeToContents()
    
    @Slot()
    def resizeToContents(self):
        """
        Resizes this widget to fit the contents of its text.
        """
        doc = self.document()
        h = doc.documentLayout().documentSize().height()
        self.setFixedHeight(h + 4)
    
    def setAutoResizeToContents(self, state):
        """
        Sets whether or not this text edit should automatically resize itself
        to fit its contents.
        
        :param      state | <bool>
        """
        self._autoResizeToContents = state
        if state:
            self.resizeToContents()
    
    def setEncoding(self, encoding):
        """
        Sets the encoding format that will be used for this text edit.  All
        text that is pasted into this edit will be automatically converted
        to this format.
        
        :param      encoding | <str>
        """
        self._encoding = encoding
    
    def setHint(self, hint):
        """
        Sets the hint text that will be rendered when no items are present.
        
        :param      hint | <str>
        """
        self._hint = hint
    
    def setHintColor(self, color):
        """
        Sets the color used for the hint rendering.
        
        :param      color | <QColor>
        """
        self._hintColor = QColor(color)
    
    def setRequireShiftForNewLine(self, state):
        """
        Sets whether or not the shift modifier is required for new lines.
        When this is True, then Return/Enter key presses will not create
        new lines in the edit, but instead trigger the returnPressed,
        textEntered and htmlEntered signals.
        
        :param     state | <bool>
        """
        self._requireShiftForNewLine = state
    
    def setRichTextEditEnabled(self, state):
        """
        Sets whether or not rich text editing is enabled for this editor.
        
        :param      state | <bool>
        """
        self._richTextEditEnabled = state
    
    def setTabsAsSpaces(self, state):
        """
        Sets whether or not tabs as spaces are used instead of tab characters.
        
        :param      state | <bool>
        """
        self._tabsAsSpaces = state
    
    def setText(self, text):
        """
        Sets the text for this instance to the inputed text.
        
        :param      text | <str>
        """
        super(XTextEdit, self).setText(projex.text.toAscii(text))
    
    def tabsAsSpaces(self):
        """
        Returns whether or not tabs as spaces are being used.
        
        :return     <bool>
        """
        return self._tabsAsSpaces
    
    @classmethod
    def getText(cls,
                parent=None,
                windowTitle='Get Text',
                label='',
                text='',
                plain=True,
                wrapped=True):
        """
        Prompts the user for a text entry using the text edit class.
        
        :param      parent | <QWidget>
                    windowTitle | <str>
                    label       | <str>
                    text        | <str>
                    plain       | <bool> | return plain text or not
        
        :return     (<str> text, <bool> accepted)
        """
        # create the dialog
        dlg = QDialog(parent)
        dlg.setWindowTitle(windowTitle)
        
        # create the layout
        layout = QVBoxLayout()
        
        # create the label
        if label:
            lbl = QLabel(dlg)
            lbl.setText(label)
            layout.addWidget(lbl)
        
        # create the widget
        widget = cls(dlg)
        widget.setText(text)
        
        if not wrapped:
            widget.setLineWrapMode(XTextEdit.NoWrap)
        
        layout.addWidget(widget)
        
        # create the buttons
        btns = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
                                Qt.Horizontal,
                                dlg)
        layout.addWidget(btns)
        
        dlg.setLayout(layout)
        dlg.adjustSize()
        
        # create connections
        btns.accepted.connect(dlg.accept)
        btns.rejected.connect(dlg.reject)
        
        if dlg.exec_():
            if plain:
                return (widget.toPlainText(), True)
            else:
                return (widget.toHtml(), True)
        else:
            return ('', False)
        
    
    x_autoResizeToContents = Property(bool,
                                      autoResizeToContents,
                                      setAutoResizeToContents)
    
    x_encoding = Property(str, encoding, setEncoding)
    
    x_requireShiftForNewLine = Property(bool,
                                      requireShiftForNewLine,
                                      setRequireShiftForNewLine)
    
    x_hint = Property(str, hint, setHint)
    x_tabsAsSpaces = Property(bool, tabsAsSpaces, setTabsAsSpaces)
    x_richTextEditEnabled = Property(bool, isRichTextEditEnabled, setRichTextEditEnabled)
Exemple #11
0
class XOrbLookupWorker(XOrbWorker):
    columnLoaded = Signal(object, object, object)
    loadedGroup = Signal(object, object, list)
    loadedRecords = Signal((object, ), (object, object))

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

        # define custom properties
        self._cancelled = False
        self._running = False
        self._batchSize = 100
        self._batched = True
        self._preloadColumns = []

    def batchSize(self):
        """
        Returns the page size for this loader.
        
        :return     <int>
        """
        return self._batchSize

    def cancel(self):
        """
        Cancels the current lookup.
        """
        if self._running:
            self.interrupt()
            self._running = False
            self._cancelled = True
            self.loadingFinished.emit()

    def isBatched(self):
        """
        Returns whether or not this worker is processing in batches.  You should
        enable this if you are not working with paged results, and disable
        if you are working with paged results.
        
        :return     <bool>
        """
        return self._batched

    def isRunning(self):
        """
        Returns whether or not this worker is currently running.
        """
        return self._running

    def loadColumns(self, records, columnName):
        """
        Loads the column information per record for the given records.
        
        :param      records     | [<orb.Table>, ..]
                    columnName  | <str>
        """
        try:
            for record in records:
                col = record.schema().column(columnName)
                if not col:
                    continue

                value = record.recordValue(col.name(), autoInflate=True)
                self.columnLoaded.emit(record, col.name(), wrapNone(value))

        except ConnectionLostError:
            self.connectionLost.emit()

        except Interruption:
            pass

    def loadBatch(self, records):
        """
        Loads the records for this instance in a batched mode.
        """
        try:
            curr_batch = records[:self.batchSize()]
            next_batch = records[self.batchSize():]

            curr_records = list(curr_batch)
            if self._preloadColumns:
                for record in curr_records:
                    record.recordValues(self._preloadColumns)

            if len(curr_records) == self.batchSize():
                self.loadedRecords[object,
                                   object].emit(curr_records, next_batch)
            else:
                self.loadedRecords[object].emit(curr_records)

        except ConnectionLostError:
            self.connectionLost.emit()

        except Interruption:
            pass

    def loadRecords(self, records):
        """
        Loads the record set for this instance.
        
        :param      records | <orb.RecordSet> || <list>
        """
        try:
            if self._running:
                return

            self._cancelled = False
            self._running = True

            try:
                self.setDatabase(records.database())
            except AttributeError:
                pass

            self.startLoading()

            # make sure the orb module is loaded, or there is really no point
            if RecordSet is None:
                logger.error('Orb was not loaded.')

            # lookup a group of results
            if RecordSet.typecheck(records) and records.groupBy():
                levels = records.groupBy()
                next_levels = levels[1:]

                for key, records in records.grouped(levels[0]).items():
                    if self._cancelled:
                        break

                    # PySide Hack! Emitting None across threads will crash Qt
                    #              when in PySide mode.
                    if key == None:
                        key = 'None'

                    self.loadedGroup.emit(key, records, next_levels)

            # lookup a list of results, in batched mode
            elif self.isBatched():
                self.loadBatch(records)

            # lookup a list of results, not in batched mode
            else:
                records = list(records)
                if self._preloadColumns:
                    for record in curr_records:
                        record.recordValues(self._preloadColumns)

                self.loadedRecords[object].emit(records)

            self._running = False
            self.finishLoading()
        except ConnectionLostError:
            self.finishLoading()
            self.connectionLost.emit()
        except Interruption:
            self.finishLoading()
        finally:
            self.finishLoading()

    def preloadColumns(self):
        """
        Sets the list of pre-load columns for this worker.
        
        :return     [<str>, ..]
        """
        return self._preloadColumns

    def setBatchSize(self, batchSize):
        """
        Sets the page size for this loader.
        
        :param     batchSize | <int>
        """
        self._batchSize = batchSize

    def setBatched(self, state):
        """
        Sets the maximum number of records to extract.  This is used in
        conjunction with paging.
        
        :param      maximum | <int>
        """
        self._batched = state

    def setPreloadColumns(self, columns):
        """
        Sets the list of pre-load columns for this worker.
        
        :param      columns | [<str>, ..]
        """
        self._preloadColumns = columns[:]
Exemple #12
0
class XDockActionLabel(QLabel):
    entered = Signal()
    exited = Signal()
    
    def __init__(self, action, pixmapSize, parent=None):
        super(XDockActionLabel, self).__init__(parent)
        
        # define custom properties
        self._action = action
        self._pixmapSize = pixmapSize
        self._position = XDockToolbar.Position.South
        self._padding = 6
        
        # setup default properties
        self.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
        self.setPixmapSize(pixmapSize)
        self.setMouseTracking(True)
    
    def action(self):
        """
        Returns the action linked with this label.
        
        :return     <QAction>
        """
        return self._action
    
    def mousePressEvent(self, event):
        """
        Handles when the user presses this label.
        
        :param      event | <QEvent>
        """
        if event.button() == Qt.LeftButton:
            self.parent().actionTriggered.emit(self.action())
            self.parent().setSelectedAction(self.action())
        elif event.button() == Qt.MidButton:
            self.parent().actionMiddleTriggered.emit(self.action())
        elif event.button() == Qt.RightButton:
            self.parent().actionMenuRequested.emit(self.action(),
                                                   self.mapToParent(event.pos()))
        
        event.accept()
    
    def padding(self):
        """
        Returns the padding value for this label.
        
        :return     <int>
        """
        return self._padding
    
    def pixmapSize(self):
        """
        Returns the size of the pixmap for this widget.
        
        :return     <QSize>
        """
        return self._pixmapSize
    
    def position(self):
        """
        Returns the position associated with this label.
        
        :return     <XDockToolbar.Position>
        """
        return self._position
    
    def setPadding(self, padding):
        """
        Sets the padding information for this label.
        
        :param      padding | <int>
        """
        self._padding = padding
    
    def setPixmapSize(self, size):
        """
        Sets the pixmap size for this label.
        
        :param      size | <QSize>
        """
        self._pixmapSize = size
        self.setPixmap(self.action().icon().pixmap(size))
        
        max_size = self.parent().maximumPixmapSize()
        
        if self.position() in (XDockToolbar.Position.North,
                               XDockToolbar.Position.South):
            self.setFixedWidth(size.width() + self.padding())
            self.setFixedHeight(max_size.height() + self.padding())
        else:
            self.setFixedWidth(max_size.width() + self.padding())
            self.setFixedHeight(size.height() + self.padding())
    
    def setPosition(self, position):
        """
        Adjusts this label to match the given position.
        
        :param      <XDockToolbar.Position>
        """
        self._position = position
        
        if position == XDockToolbar.Position.North:
            self.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        elif position == XDockToolbar.Position.East:
            self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        elif position == XDockToolbar.Position.South:
            self.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
        elif position == XDockToolbar.Position.West:
            self.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
Exemple #13
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
class XRecentFilesMenu(QMenu):
    fileTriggered = Signal(str)
    
    def __init__( self, parent ):
        super(XRecentFilesMenu, self).__init__(parent)
        
        # set menu properties
        self.setTitle('Recent Files')
        
        # set custom properties
        self._maximumLength = 10
        self._filenames     = []
        
        self.triggered.connect( self.emitFileTriggered )
        
    def addFilename( self, filename ):
        """
        Adds a new filename to the top of the list.  If the filename is \
        already loaded, it will be moved to the front of the list.
        
        :param          filename | <str>
        """
        filename = os.path.normpath(str(filename))
        
        if ( filename in self._filenames ):
            self._filenames.remove(filename)
        
        self._filenames.insert(0, filename)
        self._filenames = self._filenames[:self.maximumLength()]
        
        self.refresh()
    
    def emitFileTriggered( self, action ):
        """
        Emits that the filename has been triggered for the inputed action.
        
        :param      action | <QAction>
        """
        if ( not self.signalsBlocked() ):
            filename = str(action.data().toString())
            self.fileTriggered.emit(filename)
    
    def filenames( self ):
        """
        Returns a list of filenames that are currently being cached for this \
        recent files menu.
        
        :return     [<str>, ..]
        """
        return self._filenames
    
    def maximumLength( self ):
        """
        Returns the maximum number of files to cache for this menu at one time.
        
        :return     <int>
        """
        return self._maximumLength
    
    def refresh( self ):
        """
        Clears out the actions for this menu and then loads the files.
        """
        self.clear()
        
        for i, filename in enumerate(self.filenames()):
            name   = '%i. %s' % (i+1, os.path.basename(filename))
            action = self.addAction(name)
            action.setData(wrapVariant(filename))
    
    def restoreSettings(self, settings):
        """
        Restores the files for this menu from the settings.
        
        :param      settings | <QSettings>
        """
        value = unwrapVariant(settings.value('recent_files'))
        if value:
            self.setFilenames(value.split(os.path.pathsep))
    
    def saveSettings(self, settings):
        """
        Saves the files for this menu to the settings.
        
        :param      settings | <QSettings>
        """
        value = wrapVariant(os.path.pathsep.join(self.filenames()))
        settings.setValue('recent_files', value)
    
    def setFilenames( self, filenames ):
        """
        Sets the list of filenames that will be used for this menu to the \
        inputed list.
        
        :param      filenames | [<str>, ..]
        """
        mapped = []
        for filename in filenames:
            filename = str(filename)
            if ( not filename ):
                continue
            
            mapped.append(filename)
            if ( len(mapped) == self.maximumLength() ):
                break
        
        self._filenames = mapped
        self.refresh()
    
    def setMaximumLength( self, length ):
        """
        Sets the maximum number of files to be cached when loading.
        
        :param      length | <int>
        """
        self._maximumLength = length
        self._filenames = self._filenames[:length]
        self.refresh()
Exemple #15
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()
class XOrbRecordBox(XComboBox):
    __designer_group__ = 'ProjexUI - ORB'
    """ Defines a combo box that contains records from the ORB system. """
    loadRequested = Signal(object)

    loadingStarted = Signal()
    loadingFinished = Signal()
    currentRecordChanged = Signal(object)
    currentRecordEdited = Signal(object)
    initialized = Signal()

    def __init__(self, parent=None):
        # needs to be defined before the base class is initialized or the
        # event filter won't work
        self._treePopupWidget = None

        super(XOrbRecordBox, self).__init__(parent)

        # define custom properties
        self._currentRecord = None  # only used while loading
        self._changedRecord = -1

        self._tableTypeName = ''
        self._tableLookupIndex = ''
        self._baseHints = ('', '')
        self._tableType = None
        self._order = None
        self._query = None
        self._iconMapper = None
        self._labelMapper = str
        self._required = True
        self._loaded = False
        self._showTreePopup = False
        self._autoInitialize = False
        self._threadEnabled = True
        self._specifiedColumns = None
        self._specifiedColumnsOnly = False

        # create threading options
        self._worker = XOrbLookupWorker()
        self._workerThread = QThread()
        self._worker.moveToThread(self._workerThread)
        self._worker.setBatched(False)
        self._workerThread.start()

        # create connections
        self.loadRequested.connect(self._worker.loadRecords)
        self.lineEdit().textEntered.connect(self.assignCurrentRecord)
        self.lineEdit().editingFinished.connect(self.emitCurrentRecordEdited)
        self.lineEdit().returnPressed.connect(self.emitCurrentRecordEdited)

        self._worker.loadingStarted.connect(self.markLoadingStarted)
        self._worker.loadingFinished.connect(self.markLoadingFinished)
        self._worker.loadedRecords.connect(self.addRecordsFromThread)

        self.currentIndexChanged.connect(self.emitCurrentRecordChanged)
        QApplication.instance().aboutToQuit.connect(self.__cleanupWorker)

    def __del__(self):
        self.__cleanupWorker()

    def __cleanupWorker(self):
        if not self._workerThread:
            return

        thread = self._workerThread
        worker = self._worker

        self._workerThread = None
        self._worker = None

        worker.deleteLater()

        thread.finished.connect(thread.deleteLater)
        thread.quit()
        thread.wait()

    def addRecord(self, record):
        """
        Adds the given record to the system.
        
        :param      record | <str>
        """
        label_mapper = self.labelMapper()
        icon_mapper = self.iconMapper()

        self.addItem(label_mapper(record))
        self.setItemData(self.count() - 1, wrapVariant(record), Qt.UserRole)

        # load icon
        if icon_mapper:
            self.setItemIcon(self.count() - 1, icon_mapper(record))

        if self.showTreePopup():
            XOrbRecordItem(self.treePopupWidget(), record)

    def addRecords(self, records):
        """
        Adds the given record to the system.
        
        :param      records | [<orb.Table>, ..]
        """
        label_mapper = self.labelMapper()
        icon_mapper = self.iconMapper()

        # create the items to display
        tree = None
        if self.showTreePopup():
            tree = self.treePopupWidget()
            tree.blockSignals(True)
            tree.setUpdatesEnabled(False)

        # add the items to the list
        start = self.count()
        self.addItems(map(label_mapper, records))

        # update the item information
        for i, record in enumerate(records):
            index = start + i

            self.setItemData(index, wrapVariant(record), Qt.UserRole)

            if icon_mapper:
                self.setItemIcon(index, icon_mapper(record))

            if tree:
                XOrbRecordItem(tree, record)

        if tree:
            tree.blockSignals(False)
            tree.setUpdatesEnabled(True)

    def addRecordsFromThread(self, records):
        """
        Adds the given record to the system.
        
        :param      records | [<orb.Table>, ..]
        """
        label_mapper = self.labelMapper()
        icon_mapper = self.iconMapper()

        tree = None
        if self.showTreePopup():
            tree = self.treePopupWidget()

        # add the items to the list
        start = self.count()

        # update the item information
        blocked = self.signalsBlocked()
        self.blockSignals(True)
        for i, record in enumerate(records):
            index = start + i
            self.addItem(label_mapper(record))
            self.setItemData(index, wrapVariant(record), Qt.UserRole)

            if icon_mapper:
                self.setItemIcon(index, icon_mapper(record))

            if record == self._currentRecord:
                self.setCurrentIndex(self.count() - 1)

            if tree:
                XOrbRecordItem(tree, record)
        self.blockSignals(blocked)

    def acceptRecord(self, item):
        """
        Closes the tree popup and sets the current record.
        
        :param      record | <orb.Table>
        """
        record = item.record()
        self.treePopupWidget().close()
        self.setCurrentRecord(record)

    def assignCurrentRecord(self, text):
        """
        Assigns the current record from the inputed text.
        
        :param      text | <str>
        """
        if self.showTreePopup():
            item = self._treePopupWidget.currentItem()
            if item:
                self._currentRecord = item.record()
            else:
                self._currentRecord = None
            return

        # look up the record for the given text
        if text:
            index = self.findText(text)
        elif self.isRequired():
            index = 0
        else:
            index = -1

        # determine new record to look for
        record = self.recordAt(index)
        if record == self._currentRecord:
            return

        # set the current index and record for any changes
        self._currentRecord = record
        self.setCurrentIndex(index)

    def autoInitialize(self):
        """
        Returns whether or not this record box should auto-initialize its
        records.
        
        :return     <bool>
        """
        return self._autoInitialize

    def batchSize(self):
        """
        Returns the batch size to use when processing this record box's list
        of entries.
        
        :return     <int>
        """
        return self._worker.batchSize()

    def checkedRecords(self):
        """
        Returns a list of the checked records from this combo box.
        
        :return     [<orb.Table>, ..]
        """
        indexes = self.checkedIndexes()
        return map(self.recordAt, indexes)

    def currentRecord(self):
        """
        Returns the record found at the current index for this combo box.
        
        :rerturn        <orb.Table> || None
        """
        if self._currentRecord is None and self.isRequired():
            self._currentRecord = self.recordAt(self.currentIndex())
        return self._currentRecord

    def dragEnterEvent(self, event):
        """
        Listens for query's being dragged and dropped onto this tree.
        
        :param      event | <QDragEnterEvent>
        """
        data = event.mimeData()
        if data.hasFormat('application/x-orb-table') and \
           data.hasFormat('application/x-orb-query'):
            tableName = self.tableTypeName()
            if str(data.data('application/x-orb-table')) == tableName:
                event.acceptProposedAction()
                return
        elif data.hasFormat('application/x-orb-records'):
            event.acceptProposedAction()
            return

        super(XOrbRecordBox, self).dragEnterEvent(event)

    def dragMoveEvent(self, event):
        """
        Listens for query's being dragged and dropped onto this tree.
        
        :param      event | <QDragEnterEvent>
        """
        data = event.mimeData()
        if data.hasFormat('application/x-orb-table') and \
           data.hasFormat('application/x-orb-query'):
            tableName = self.tableTypeName()
            if str(data.data('application/x-orb-table')) == tableName:
                event.acceptProposedAction()
                return
        elif data.hasFormat('application/x-orb-records'):
            event.acceptProposedAction()
            return

        super(XOrbRecordBox, self).dragMoveEvent(event)

    def dropEvent(self, event):
        """
        Listens for query's being dragged and dropped onto this tree.
        
        :param      event | <QDropEvent>
        """
        # overload the current filtering options
        data = event.mimeData()
        if data.hasFormat('application/x-orb-table') and \
           data.hasFormat('application/x-orb-query'):
            tableName = self.tableTypeName()
            if str(data.data('application/x-orb-table')) == tableName:
                data = str(data.data('application/x-orb-query'))
                query = Q.fromXmlString(data)
                self.setQuery(query)
                return

        elif self.tableType() and data.hasFormat('application/x-orb-records'):
            from projexui.widgets.xorbtreewidget import XOrbTreeWidget
            records = XOrbTreeWidget.dataRestoreRecords(data)

            for record in records:
                if isinstance(record, self.tableType()):
                    self.setCurrentRecord(record)
                    return

        super(XOrbRecordBox, self).dropEvent(event)

    def emitCurrentRecordChanged(self):
        """
        Emits the current record changed signal for this combobox, provided \
        the signals aren't blocked.
        """
        record = unwrapVariant(self.itemData(self.currentIndex(), Qt.UserRole))
        if not Table.recordcheck(record):
            record = None

        self._currentRecord = record
        if not self.signalsBlocked():
            self._changedRecord = record
            self.currentRecordChanged.emit(record)

    def emitCurrentRecordEdited(self):
        """
        Emits the current record edited signal for this combobox, provided the
        signals aren't blocked and the record has changed since the last time.
        """
        if self._changedRecord == -1:
            return

        if self.signalsBlocked():
            return

        record = self._changedRecord
        self._changedRecord = -1
        self.currentRecordEdited.emit(record)

    def eventFilter(self, object, event):
        """
        Filters events for the popup tree widget.
        
        :param      object | <QObject>
                    event  | <QEvent>
        
        :retuen     <bool> | consumed
        """
        if not (object and object == self._treePopupWidget):
            return super(XOrbRecordBox, self).eventFilter(object, event)

        elif event.type() == event.KeyPress:
            # accept lookup
            if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab,
                               Qt.Key_Backtab):

                item = object.currentItem()
                text = self.lineEdit().text()

                if not text:
                    record = None
                    item = None

                elif isinstance(item, XOrbRecordItem):
                    record = item.record()

                if record and item.isSelected() and not item.isHidden():
                    self.hidePopup()
                    self.setCurrentRecord(record)
                    event.accept()
                    return True

                else:
                    self.setCurrentRecord(None)
                    self.hidePopup()
                    self.lineEdit().setText(text)
                    self.lineEdit().keyPressEvent(event)
                    event.accept()
                    return True

            # cancel lookup
            elif event.key() == Qt.Key_Escape:
                text = self.lineEdit().text()
                self.setCurrentRecord(None)
                self.lineEdit().setText(text)
                self.hidePopup()
                event.accept()
                return True

            # update the search info
            else:
                self.lineEdit().keyPressEvent(event)

        elif event.type() == event.Show:
            object.resizeToContents()
            object.horizontalScrollBar().setValue(0)

        elif event.type() == event.KeyRelease:
            self.lineEdit().keyReleaseEvent(event)

        elif event.type() == event.MouseButtonPress:
            local_pos = object.mapFromGlobal(event.globalPos())
            in_widget = object.rect().contains(local_pos)

            if not in_widget:
                text = self.lineEdit().text()
                self.setCurrentRecord(None)
                self.lineEdit().setText(text)
                self.hidePopup()
                event.accept()
                return True

        return super(XOrbRecordBox, self).eventFilter(object, event)

    def focusNextChild(self, event):
        if not self.isLoading():
            self.assignCurrentRecord(self.lineEdit().text())

        return super(XOrbRecordBox, self).focusNextChild(event)

    def focusNextPrevChild(self, event):
        if not self.isLoading():
            self.assignCurrentRecord(self.lineEdit().text())

        return super(XOrbRecordBox, self).focusNextPrevChild(event)

    def focusInEvent(self, event):
        """
        When this widget loses focus, try to emit the record changed event
        signal.
        """
        self._changedRecord = -1
        super(XOrbRecordBox, self).focusInEvent(event)

    def focusOutEvent(self, event):
        """
        When this widget loses focus, try to emit the record changed event
        signal.
        """
        if not self.isLoading():
            self.assignCurrentRecord(self.lineEdit().text())

        super(XOrbRecordBox, self).focusOutEvent(event)

    def hidePopup(self):
        """
        Overloads the hide popup method to handle when the user hides
        the popup widget.
        """
        if self._treePopupWidget and self.showTreePopup():
            self._treePopupWidget.close()

        super(XOrbRecordBox, self).hidePopup()

    def iconMapper(self):
        """
        Returns the icon mapping method to be used for this combobox.
        
        :return     <method> || None
        """
        return self._iconMapper

    def isLoading(self):
        """
        Returns whether or not this combobox is loading records.
        
        :return     <bool>
        """
        return self._worker.isRunning()

    def isRequired(self):
        """
        Returns whether or not this combo box requires the user to pick a
        selection.
        
        :return     <bool>
        """
        return self._required

    def isThreadEnabled(self):
        """
        Returns whether or not threading is enabled for this combo box.
        
        :return     <bool>
        """
        return self._threadEnabled

    def labelMapper(self):
        """
        Returns the label mapping method to be used for this combobox.
        
        :return     <method> || None
        """
        return self._labelMapper

    @Slot(object)
    def lookupRecords(self, record):
        """
        Lookups records based on the inputed record.  This will use the 
        tableLookupIndex property to determine the Orb Index method to
        use to look up records.  That index method should take the inputed
        record as an argument, and return a list of records.
        
        :param      record | <orb.Table>
        """
        table_type = self.tableType()
        if not table_type:
            return

        index = getattr(table_type, self.tableLookupIndex(), None)
        if not index:
            return

        self.setRecords(index(record))

    def markLoadingStarted(self):
        """
        Marks this widget as loading records.
        """
        if self.isThreadEnabled():
            XLoaderWidget.start(self)

        if self.showTreePopup():
            tree = self.treePopupWidget()
            tree.setCursor(Qt.WaitCursor)
            tree.clear()
            tree.setUpdatesEnabled(False)
            tree.blockSignals(True)

            self._baseHints = (self.hint(), tree.hint())
            tree.setHint('Loading records...')
            self.setHint('Loading records...')
        else:
            self._baseHints = (self.hint(), '')
            self.setHint('Loading records...')

        self.setCursor(Qt.WaitCursor)
        self.blockSignals(True)
        self.setUpdatesEnabled(False)

        # prepare to load
        self.clear()
        use_dummy = not self.isRequired() or self.isCheckable()
        if use_dummy:
            self.addItem('')

        self.loadingStarted.emit()

    def markLoadingFinished(self):
        """
        Marks this widget as finished loading records.
        """
        XLoaderWidget.stop(self, force=True)

        hint, tree_hint = self._baseHints
        self.setHint(hint)

        # set the tree widget
        if self.showTreePopup():
            tree = self.treePopupWidget()
            tree.setHint(tree_hint)
            tree.unsetCursor()
            tree.setUpdatesEnabled(True)
            tree.blockSignals(False)

        self.unsetCursor()
        self.blockSignals(False)
        self.setUpdatesEnabled(True)
        self.loadingFinished.emit()

    def order(self):
        """
        Returns the ordering for this widget.
        
        :return     [(<str> column, <str> asc|desc, ..] || None
        """
        return self._order

    def query(self):
        """
        Returns the query used when querying the database for the records.
        
        :return     <Query> || None
        """
        return self._query

    def records(self):
        """
        Returns the record list that ist linked with this combo box.
        
        :return     [<orb.Table>, ..]
        """
        records = []
        for i in range(self.count()):
            record = self.recordAt(i)
            if record:
                records.append(record)
        return records

    def recordAt(self, index):
        """
        Returns the record at the inputed index.
        
        :return     <orb.Table> || None
        """
        return unwrapVariant(self.itemData(index, Qt.UserRole))

    def refresh(self, records):
        """
        Refreshs the current user interface to match the latest settings.
        """
        self._loaded = True

        if self.isLoading():
            return

        # load the information
        if RecordSet.typecheck(records):
            table = records.table()
            self.setTableType(table)

            if self.order():
                records.setOrder(self.order())

            # load specific data for this record box
            if self.specifiedColumnsOnly():
                records.setColumns(
                    map(lambda x: x.name(), self.specifiedColumns()))

            # load the records asynchronously
            if self.isThreadEnabled() and \
               table and \
               table.getDatabase().isThreadEnabled():
                # assign ordering based on tree table
                if self.showTreePopup():
                    tree = self.treePopupWidget()
                    if tree.isSortingEnabled():
                        col = tree.sortColumn()
                        colname = tree.headerItem().text(col)
                        column = table.schema().column(colname)

                        if column:
                            if tree.sortOrder() == Qt.AscendingOrder:
                                sort_order = 'asc'
                            else:
                                sort_order = 'desc'

                            records.setOrder([(column.name(), sort_order)])

                self.loadRequested.emit(records)
                return

        # load the records synchronously
        self.loadingStarted.emit()
        curr_record = self.currentRecord()
        self.blockSignals(True)
        self.setUpdatesEnabled(False)
        self.clear()
        use_dummy = not self.isRequired() or self.isCheckable()
        if use_dummy:
            self.addItem('')
        self.addRecords(records)
        self.setUpdatesEnabled(True)
        self.blockSignals(False)
        self.setCurrentRecord(curr_record)
        self.loadingFinished.emit()

    def setAutoInitialize(self, state):
        """
        Sets whether or not this combo box should auto initialize itself
        when it is shown.
        
        :param      state | <bool>
        """
        self._autoInitialize = state

    def setBatchSize(self, size):
        """
        Sets the batch size of records to look up for this record box.
        
        :param      size | <int>
        """
        self._worker.setBatchSize(size)

    def setCheckedRecords(self, records):
        """
        Sets the checked off records to the list of inputed records.
        
        :param      records | [<orb.Table>, ..]
        """
        QApplication.sendPostedEvents(self, -1)
        indexes = []

        for i in range(self.count()):
            record = self.recordAt(i)
            if record is not None and record in records:
                indexes.append(i)

        self.setCheckedIndexes(indexes)

    def setCurrentRecord(self, record, autoAdd=False):
        """
        Sets the index for this combobox to the inputed record instance.
        
        :param      record      <orb.Table>
        
        :return     <bool> success
        """
        if record is not None and not Table.recordcheck(record):
            return False

        # don't reassign the current record
        # clear the record
        if record is None:
            self._currentRecord = None
            blocked = self.signalsBlocked()
            self.blockSignals(True)
            self.setCurrentIndex(-1)
            self.blockSignals(blocked)

            if not blocked:
                self.currentRecordChanged.emit(None)

            return True

        elif record == self.currentRecord():
            return False

        self._currentRecord = record
        found = False

        blocked = self.signalsBlocked()
        self.blockSignals(True)
        for i in range(self.count()):
            stored = unwrapVariant(self.itemData(i, Qt.UserRole))
            if stored == record:
                self.setCurrentIndex(i)
                found = True
                break

        if not found and autoAdd:
            self.addRecord(record)
            self.setCurrentIndex(self.count() - 1)

        self.blockSignals(blocked)

        if not blocked:
            self.currentRecordChanged.emit(record)
        return False

    def setIconMapper(self, mapper):
        """
        Sets the icon mapping method for this combobox to the inputed mapper. \
        The inputed mapper method should take a orb.Table instance as input \
        and return a QIcon as output.
        
        :param      mapper | <method> || None
        """
        self._iconMapper = mapper

    def setLabelMapper(self, mapper):
        """
        Sets the label mapping method for this combobox to the inputed mapper.\
        The inputed mapper method should take a orb.Table instance as input \
        and return a string as output.
        
        :param      mapper | <method>
        """
        self._labelMapper = mapper

    def setOrder(self, order):
        """
        Sets the order for this combo box to the inputed order.  This will
        be used in conjunction with the query when loading records to the
        combobox.
        
        :param      order | [(<str> column, <str> asc|desc), ..] || None
        """
        self._order = order

    def setQuery(self, query, autoRefresh=True):
        """
        Sets the query for this record box for generating records.
        
        :param      query | <Query> || None
        """
        self._query = query

        tableType = self.tableType()
        if not tableType:
            return False

        if autoRefresh:
            self.refresh(tableType.select(where=query))

        return True

    def setRecords(self, records):
        """
        Sets the records on this combobox to the inputed record list.
        
        :param      records | [<orb.Table>, ..]
        """
        self.refresh(records)

    def setRequired(self, state):
        """
        Sets the required state for this combo box.  If the column is not
        required, a blank record will be included with the choices.
        
        :param      state | <bool>
        """
        self._required = state

    def setShowTreePopup(self, state):
        """
        Sets whether or not to use an ORB tree widget in the popup for this
        record box.
        
        :param      state | <bool>
        """
        self._showTreePopup = state

    def setSpecifiedColumns(self, columns):
        """
        Sets the specified columns for this combobox widget.
        
        :param      columns | [<orb.Column>, ..] || [<str>, ..] || None
        """
        self._specifiedColumns = columns
        self._specifiedColumnsOnly = columns is not None

    def setSpecifiedColumnsOnly(self, state):
        """
        Sets whether or not only specified columns should be
        loaded for this record box.
        
        :param      state | <bool>
        """
        self._specifiedColumnsOnly = state

    def setTableLookupIndex(self, index):
        """
        Sets the name of the index method that will be used to lookup
        records for this combo box.
        
        :param    index | <str>
        """
        self._tableLookupIndex = str(index)

    def setTableType(self, tableType):
        """
        Sets the table type for this record box to the inputed table type.
        
        :param      tableType | <orb.Table>
        """
        self._tableType = tableType

        if tableType:
            self._tableTypeName = tableType.schema().name()
        else:
            self._tableTypeName = ''

    def setTableTypeName(self, name):
        """
        Sets the table type name for this record box to the inputed name.
        
        :param      name | <str>
        """
        self._tableTypeName = str(name)
        self._tableType = None

    def setThreadEnabled(self, state):
        """
        Sets whether or not threading should be enabled for this widget.  
        Actual threading will be determined by both this property, and whether
        or not the active ORB backend supports threading.
        
        :param      state | <bool>
        """
        self._threadEnabled = state

    def setVisible(self, state):
        """
        Sets the visibility for this record box.
        
        :param      state | <bool>
        """
        super(XOrbRecordBox, self).setVisible(state)

        if state and not self._loaded:
            if self.autoInitialize():
                table = self.tableType()
                if not table:
                    return

                self.setRecords(table.select(where=self.query()))
            else:
                self.initialized.emit()

    def showPopup(self):
        """
        Overloads the popup method from QComboBox to display an ORB tree widget
        when necessary.
        
        :sa     setShowTreePopup
        """
        if not self.showTreePopup():
            return super(XOrbRecordBox, self).showPopup()

        tree = self.treePopupWidget()

        if tree and not tree.isVisible():
            tree.move(self.mapToGlobal(QPoint(0, self.height())))
            tree.resize(self.width(), 250)
            tree.resizeToContents()
            tree.filterItems('')
            tree.setFilteredColumns(range(tree.columnCount()))
            tree.show()

    def showTreePopup(self):
        """
        Sets whether or not to use an ORB tree widget in the popup for this
        record box.
        
        :return     <bool>
        """
        return self._showTreePopup

    def specifiedColumns(self):
        """
        Returns the list of columns that are specified based on the column
        view for this widget.
        
        :return     [<orb.Column>, ..]
        """
        columns = []
        table = self.tableType()
        tree = self.treePopupWidget()
        schema = table.schema()

        if self._specifiedColumns is not None:
            colnames = self._specifiedColumns
        else:
            colnames = tree.columns()

        for colname in colnames:
            if isinstance(colname, Column):
                columns.append(colname)
            else:
                col = schema.column(colname)
                if col and not col.isProxy():
                    columns.append(col)

        return columns

    def specifiedColumnsOnly(self):
        """
        Returns whether or not only specified columns should be loaded
        for this record box.
        
        :return     <int>
        """
        return self._specifiedColumnsOnly

    def tableLookupIndex(self):
        """
        Returns the name of the index method that will be used to lookup
        records for this combo box.
        
        :return     <str>
        """
        return self._tableLookupIndex

    def tableType(self):
        """
        Returns the table type for this instance.
        
        :return     <subclass of orb.Table> || None
        """
        if not self._tableType:
            if self._tableTypeName:
                self._tableType = Orb.instance().model(str(
                    self._tableTypeName))

        return self._tableType

    def tableTypeName(self):
        """
        Returns the table type name that is set for this combo box.
        
        :return     <str>
        """
        return self._tableTypeName

    def treePopupWidget(self):
        """
        Returns the popup widget for this record box when it is supposed to
        be an ORB tree widget.
        
        :return     <XTreeWidget>
        """
        if not self._treePopupWidget:
            # create the treewidget
            tree = XTreeWidget(self)
            tree.setWindowFlags(Qt.Popup)
            tree.setFocusPolicy(Qt.StrongFocus)
            tree.installEventFilter(self)
            tree.setAlternatingRowColors(True)
            tree.setShowGridColumns(False)
            tree.setRootIsDecorated(False)
            tree.setVerticalScrollMode(tree.ScrollPerPixel)

            # create connections
            tree.itemClicked.connect(self.acceptRecord)

            self.lineEdit().textEdited.connect(tree.filterItems)
            self.lineEdit().textEdited.connect(self.showPopup)

            self._treePopupWidget = tree

        return self._treePopupWidget

    def worker(self):
        """
        Returns the worker object for loading records for this record box.
        
        :return     <XOrbLookupWorker>
        """
        return self._worker

    x_batchSize = Property(int, batchSize, setBatchSize)
    x_required = Property(bool, isRequired, setRequired)
    x_tableTypeName = Property(str, tableTypeName, setTableTypeName)
    x_tableLookupIndex = Property(str, tableLookupIndex, setTableLookupIndex)
    x_showTreePopup = Property(bool, showTreePopup, setShowTreePopup)
    x_threadEnabled = Property(bool, isThreadEnabled, setThreadEnabled)
class 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 = str(filename[0])
        
        filename = str(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(str(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)
Exemple #18
0
class XComboBox(QComboBox):
    """
    ~~>[img:widgets/xcombobox.png]
    The XComboBox class is a simple extension to the standard QComboBox
    that provides a couple enhancement features, namely the ability to 
    add a hint to the line edit and supporting multi-selection via checkable
    items.
    
    == Example ==
    
    |>>> from projexui.widgets.xcombobox import XComboBox
    |>>> import projexui
    |
    |>>> # create the combobox
    |>>> combo = projexui.testWidget(XComboBox)
    |
    |>>> # set the hint
    |>>> combo.setHint('select type')
    |
    |>>> # create items, make checkable
    |>>> combo.addItems(['A', 'B', 'C'])
    |>>> combo.setCheckable(True)
    |
    |>>> # set the checked items
    |>>> combo.setCheckedItems(['C'])
    |>>> combo.setCheckedIndexes([0, 2])
    |
    |>>> # retrieve checked items
    |>>> combo.checkedItems()
    |['A', 'C']
    |>>> combo.checkedIndexes()
    |[0, 2]
    |
    |>>> # connect to signals
    |>>> def printChecked(): print checked.checkedItems()
    |>>> combo.checkedIndexesChanged.connect(printChecked)
    |
    |>>> # modify selection and see the output
    """
    __designer_icon__ = resources.find('img/ui/combobox.png')

    checkedIndexesChanged = Signal(list)
    checkedItemsChanged = Signal(list)

    def __init__(self, parent=None):
        # define custom properties
        self._checkable = False
        self._hint = ''
        self._separator = ','
        self._autoRaise = False
        self._hovered = False
        self._lineEdit = None

        # setup the checkable popup widget
        self._checkablePopup = None

        # set default properties
        super(XComboBox, self).__init__(parent)
        self.setLineEdit(XLineEdit(self))

    def autoRaise(self):
        """
        Returns whether or not this combo box should automatically
        raise up.
        
        :return     <bool>
        """
        return self._autoRaise

    def adjustCheckState(self):
        """
        Updates when new items are added to the system.
        """
        if self.isCheckable():
            self.updateCheckState()

    def checkablePopup(self):
        """
        Returns the popup if this widget is checkable.
        
        :return     <QListView> || None
        """
        if not self._checkablePopup and self.isCheckable():
            popup = QListView(self)
            popup.setSelectionMode(QListView.NoSelection)
            popup.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            popup.setWindowFlags(Qt.Popup)
            popup.installEventFilter(self)
            popup.doubleClicked.connect(self.checkModelIndex)
            self._checkablePopup = popup

        return self._checkablePopup

    def checkModelIndex(self, modelIndex):
        """
        Sets the current index as the checked index.
        
        :param      modelIndex | <QModelIndex>
        """
        self.checkablePopup().hide()

        if not self.isCheckable():
            return

        self.setCheckedIndexes([modelIndex.row()])

    def currentText(self):
        """
        Returns the current text for this combobox, including the hint option \
        if no text is set.
        """
        lineEdit = self.lineEdit()
        if lineEdit:
            return lineEdit.currentText()
        text = nativestring(super(XComboBox, self).currentText())
        if not text:
            return self._hint
        return text

    def checkedIndexes(self):
        """
        Returns a list of checked indexes for this combobox.
        
        :return     [<int>, ..]
        """
        if (not self.isCheckable()):
            return []

        model = self.model()
        return [i for i in range(self.count()) if model.item(i).checkState()]

    def checkedItems(self):
        """
        Returns the checked items for this combobox.
        
        :return     [<str>, ..]
        """
        if not self.isCheckable():
            return []

        return [nativestring(self.itemText(i)) for i in self.checkedIndexes()]

    def enterEvent(self, event):
        self._hovered = True
        super(XComboBox, self).enterEvent(event)

        if self.autoRaise():
            try:
                self.lineEdit().show()
            except AttributeError:
                pass

    def eventFilter(self, object, event):
        """
        Filters events for the popup widget.
        
        :param      object | <QObject>
                    event  | <QEvent>
        """
        # popup the editor when clicking in the line edit for a checkable state
        if object == self.lineEdit() and self.isEnabled():
            if not self.isCheckable():
                return super(XComboBox, self).eventFilter(object, event)

            # show the popup when the user clicks on it
            elif event.type() == event.MouseButtonPress:
                self.showPopup()

            # eat the wheel event when the user is scrolling
            elif event.type() == event.Wheel:
                return True

        # make sure we're looking for the checkable popup
        elif object == self._checkablePopup:
            if event.type() == event.KeyPress and \
                 event.key() in (Qt.Key_Escape, Qt.Key_Return, Qt.Key_Enter):
                object.close()

            elif event.type() == event.MouseButtonPress:
                if not object.geometry().contains(event.pos()):
                    object.close()

        return super(XComboBox, self).eventFilter(object, event)

    def hint(self):
        """
        Returns the hint for this combobox.
        
        :return     <str>
        """
        return self._hint

    def hintColor(self):
        """
        Returns the hint color for this combo box provided its line edit is
        an XLineEdit instance.
        
        :return     <QColor>
        """
        lineEdit = self.lineEdit()
        if isinstance(lineEdit, XLineEdit):
            return lineEdit.hintColor()
        return QColor()

    def isCheckable(self):
        """
        Returns whether or not this combobox has checkable options.
        
        :return     <bool>
        """
        try:
            return self._checkable
        except AttributeError:
            return False

    def items(self):
        """
        Returns the labels for the different items in this combo box.
        
        :return     [<str>, ..]
        """
        return [self.itemText(i) for i in range(self.count())]

    def leaveEvent(self, event):
        self._hovered = False
        super(XComboBox, self).leaveEvent(event)

        if self.autoRaise():
            try:
                self.lineEdit().hide()
            except AttributeError:
                pass

    def lineEdit(self):
        """
        Returns the line editor associated with this combobox.  This will
        return the object stored at the reference for the editor since
        sometimes the internal Qt process will raise a RuntimeError that
        the C/C++ object has been deleted.
        
        :return     <XLineEdit> || None
        """
        try:
            edit = self._lineEdit()
        except TypeError:
            edit = None

        if edit is None:
            self._edit = None
        return edit

    def paintEvent(self, event):
        """
        Paints this combobox based on whether or not it is visible.
        
        :param      event | <QPaintEvent>
        """
        if not self.autoRaise() or (self._hovered and self.isEnabled()):
            super(XComboBox, self).paintEvent(event)

            text = QComboBox.currentText(self)
            if not text and self._hint and not self.lineEdit():
                text = self._hint
                palette = self.palette()
                with XPainter(self) as painter:
                    painter.setPen(
                        palette.color(palette.Disabled, palette.Text))
                    painter.drawText(5, 0, self.width(), self.height(),
                                     Qt.AlignLeft | Qt.AlignVCenter,
                                     self.currentText())

        else:
            palette = self.palette()

            with XPainter(self) as painter:
                text = QComboBox.currentText(self)
                if not text:
                    text = self.hint()
                    painter.setPen(
                        palette.color(palette.Disabled, palette.WindowText))

                painter.drawText(5, 0, self.width(), self.height(),
                                 Qt.AlignLeft | Qt.AlignVCenter, text)

                x = self.width() - 15
                y = 4
                pixmap = QPixmap(
                    resources.find('img/treeview/triangle_down.png'))
                painter.drawPixmap(x, y, pixmap)

    def separator(self):
        """
        Returns the separator that will be used for joining together 
        the options when in checked mode.  By default, this will be a comma.
        
        :return     <str>
        """
        return self._separator

    def setAutoRaise(self, state):
        """
        Sets whether or not this combo box should automatically
        raise up.
        
        :param      state | <bool>
        """
        self._autoRaise = state
        self.setMouseTracking(state)
        try:
            self.lineEdit().setVisible(not state)
        except AttributeError:
            pass

    def setCheckedIndexes(self, indexes):
        """
        Sets a list of checked indexes for this combobox.
        
        :param      indexes | [<int>, ..]
        """
        if not self.isCheckable():
            return

        model = self.model()
        model.blockSignals(True)
        for i in range(self.count()):
            if not self.itemText(i):
                continue

            item = model.item(i)

            if i in indexes:
                state = Qt.Checked
            else:
                state = Qt.Unchecked

            item.setCheckState(state)
        model.blockSignals(False)
        self.updateCheckedText()

    def setCheckedItems(self, items):
        """
        Returns the checked items for this combobox.
        
        :return     items | [<str>, ..]
        """
        if not self.isCheckable():
            return

        model = self.model()
        for i in range(self.count()):
            item_text = self.itemText(i)
            if not item_text:
                continue

            if nativestring(item_text) in items:
                state = Qt.Checked
            else:
                state = Qt.Unchecked

            model.item(i).setCheckState(state)

    def setCheckable(self, state):
        """
        Sets whether or not this combobox stores checkable items.
        
        :param      state | <bool>
        """
        self._checkable = state

        # need to be editable to be checkable
        edit = self.lineEdit()
        if state:
            self.setEditable(True)
            edit.setReadOnly(True)

            # create connections
            model = self.model()
            model.rowsInserted.connect(self.adjustCheckState)
            model.dataChanged.connect(self.updateCheckedText)

        elif edit:
            edit.setReadOnly(False)

        self.updateCheckState()
        self.updateCheckedText()

    def setEditable(self, state):
        """
        Sets whether or not this combobox will be editable, updating its \
        line edit to an XLineEdit if necessary.
        
        :param      state | <bool>
        """
        super(XComboBox, self).setEditable(state)

        if state:
            edit = self.lineEdit()
            if edit and isinstance(edit, XLineEdit):
                return
            elif edit:
                edit.setParent(None)
                edit.deleteLater()

            edit = XLineEdit(self)
            edit.setHint(self.hint())
            self.setLineEdit(edit)

    def setLineEdit(self, edit):
        """
        Sets the line edit for this widget.
        
        :warning    If the inputed edit is NOT an instance of XLineEdit, \
                    this method will destroy the edit and create a new \
                    XLineEdit instance.
        
        :param      edit | <XLineEdit>
        """
        if edit and not isinstance(edit, XLineEdit):
            edit.setParent(None)
            edit.deleteLater()

            edit = XLineEdit(self)

        if edit is not None:
            edit.installEventFilter(self)
            self._lineEdit = weakref.ref(edit)
        else:
            self._lineEdit = None

        super(XComboBox, self).setLineEdit(edit)

    def setHint(self, hint):
        """
        Sets the hint for this line edit that will be displayed when in \
        editable mode.
        
        :param      hint | <str>
        """
        self._hint = hint

        lineEdit = self.lineEdit()
        if isinstance(lineEdit, XLineEdit):
            lineEdit.setHint(hint)

    def setHintColor(self, color):
        """
        Sets the hint color for this combo box provided its line edit is
        an XLineEdit instance.
        
        :param      color | <QColor>
        """
        lineEdit = self.lineEdit()
        if isinstance(lineEdit, XLineEdit):
            lineEdit.setHintColor(color)

    @Slot(str)
    def setSeparator(self, separator):
        """
        Sets the separator that will be used when joining the checked items
        for this combo in the display.
        
        :param      separator | <str>
        """
        self._separator = nativestring(separator)
        self.updateCheckedText()

    def showPopup(self):
        """
        Displays a custom popup widget for this system if a checkable state \
        is setup.
        """
        if not self.isCheckable():
            return super(XComboBox, self).showPopup()

        if not self.isVisible():
            return

        # update the checkable widget popup
        point = self.mapToGlobal(QPoint(0, self.height() - 1))
        popup = self.checkablePopup()
        popup.setModel(self.model())
        popup.move(point)
        popup.setFixedWidth(self.width())

        height = (self.count() * 19) + 2
        if height > 400:
            height = 400
            popup.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        else:
            popup.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        popup.setFixedHeight(height)
        popup.show()
        popup.raise_()

    def updateCheckState(self):
        """
        Updates the items to reflect the current check state system.
        """
        checkable = self.isCheckable()
        model = self.model()
        flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled

        for i in range(self.count()):
            item = model.item(i)

            if not (checkable and item.text()):
                item.setCheckable(False)
                item.setFlags(flags)

            # only allow checking for items with text
            else:
                item.setCheckable(True)
                item.setFlags(flags | Qt.ItemIsUserCheckable)

    def updateCheckedText(self):
        """
        Updates the text in the editor to reflect the latest state.
        """
        if not self.isCheckable():
            return

        indexes = self.checkedIndexes()
        items = self.checkedItems()

        if len(items) < 2 or self.separator():
            self.lineEdit().setText(self.separator().join(items))
        else:
            self.lineEdit().setText('{0} items selected'.format(len(items)))

        if not self.signalsBlocked():
            self.checkedItemsChanged.emit(items)
            self.checkedIndexesChanged.emit(indexes)

    def toggleModelIndex(self, modelIndex):
        """
        Toggles the index's check state.
        
        :param      modelIndex | <QModelIndex>
        """
        if not self.isCheckable():
            return

        item = self.model().item(modelIndex.row())
        if item.checkState() == Qt.Checked:
            state = Qt.Unchecked
        else:
            state = Qt.Checked

        item.setCheckState(state)

    # define qt properties
    x_hint = Property(str, hint, setHint)
    x_checkable = Property(bool, isCheckable, setCheckable)
    x_separator = Property(str, separator, setSeparator)
    x_autoRaise = Property(bool, autoRaise, setAutoRaise)
Exemple #19
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 #20
0
class XOrbRecordEdit(QWidget):
    """ """
    __designer_container__ = True
    __designer_group__ = 'ProjexUI - ORB'
    
    saved = Signal()
    
    def __init__( self, parent = None ):
        super(XOrbRecordEdit, self).__init__( parent )
        
        # define custom properties
        self._record = None
        self._model  = None
        self._uifile = ''
        
        # set default properties
        
        # create connections
    
    def model( self ):
        """
        Returns the model that is linked to this widget.
        
        :return     <orb.Table>
        """
        return self._model
    
    def record( self ):
        """
        Returns the record linked with widget.
        
        :return     <orb.Table> || None
        """
        return self._record
    
    def rebuild( self ):
        """
        Rebuilds the interface for this widget based on the current model.
        """
        self.setUpdatesEnabled(False)
        self.blockSignals(True)
        
        # clear out all the subwidgets for this widget
        for child in self.findChildren(QObject):
            child.setParent(None)
            child.deleteLater()
        
        # load up all the interface for this widget
        schema = self.schema()
        if ( schema ):
            self.setEnabled(True)
            
            uifile = self.uiFile()
            
            # load a user defined file
            if ( uifile ):
                projexui.loadUi('', self, uifile)
                
                for widget in self.findChildren(XOrbColumnEdit):
                    columnName = widget.columnName()
                    column     = schema.column(columnName)
                    
                    if ( column ):
                        widget.setColumn(column)
                    else:
                        logger.debug('%s is not a valid column of %s' % \
                                     (columnName, schema.name()))
            
            # dynamically load files
            else:
                layout = QFormLayout()
                layout.setContentsMargins(0, 0, 0, 0)
                columns = schema.columns()
                columns.sort(key = lambda x: x.displayName())
                
                record = self.record()
                
                for column in columns:
                    # ignore protected columns
                    if ( column.name().startswith('_') ):
                        continue
                    
                    label   = column.displayName()
                    coltype = column.columnType()
                    name    = column.name()
                    
                    # create the column edit widget
                    widget  = XOrbColumnEdit(self)
                    widget.setObjectName('ui_' + name)
                    widget.setColumnName(name)
                    widget.setColumnType(coltype)
                    widget.setColumn(column)
                    
                    layout.addRow(QLabel(label, self), widget)
                
                self.setLayout(layout)
                self.adjustSize()
                
                self.setWindowTitle('Edit %s' % schema.name())
        else:
            self.setEnabled(False)
        
        self.setUpdatesEnabled(True)
        self.blockSignals(False)
    
    @Slot()
    def save( self ):
        """
        Saves the values from the editor to the system.
        """
        schema = self.schema()
        if ( not schema ):
            self.saved.emit()
            return
        
        record = self.record()
        if not record:
            record = self._model()
        
        # validate the information
        save_data    = []
        column_edits = self.findChildren(XOrbColumnEdit)
        for widget in column_edits:
            columnName  = widget.columnName()
            column      = schema.column(columnName)
            
            if ( not column ):
                logger.warning('%s is not a valid column of %s.' % \
                               (columnName, schema.name()))
                continue
            
            value = widget.value()
            if ( value == IGNORED ):
                continue
            
            # check for required columns
            if ( column.required() and not value ):
                name = column.displayName()
                QMessageBox.information(self, 
                                        'Missing Required Field',
                                        '%s is a required field.' % name)
                return
            
            # check for unique columns
            elif ( column.unique() ):
                # check for uniqueness
                query        = Q(column.name()) == value
                if ( record.isRecord() ):
                    query   &= Q(self._model) != record
                
                columns  = self._model.schema().primaryColumns()
                result   = self._model.select(columns = columns, where = query)
                
                if ( result.total() ):
                    QMessageBox.information(self,
                                            'Duplicate Entry',
                                            '%s already exists.' % value)
                    return
            
            save_data.append((column, value))
        
        # record the properties for the record
        for column, value in save_data:
            record.setRecordValue(column.name(), value)
        
        self._record = record
        
        self.saved.emit()
    
    def schema( self ):
        """
        Returns the schema instance linked to this object.
        
        :return     <orb.TableSchema> || None
        """
        if ( self._model ):
            return self._model.schema()
        return None
    
    def setRecord( self, record ):
        """
        Sets the record instance for this widget to the inputed record.
        
        :param      record | <orb.Table>
        """
        self._record = record
        
        if ( not record ):
            return
        
        self.setModel(record.__class__)
        schema = self.model().schema()
        
        # set the information
        column_edits = self.findChildren(XOrbColumnEdit)
        for widget in column_edits:
            columnName  = widget.columnName()
            column      = schema.column(columnName)
            
            if ( not column ):
                logger.warning('%s is not a valid column of %s.' % \
                               (columnName, schema.name()))
                continue
                
            # update the values
            widget.setValue(record.recordValue(columnName))
            
    def setModel( self, model ):
        """
        Defines the model that is going to be used to define the interface for
        this widget.
        
        :param      model | <subclass of orb.Table>
        """
        if model == self._model:
            return False
            
        self._model = model
        
        if not self._record and model:
            self._record = model()
        
        if model:
            uifile = model.schema().property('uifile')
            if ( uifile ):
                self.setUiFile(uifile)
        
        self.rebuild()
        
        return True
    
    def setUiFile( self, uifile ):
        """
        Sets the ui file that will be loaded for this record edit.
        
        :param      uifile | <str>
        """
        self._uifile = uifile
    
    def uiFile( self ):
        """
        Returns the ui file that is assigned to this edit.
        
        :return     <str>
        """
        return self._uifile
    
    @classmethod
    def edit( cls, record, parent = None, uifile = '', commit = True ):
        """
        Prompts the user to edit the inputed record.
        
        :param      record | <orb.Table>
                    parent | <QWidget>
        
        :return     <bool> | accepted
        """
        # create the dialog
        dlg = QDialog(parent)
        dlg.setWindowTitle('Edit %s' % record.schema().name())
        
        # create the widget
        cls    = record.schema().property('widgetClass', cls)
        
        widget = cls(dlg)
        
        if ( uifile ):
            widget.setUiFile(uifile)
        
        widget.setRecord(record)
        widget.layout().setContentsMargins(0, 0, 0, 0)
        
        # create buttons
        opts = QDialogButtonBox.Save | QDialogButtonBox.Cancel
        btns = QDialogButtonBox(opts, Qt.Horizontal, dlg)
        
        # create layout
        layout = QVBoxLayout()
        layout.addWidget(widget)
        layout.addWidget(btns)
        
        dlg.setLayout(layout)
        dlg.adjustSize()
        
        # create connections
        #btns.accepted.connect(widget.save)
        btns.rejected.connect(dlg.reject)
        widget.saved.connect(dlg.accept)
        
        if ( dlg.exec_() ):
            if commit:
                result = widget.record().commit()
                if 'errored' in result:
                    QMessageBox.information(self.window(),
                                            'Error Committing to Database',
                                            result['errored'])
                    return False
            return True
        return False
    
    @classmethod
    def create( cls, model, parent = None, uifile = '', commit = True ):
        """
        Prompts the user to create a new record for the inputed table.
        
        :param      model   | <subclass of orb.Table>
                    parent  | <QWidget>
        
        :return     <orb.Table> || None/ | instance of the inputed table class
        """
        # create the dialog
        dlg = QDialog(parent)
        dlg.setWindowTitle('Create %s' % model.schema().name())
        
        # create the widget
        cls    = model.schema().property('widgetClass', cls)
        widget = cls(dlg)
        
        if ( uifile ):
            widget.setUiFile(uifile)
        
        widget.setModel(model)
        widget.layout().setContentsMargins(0, 0, 0, 0)
        
        # create buttons
        opts = QDialogButtonBox.Save | QDialogButtonBox.Cancel
        btns = QDialogButtonBox(opts, Qt.Horizontal, dlg)
        
        # create layout
        layout = QVBoxLayout()
        layout.addWidget(widget)
        layout.addWidget(btns)
        
        dlg.setLayout(layout)
        dlg.adjustSize()
        
        # create connections
        btns.accepted.connect(widget.save)
        btns.rejected.connect(dlg.reject)
        widget.saved.connect(dlg.accept)
        
        if ( dlg.exec_() ):
            record = widget.record()
            if ( commit ):
                record.commit()
            return record
        return None
Exemple #21
0
class XOrbRecordWidget(QWidget):
    """ """
    __designer_group__ = 'ProjexUI - ORB'

    saved = Signal()
    aboutToSave = Signal()
    aboutToSaveRecord = Signal(object)
    recordSaved = Signal(object)
    resizeRequested = Signal()

    _tableType = None

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

        # define custom properties
        self._record = None
        self._autoCommitOnSave = False
        self._multipleCreateEnabled = True
        self._savedColumnsOnReset = []
        self._saveSignalBlocked = False

    def autoCommitOnSave(self):
        """
        Returns whether or not this widget should automatically commit changes
        when the widget is saved.
        
        :return     <bool>
        """
        return self._autoCommitOnSave

    def fitToContents(self):
        """
        Adjusts the size for this widget.
        """
        self.adjustSize()
        self.resizeRequested.emit()

    def loadValues(self, values):
        """
        Loads the values from the inputed dictionary to the widget.
        
        :param      values | <dict>
        """
        table = self.tableType()
        if table:
            schema = table.schema()
        else:
            schema = None

        process = []
        for widget in self.findChildren(QWidget):
            prop = widget.property('columnName')
            if not prop:
                continue

            order = widget.property('columnOrder')
            if order:
                order = unwrapVariant(order)
            else:
                order = 10000000

            process.append((order, widget, prop))

        process.sort()
        for order, widget, prop in process:
            columnName = str(unwrapVariant(prop, ''))

            if not columnName:
                continue

            if isinstance(widget, XEnumBox) and schema:
                column = schema.column(columnName)
                if column.enum() is not None:
                    widget.setEnum(column.enum())

            if columnName in values:
                projexui.setWidgetValue(widget, values.get(columnName))

    def multipleCreateEnabled(self):
        """
        Returns whether or not multiple records can be created in succession
        for this widget.
        
        :return     <bool>
        """
        return self._multipleCreateEnabled

    def record(self):
        """
        Returns the record linked with this widget.
        
        :return     <orb.Table>
        """
        return self._record

    def reset(self):
        """
        Resets this widget's data to a new class an reinitializes.  This method
        needs to have a table type defined for this widget to work.
        
        :sa     setTableType, tableType
        
        :return     <bool> | success
        """
        ttype = self.tableType()
        if (not ttype):
            return False

        values = self.saveValues()
        self.setRecord(ttype())
        restore_values = {}
        for column in self.savedColumnsOnReset():
            if column in restore_values:
                restore_values[column] = values[column]
        if restore_values:
            self.loadValues(restore_values)

        return True

    def saveValues(self):
        """
        Generates a dictionary of values from the widgets in this editor that
        will be used to save the values on its record.
        
        :return     {<str> columnName: <variant> value, ..}
        """
        output = {}

        for widget in self.findChildren(QWidget):
            prop = widget.property('columnName')
            if not prop:
                continue

            columnName = str(unwrapVariant(prop, ''))

            if (not columnName):
                continue

            value, success = projexui.widgetValue(widget)
            if (not success):
                continue

            # convert from values to standard python values
            if (isinstance(value, QDateTime)):
                value = value.toPyDateTime()
            elif (isinstance(value, QDate)):
                value = value.toPyDate()
            elif (isinstance(value, QTime)):
                value = value.toPyTime()
            elif (type(value).__name__ == 'QString'):
                value = str(value)

            output[columnName] = value

        return output

    def save(self):
        """
        Saves the changes from the ui to this widgets record instance.
        """
        record = self.record()
        if not record:
            logger.warning('No record has been defined for %s.' % self)
            return False

        if not self.signalsBlocked():
            self.aboutToSaveRecord.emit(record)
            self.aboutToSave.emit()

        values = self.saveValues()

        # ignore columns that are the same (fixes bugs in encrypted columns)
        check = values.copy()
        for column_name, value in check.items():
            if (value == record.recordValue(column_name)):
                check.pop(column_name)

        # check to see if nothing has changed
        if (not check and record.isRecord()):
            if not self.signalsBlocked():
                self.recordSaved.emit(record)
                self.saved.emit()
                self._saveSignalBlocked = False
            else:
                self._saveSignalBlocked = True

            if self.autoCommitOnSave():
                status, result = record.commit()
                if status == 'errored':
                    if 'db_error' in result:
                        msg = str(result['db_error'])
                    else:
                        msg = 'An unknown database error has occurred.'

                    QMessageBox.information(self, 'Commit Error', msg)
                    return False
            return True

        # validate the modified values
        success, msg = record.validateValues(check)
        if (not success):
            QMessageBox.information(None, 'Could Not Save', msg)
            return False

        record.setRecordValues(**values)
        success, msg = record.validateRecord()
        if not success:
            QMessageBox.information(None, 'Could Not Save', msg)
            return False

        if (self.autoCommitOnSave()):
            result = record.commit()
            if 'errored' in result:
                QMessageBox.information(None, 'Could Not Save', msg)
                return False

        if (not self.signalsBlocked()):
            self.recordSaved.emit(record)
            self.saved.emit()
            self._saveSignalBlocked = False
        else:
            self._saveSignalBlocked = True

        return True

    def saveSignalBlocked(self):
        """
        Returns whether or not a save has occurred silently, and the signal
        has been surpressed.
        
        :return     <bool>
        """
        return self._saveSignalBlocked

    def saveSilent(self):
        """
        Saves the record, but does not emit the saved signal.  This method
        is useful when chaining together multiple saves.  Check the 
        saveSignalBlocked value to know if it was muted to know if any
        saves occurred.
        
        :return     <bool>
        """
        self.blockSignals(True)
        success = self.save()
        self.blockSignals(False)
        return success

    def savedColumnsOnReset(self):
        """
        Returns a list of the columns that should be stored when the widget
        is reset.
        
        :return     [<str>, ..]
        """
        return self._savedColumnsOnReset

    def setAutoCommitOnSave(self, state):
        """
        Sets whether or not the widget will automatically commit changes when
        the widget is saved.
        
        :param      state | <bool>
        """
        self._autoCommitOnSave = state

    def setMultipleCreateEnabled(self, state):
        """
        Sets whether or not multiple creation is enabled for this widget.
        
        :param      state | <bool>
        """
        self._multipleCreateEnabled = state

    def setRecord(self, record):
        """
        Sets the record instance linked with this widget.
        
        :param      record | <orb.Table>
        """
        self._record = record
        if record is not None:
            self.loadValues(record.recordValues(autoInflate=True))
        else:
            self.loadValues({})

    def setSavedColumnsOnReset(self, columns):
        """
        Sets a list of the columns that should be stored when the widget
        is reset.
        
        :param      columns | [<str>, ..]
        """
        self._savedColumnsOnReset = columns

    @classmethod
    def getDialog(cls, name, parent=None):
        """
        Generates a dialog for this class widget and returns it.
        
        :param      parent | <QtGui.QWidget> || None
        
        :return     <QtGui.QDialog>
        """
        key = '_{0}__{1}_dialog'.format(cls.__name__, name)
        dlg = getattr(cls, key, None)

        if dlg is not None:
            return dlg

        if parent is None:
            parent = QApplication.activeWindow()
        dlg = QDialog(parent)

        # create widget
        widget = cls(dlg)
        dlg.__dict__['_mainwidget'] = widget
        widget.layout().setContentsMargins(0, 0, 0, 0)

        # create buttons
        opts = QDialogButtonBox.Save | QDialogButtonBox.Cancel
        buttons = QDialogButtonBox(opts, Qt.Horizontal, dlg)

        # create layout
        layout = QVBoxLayout()
        layout.addWidget(widget)
        layout.addWidget(buttons)
        dlg.setLayout(layout)
        dlg.resize(widget.minimumSize() + QSize(15, 15))
        widget.resizeRequested.connect(dlg.adjustSize)

        # create connections
        buttons.accepted.connect(widget.save)
        buttons.rejected.connect(dlg.reject)
        widget.saved.connect(dlg.accept)
        widget.setFocus()

        dlg.adjustSize()
        if parent and parent.window():
            center = parent.window().geometry().center()
            dlg.move(center.x() - dlg.width() / 2.0,
                     center.y() - dlg.height() / 2.0)

        setattr(cls, key, dlg)
        return dlg

    @classmethod
    def edit(cls, record, parent=None, autoCommit=True, title=''):
        """
        Prompts the user to edit the inputed record.
        
        :param      record | <orb.Table>
                    parent | <QWidget>
        
        :return     <bool> | accepted
        """
        dlg = cls.getDialog('edit', parent)

        # update the title
        try:
            name = record.schema().displayName()
        except AttributeError:
            name = record.__class__.__name__

        if not title:
            title = 'Edit {0}'.format(name)
        dlg.setWindowTitle(title)

        widget = dlg._mainwidget
        widget.setRecord(record)
        widget.setAutoCommitOnSave(autoCommit)

        result = dlg.exec_()

        return result == 1

    @classmethod
    def create(cls,
               model=None,
               parent=None,
               autoCommit=True,
               defaults=None,
               title=''):
        """
        Prompts the user to create a new record.
        
        :param      model  | <subclass of orb.Table>
                    parent | <QWidget>
                    autoCommit | <bool>
                    defaults | <dict> || None
        
        :return     <orb.Table> || None
        """
        if model is None:
            model = cls.tableType()
            if model is None:
                return None

        dlg = cls.getDialog('create', parent)

        # create dialog
        if not title:
            title = 'Create {0}'.format(type(model).__name__)
        dlg.setWindowTitle(title)

        widget = dlg._mainwidget
        widget.setRecord(model())
        widget.setAutoCommitOnSave(autoCommit)

        if defaults:
            widget.loadValues(defaults)
        else:
            widget.loadValues({})

        result = dlg.exec_()
        if result or widget.saveSignalBlocked():
            output = widget.record()
        else:
            output = None

        return output

    @classmethod
    def setTableType(cls, tableType):
        """
        Sets the table type for this widget to the inputed type.
        
        :param      tableType | <subclass of orb.Table>
        """
        cls._tableType = tableType

    @classmethod
    def tableType(cls):
        """
        Returns the table type for this widget.
        
        :return     <subclass of orb.Table> || None
        """
        return cls._tableType
class XQueryBuilderWidget(QWidget):
    """ """
    saveRequested = Signal()
    resetRequested = Signal()
    cancelRequested = Signal()

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

        # load the user interface
        projexui.loadUi(__file__, self)

        self.setMinimumWidth(470)

        # define custom properties
        self._rules = {}
        self._defaultQuery = []
        self._completionTerms = []
        self._minimumCount = 1

        # set default properties
        self._container = QWidget(self)
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(2)
        layout.addStretch(1)
        self._container.setLayout(layout)
        self.uiQueryAREA.setWidget(self._container)

        # create connections
        self.uiResetBTN.clicked.connect(self.emitResetRequested)
        self.uiSaveBTN.clicked.connect(self.emitSaveRequested)
        self.uiCancelBTN.clicked.connect(self.emitCancelRequested)

        self.resetRequested.connect(self.reset)

    def addLineWidget(self, query=None):
        """
        Adds a new line widget to the system with the given values.
        
        :param      query | (<str> term, <str> operator, <str> vlaue) || None
        """
        widget = XQueryLineWidget(self)
        widget.setTerms(sorted(self._rules.keys()))
        widget.setQuery(query)

        index = self._container.layout().count() - 1
        self._container.layout().insertWidget(index, widget)

        widget.addRequested.connect(self.addLineWidget)
        widget.removeRequested.connect(self.removeLineWidget)

        # update the remove enabled options for these widgets
        self.updateRemoveEnabled()

    def addRule(self, rule):
        """
        Adds a rule to the system.
        
        :param      rule | <XQueryRule>
        """
        self._rules[rule.term()] = rule
        self.updateRules()

    def clear(self):
        """
        Clears out all the widgets from the system.
        """
        for lineWidget in self.lineWidgets():
            lineWidget.setParent(None)
            lineWidget.deleteLater()

    def completionTerms(self):
        """
        Returns the list of terms that will be used as a global override
        for completion terms when the query rule generates a QLineEdit instance.
        
        :return     [<str>, ..]
        """
        return self._completionTerms

    def count(self):
        """
        Returns the count of the line widgets in the system.
        
        :return     <int>
        """
        return len(self.lineWidgets())

    def currentQuery(self):
        """
        Returns the current query string for this widget.
        
        :return     [(<str> term, <str> operator, <str> value), ..]
        """
        widgets = self.lineWidgets()
        output = []
        for widget in widgets:
            output.append(widget.query())
        return output

    def defaultQuery(self):
        """
        Returns the default query for the system.
        
        :return     [(<str> term, <str> operator, <str> value), ..]
        """
        return self._defaultQuery

    def keyPressEvent(self, event):
        """
        Emits the save requested signal for this builder for when the enter
        or return press is clicked.
        
        :param      event | <QKeyEvent>
        """
        if (event.key() in (Qt.Key_Enter, Qt.Key_Return)):
            self.emitSaveRequested()

        super(XQueryBuilderWidget, self).keyPressEvent(event)

    def emitCancelRequested(self):
        """
        Emits the cancel requested signal.
        """
        if (not self.signalsBlocked()):
            self.cancelRequested.emit()

    def emitResetRequested(self):
        """
        Emits the reste requested signal.
        """
        if (not self.signalsBlocked()):
            self.resetRequested.emit()

    def emitSaveRequested(self):
        """
        Emits the save requested signal.
        """
        if (not self.signalsBlocked()):
            self.saveRequested.emit()

    def findRule(self, term):
        """
        Looks up a rule by the inputed term.
        
        :param      term | <str>
        
        :return     <XQueryRule> || None
        """
        return self._rules.get(nativestring(term))

    def removeLineWidget(self, widget):
        """
        Removes the line widget from the query.
        
        :param      widget | <XQueryLineWidget>
        """
        widget.setParent(None)
        widget.deleteLater()

        self.updateRemoveEnabled()

    def minimumCount(self):
        """
        Defines the minimum number of query widgets that are allowed.
        
        :return     <int>
        """
        return self._minimumCount

    def lineWidgets(self):
        """
        Returns a list of line widgets for this system.
        
        :return     [<XQueryLineWidget>, ..]
        """
        return self.findChildren(XQueryLineWidget)

    def reset(self):
        """
        Resets the system to the default query.
        """
        self.setCurrentQuery(self.defaultQuery())

    def setCompletionTerms(self, terms):
        """
        Sets the list of terms that will be used as a global override
        for completion terms when the query rule generates a QLineEdit instance.
        
        :param     terms | [<str>, ..]
        """
        self._completionTerms = terms

    def setCurrentQuery(self, query):
        """
        Sets the query for this system to the inputed query.
        
        :param      query | [(<str> term, <str> operator, <str> value), ..]
        """
        self.clear()

        for entry in query:
            self.addLineWidget(entry)

        # make sure we have the minimum number of widgets
        for i in range(self.minimumCount() - len(query)):
            self.addLineWidget()

    def setDefaultQuery(self, query):
        """
        Sets the default query that will be used when the user clicks on the \
        reset button or the reset method is called.
        
        :param      query | [(<str> term, <str> operator, <str> value), ..]
        """
        self._defaultQuery = query[:]

    def setMinimumCount(self, count):
        """
        Sets the minimum number of line widgets that are allowed at any \
        given time.
        
        :param      count | <int>
        """
        self._minimumCount = count

    def setRules(self, rules):
        """
        Sets all the rules for this builder.
        
        :param      rules | [<XQueryRule>, ..]
        """
        if (type(rules) in (list, tuple)):
            self._rules = dict([(x.term(), x) for x in rules])
            self.updateRules()
            return True

        elif (type(rules) == dict):
            self._rules = rules.copy()
            self.updateRules()
            return True

        else:
            return False

    def setTerms(self, terms):
        """
        Sets a simple rule list by accepting a list of strings for terms.  \
        This is a convenience method for the setRules method.
        
        :param      rules | [<str> term, ..]
        """
        return self.setRules([XQueryRule(term=term) for term in terms])

    def updateRemoveEnabled(self):
        """
        Updates the remove enabled baesd on the current number of line widgets.
        """
        lineWidgets = self.lineWidgets()
        count = len(lineWidgets)
        state = self.minimumCount() < count

        for widget in lineWidgets:
            widget.setRemoveEnabled(state)

    def updateRules(self):
        """
        Updates the query line items to match the latest rule options.
        """
        terms = sorted(self._rules.keys())
        for child in self.lineWidgets():
            child.setTerms(terms)
Exemple #23
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 #24
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 XRolloutWidget(QScrollArea):
    """ """
    itemCollapsed = Signal(int)
    itemExpanded = Signal(int)

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

        # define custom properties
        self.setWidgetResizable(True)

        # set default properties
        widget = QWidget(self)
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(3)
        layout.addStretch(1)
        widget.setLayout(layout)

        self.setWidget(widget)

    def clear(self):
        """
        Clears out all of the rollout items from the widget.
        """
        self.blockSignals(True)
        self.setUpdatesEnabled(False)
        for child in self.findChildren(XRolloutItem):
            child.setParent(None)
            child.deleteLater()
        self.setUpdatesEnabled(True)
        self.blockSignals(False)

    def addRollout(self, widget, title, expanded=False):
        """
        Adds a new widget to the rollout system.
        
        :param      widget      | <QWidget>
                    title       | <str>
                    expanded    | <bool>
        
        :return     <XRolloutItem>
        """
        layout = self.widget().layout()
        item = XRolloutItem(self, widget, title, expanded)
        layout.insertWidget(layout.count() - 1, item)
        return item

    def count(self):
        """
        Returns the number of items that are associated with this rollout.
        
        :return     <int>
        """
        return self.widget().layout().count() - 1

    def itemAt(self, index):
        """
        Returns the rollout item at the inputed index.
        
        :return     <XRolloutItem> || None
        """
        layout = self.widget().layout()
        if (0 <= index and index < (layout.count() - 1)):
            return layout.itemAt(index).widget()
        return None

    def items(self):
        """
        Returns all the rollout items for this widget.
        
        :return     [<XRolloutItem>, ..]
        """
        layout = self.widget().layout()
        return [layout.itemAt(i).widget() for i in range(layout.count() - 1)]

    def takeAt(self, index):
        """
        Removes the widget from the rollout at the inputed index.
        
        :param      index | <int>
        
        :return     <QWidget> || None
        """
        layout = self.widget().layout()
        item = layout.takeAt(index)
        if (not item):
            return None

        return item.widget().widget()
class XHistoryStack(QtCore.QObject):
    currentIndexChanged = Signal(int)
    currentUrlChanged = Signal(str)

    def __init__(self, parent=None):
        super(XHistoryStack, self).__init__(parent)
        self._blockStack = False
        self._stack = []
        self._titles = {}
        self._homeUrl = ''
        self._currentIndex = -1
        self._maximum = 20

    def backUrl(self, count=1):
        """
        Returns the url that will be used when traversing backward.
        
        :return     <str>
        """
        try:
            return self._stack[self._currentIndex + count]
        except IndexError:
            return ''

    def clear(self):
        """
        Clears the current history.
        """
        self._stack = []
        self._titles = {}
        self._currentIndex = -1
        self.emitCurrentChanged()

    def count(self):
        """
        Returns the count for all the urls in the system.
        
        :return     <int>
        """
        return len(self._stack)

    def currentIndex(self):
        """
        Returns the current index for the history stack.
        
        :return     <int>
        """
        return self._currentIndex

    def currentUrl(self):
        """
        Returns the current url path for the history stack.
        
        :return     <str>
        """
        return self.urlAt(self.currentIndex())

    def emitCurrentChanged(self):
        """
        Emits the current index changed signal provided signals are not blocked.
        """
        if (not self.signalsBlocked()):
            self.currentIndexChanged.emit(self.currentIndex())
            self.currentUrlChanged.emit(self.currentUrl())

    def forwardUrl(self, count=1):
        """
        Returns the url that will be used when traversing backward.
        
        :return     <str>
        """
        try:
            return self._stack[self._currentIndex - count]
        except IndexError:
            return ''

    def future(self):
        """
        Shows all the future - all the urls from after the current index \
        in the stack.
        
        :return     [ <str>, .. ]
        """
        return self._stack[:self._currentIndex]

    def goBack(self):
        """
        Goes up one level if possible and returns the url at the current level.
        If it cannot go up, then a blank string will be returned.
        
        :return     <str>
        """
        index = self._currentIndex + 1
        if (index == len(self._stack)):
            return ''

        self._blockStack = True
        self._currentIndex = index
        self.emitCurrentChanged()
        self._blockStack = False
        return self.currentUrl()

    def goHome(self):
        """
        Goes to the home url.  If there is no home url specifically set, then \
        this will go to the first url in the history.  Otherwise, it will \
        look to see if the home url is in the stack and go to that level, if \
        the home url is not found, then it will be pushed to the top of the \
        stack using the push method.
        """
        self._blockStack = True
        url = self.homeUrl()
        if (url and url in self._stack):
            self._currentIndex = self._stack.index(url)
            self.emitCurrentChanged()
        elif (url):
            self.push(url)
        else:
            self._currentIndex = len(self._stack) - 1
            self.emitCurrentChanged()
        self._blockStack = False

    def goForward(self):
        """
        Goes down one level if possible and returns the url at the current \
        level.  If it cannot go down, then a blank string will be returned.
        
        :return     <str>
        """
        index = self._currentIndex - 1
        if (index < 0):
            return ''

        self._blockStack = True
        self._currentIndex = index
        self.emitCurrentChanged()
        self._blockStack = False
        return self.currentUrl()

    def history(self):
        """
        Shows all the history - all the urls from before the current index \
        in the stack.
        
        :return     [ <str>, .. ]
        """
        return self._stack[self._currentIndex + 1:]

    def hasHistory(self):
        """
        Returns whether or not there is currently history in the system.
        
        :return     <bool>
        """
        return self._currentIndex < (len(self._stack) - 1)

    def hasFuture(self):
        """
        Returns whether or not there are future urls in the system.
        
        :return     <bool>
        """
        return self._currentIndex > 0

    def homeUrl(self):
        """
        Returns the home url for this stack instance.
        
        :return     <str>
        """
        return self._homeUrl

    def indexOf(self, url):
        """
        Returns the index of the inputed url for this stack.  If the url is \
        not found, then -1 is returned.
        
        :param      url | <str>
        
        :return     <int>
        """
        url = str(url)
        if (url in self._stack):
            return self._stack.index(url)
        return -1

    def maximum(self):
        """
        Returns the maximum number of urls that should be stored in history.
        
        :return     <int>
        """
        return self._maximum

    def push(self, url, title=''):
        """
        Pushes the url into the history stack at the current index.
        
        :param      url | <str>
        
        :return     <bool> | changed
        """
        # ignore refreshes of the top level
        if self.currentUrl() == url or self._blockStack:
            return False

        new_stack = self._stack[self._currentIndex:]
        new_stack.insert(0, str(url))
        self._stack = new_stack[:self.maximum()]
        self._currentIndex = 0
        self._titles[str(url)] = title
        self.emitCurrentChanged()
        return True

    def setCurrentIndex(self, index):
        """
        Sets the current index for the history stack.
        
        :param      index | <int>
        
        :return     <bool> | success
        """
        if (0 <= index and index < len(self._stack)):
            self._currentIndex = index
            self.emitCurrentChanged()
            return True
        return False

    def setCurrentUrl(self, url):
        """
        Sets the current url from within the history.  If the url is not \
        found, then nothing will happen.  Use the push method to add new \
        urls to the stack.
        
        :param      url | <str>
        """
        url = str(url)
        if (not url in self._stack):
            return False

        self._blockStack = True
        self.setCurrentIndex(self._stack.index(url))
        self._blockStack = False

        return True

    def setHomeUrl(self, url):
        """
        Defines the home url for this history stack.
        
        :param      url | <str>
        """
        self._homeUrl = url

    def setMaximum(self, maximum):
        """
        Sets the maximum number of urls to store in history.
        
        :param      maximum | <str>
        """
        self._maximum = maximum
        self._stack = self._stack[:maximum]

        if (maximum <= self._currentIndex):
            self._currentIndex = maximum - 1

    def urlAt(self, index):
        """
        Returns the url at the inputed index wihtin the stack.  If the index \
        is invalid, then a blank string is returned.
        
        :return     <str>
        """
        if (0 <= index and index < len(self._stack)):
            return self._stack[index]
        return ''

    def urls(self):
        """
        Returns a list of the urls in this history stack.
        
        :return     [<str>, ..]
        """
        return self._stack

    def titleOf(self, url):
        """
        Returns the title for the inputed url.
        
        :param      url | <str>
        
        :return     <str>
        """
        title = self._titles.get(str(url))
        if not title:
            title = str(url).split('/')[-1]

        return title
Exemple #27
0
class XWalkthroughWidget(QtGui.QWidget):
    finished = Signal()

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

        # setup the properties
        self.setAutoFillBackground(True)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.setMouseTracking(True)

        # install the event filter
        parent.installEventFilter(self)

        # define child widgets
        self._direction = QtGui.QBoxLayout.TopToBottom
        self._slideshow = XStackedWidget(self)
        self._previousButton = XWalkthroughButton('Previous', self)
        self._nextButton = XWalkthroughButton('Finish', self)
        self._previousButton.hide()

        self.resize(parent.size())

        # setup look for the widget
        clr = QtGui.QColor('black')
        clr.setAlpha(120)

        palette = self.palette()
        palette.setColor(palette.Window, clr)
        palette.setColor(palette.WindowText, QtGui.QColor('white'))
        self.setPalette(palette)

        # create connections
        self._slideshow.currentChanged.connect(self.updateUi)
        self._previousButton.clicked.connect(self.goBack)
        self._nextButton.clicked.connect(self.goForward)

    def addSlide(self, slide):
        """
        Adds a new slide to the widget.
        
        :param      slide | <XWalkthroughSlide>
        
        :return     <QtGui.QGraphicsView>
        """
        # create the scene
        scene = XWalkthroughScene(self)
        scene.setReferenceWidget(self.parent())
        scene.load(slide)

        # create the view
        view = QtGui.QGraphicsView(self)
        view.setCacheMode(view.CacheBackground)
        view.setScene(scene)
        view.setStyleSheet('background: transparent')
        view.setFrameShape(view.NoFrame)
        view.setInteractive(False)
        view.setFocusPolicy(QtCore.Qt.NoFocus)
        view.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
        view.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
        view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

        # add the slide
        self._slideshow.addWidget(view)
        self.updateUi()

        return view

    def autoLayout(self):
        """
        Automatically lays out the contents for this widget.
        """
        try:
            direction = self.currentSlide().scene().direction()
        except AttributeError:
            direction = QtGui.QBoxLayout.TopToBottom

        size = self.size()
        self._slideshow.resize(size)

        prev = self._previousButton
        next = self._nextButton

        if direction == QtGui.QBoxLayout.BottomToTop:
            y = 9
        else:
            y = size.height() - prev.height() - 9

        prev.move(9, y)
        next.move(size.width() - next.width() - 9, y)

        # update the layout for the slides
        for i in range(self._slideshow.count()):
            widget = self._slideshow.widget(i)
            widget.scene().autoLayout(size)

    def cancel(self):
        """
        Hides/exits the walkthrough.
        """
        self.hide()

    def clear(self):
        """
        Clears the content for this widget.
        """
        for i in range(self._slideshow.count()):
            widget = self._slideshow.widget(0)
            widget.close()
            widget.setParent(None)
            widget.deleteLater()

        self.updateUi()

    def currentSlide(self):
        """
        Returns the current slide that is being displayed for this walkthrough.
        
        :return     <QtGui.QGraphicsView>
        """
        return self._slideshow.currentWidget()

    def eventFilter(self, obj, event):
        """
        Filters the parent object for its resize event.
        
        :param      obj     | <QtCore.QObject>
                    event   | <QtCore.QEvent>
        
        :return     <bool> | consumed
        """
        if event.type() == event.Resize:
            self.resize(event.size())

        return False

    def goBack(self):
        """
        Moves to the previous slide.
        """
        self._slideshow.slideInPrev()

    def goForward(self):
        """
        Moves to the next slide or finishes the walkthrough.
        """
        if self._slideshow.currentIndex() == self._slideshow.count() - 1:
            self.finished.emit()
        else:
            self._slideshow.slideInNext()

    def keyPressEvent(self, event):
        """
        Listens for the left/right keys and the escape key to control
        the slides.
        
        :param      event | <QtCore.Qt.QKeyEvent>
        """
        if event.key() == QtCore.Qt.Key_Escape:
            self.cancel()
        elif event.key() == QtCore.Qt.Key_Left:
            self.goBack()
        elif event.key() == QtCore.Qt.Key_Right:
            self.goForward()
        elif event.key() == QtCore.Qt.Key_Home:
            self.restart()

        super(XWalkthroughWidget, self).keyPressEvent(event)

    def load(self, walkthrough):
        """
        Loads the XML text for a new walkthrough.
        
        :param      walkthrough | <XWalkthrough> || <str> || <xml.etree.ElementTree.Element>
        """
        if type(walkthrough) in (str, unicode):
            walkthrough = XWalkthrough.load(walkthrough)

        self.setUpdatesEnabled(False)
        self.clear()
        for slide in walkthrough.slides():
            self.addSlide(slide)
        self.setUpdatesEnabled(True)
        self.updateUi()

    def restart(self):
        """
        Restarts this walkthrough from the beginning.
        """
        self._slideshow.setCurrentIndex(0)

    def resizeEvent(self, event):
        """
        Moves the widgets around the system.
        
        :param      event | <QtGui.QResizeEvent>
        """
        super(XWalkthroughWidget, self).resizeEvent(event)

        if self.isVisible():
            self.autoLayout()

    def updateUi(self):
        """
        Updates the interface to show the selection buttons.
        """
        index = self._slideshow.currentIndex()
        count = self._slideshow.count()

        self._previousButton.setVisible(index != 0)
        self._nextButton.setText('Finish' if index == count - 1 else 'Next')
        self.autoLayout()

    def mouseReleaseEvent(self, event):
        """
        Moves the slide forward when clicked.
        
        :param      event | <QtCore.QMouseEvent>
        """
        if event.button() == QtCore.Qt.LeftButton:
            if event.modifiers() == QtCore.Qt.ControlModifier:
                self.goBack()
            else:
                self.goForward()

        super(XWalkthroughWidget, self).mouseReleaseEvent(event)

    def showEvent(self, event):
        """
        Raises this widget when it is shown.
        
        :param      event | <QtCore.QShowEvent>
        """
        super(XWalkthroughWidget, self).showEvent(event)

        self.autoLayout()
        self.restart()
        self.setFocus()
        self.raise_()
Exemple #28
0
class XToolBar(QToolBar):
    collapseToggled = Signal(bool)

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

        # set custom properties
        self._collapseButton = None
        self._collapsable = True
        self._collapsed = True
        self._collapsedSize = 14
        self._autoCollapsible = False
        self._precollapseSize = None
        self._shadowed = False
        self._colored = False

        # set standard options
        self.layout().setSpacing(0)
        self.layout().setContentsMargins(1, 1, 1, 1)
        self.setMovable(False)
        self.clear()
        self.setMouseTracking(True)
        self.setOrientation(Qt.Horizontal)
        self.setCollapsed(False)

    def autoCollapsible(self):
        """
        Returns whether or not this toolbar is auto-collapsible.  When
        True, it will enter its collapsed state when the user hovers out
        of the bar.
        
        :return     <bool>
        """
        return self._autoCollapsible

    def clear(self):
        """
        Clears out this toolbar from the system.
        """
        # preserve the collapse button
        super(XToolBar, self).clear()

        # clears out the toolbar
        if self.isCollapsable():
            self._collapseButton = QToolButton(self)
            self._collapseButton.setAutoRaise(True)
            self._collapseButton.setSizePolicy(QSizePolicy.Expanding,
                                               QSizePolicy.Expanding)

            self.addWidget(self._collapseButton)
            self.refreshButton()

            # create connection
            self._collapseButton.clicked.connect(self.toggleCollapsed)

        elif self._collapseButton:
            self._collapseButton.setParent(None)
            self._collapseButton.deleteLater()
            self._collapseButton = None

    def count(self):
        """
        Returns the number of actions linked with this toolbar.
        
        :return     <int>
        """
        return len(self.actions())

    def collapseButton(self):
        """
        Returns the collapsing button for this toolbar.
        
        :return     <QToolButton>
        """
        return self._collapseButton

    def isCollapsable(self):
        """
        Returns whether or not this toolbar is collapsable.
        
        :return     <bool>
        """
        return self._collapsable

    def isCollapsed(self):
        """
        Returns whether or not this toolbar is in a collapsed state.
        
        :return     <bool>
        """
        return self._collapsed and self.isCollapsable()

    def isColored(self):
        """
        Returns whether or not to colorize the buttons on the toolbar
        when they are highlighted.
        
        :return     <bool>
        """
        return self._colored

    def isShadowed(self):
        """
        Returns whether or not to show this toolbar with shadows.
        
        :return     <bool>
        """
        return self._shadowed

    def refreshButton(self):
        """
        Refreshes the button for this toolbar.
        """
        collapsed = self.isCollapsed()
        btn = self._collapseButton

        if not btn:
            return

        btn.setMaximumSize(MAX_SIZE, MAX_SIZE)

        # set up a vertical scrollbar
        if self.orientation() == Qt.Vertical:
            btn.setMaximumHeight(12)
        else:
            btn.setMaximumWidth(12)

        icon = ''

        # collapse/expand a vertical toolbar
        if self.orientation() == Qt.Vertical:
            if collapsed:
                self.setFixedWidth(self._collapsedSize)
                btn.setMaximumHeight(MAX_SIZE)
                btn.setArrowType(Qt.RightArrow)
            else:
                self.setMaximumWidth(MAX_SIZE)
                self._precollapseSize = None
                btn.setMaximumHeight(12)
                btn.setArrowType(Qt.LeftArrow)

        else:
            if collapsed:
                self.setFixedHeight(self._collapsedSize)
                btn.setMaximumWidth(MAX_SIZE)
                btn.setArrowType(Qt.DownArrow)
            else:
                self.setMaximumHeight(1000)
                self._precollapseSize = None
                btn.setMaximumWidth(12)
                btn.setArrowType(Qt.UpArrow)

        for index in range(1, self.layout().count()):
            item = self.layout().itemAt(index)
            if not item.widget():
                continue

            if collapsed:
                item.widget().setMaximumSize(0, 0)
            else:
                item.widget().setMaximumSize(MAX_SIZE, MAX_SIZE)

        if not self.isCollapsable():
            btn.hide()
        else:
            btn.show()

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

        if not self._collapsed:
            if self.orientation() == Qt.Vertical:
                self._precollapseSize = self.width()
            else:
                self._precollapseSize = self.height()

    def setAutoCollapsible(self, state):
        """
        Sets whether or not this toolbar is auto-collapsible.
        
        :param      state | <bool>
        """
        self._autoCollapsible = state

    def setCollapsed(self, state):
        """
        Sets whether or not this toolbar is in a collapsed state.
        
        :return     <bool> changed
        """
        if state == self._collapsed:
            return False

        self._collapsed = state
        self.refreshButton()

        if not self.signalsBlocked():
            self.collapseToggled.emit(state)

        return True

    def setCollapsable(self, state):
        """
        Sets whether or not this toolbar is collapsable.
        
        :param      state | <bool>
        """
        if self._collapsable == state:
            return

        self._collapsable = state
        self.clear()

    def setOrientation(self, orientation):
        """
        Sets the orientation for this toolbar to the inputed value, and \
        updates the contents margins and collapse button based on the vaule.
        
        :param      orientation | <Qt.Orientation>
        """
        super(XToolBar, self).setOrientation(orientation)
        self.refreshButton()

    def setShadowed(self, state):
        """
        Sets whether or not this toolbar is shadowed.
        
        :param      state | <bool>
        """
        self._shadowed = state
        if state:
            self._colored = False

        for child in self.findChildren(XToolButton):
            child.setShadowed(state)

    def setColored(self, state):
        """
        Sets whether or not this toolbar is shadowed.
        
        :param      state | <bool>
        """
        self._colored = state
        if state:
            self._shadowed = False

        for child in self.findChildren(XToolButton):
            child.setColored(state)

    def toggleCollapsed(self):
        """
        Toggles the collapsed state for this toolbar.
        
        :return     <bool> changed
        """
        return self.setCollapsed(not self.isCollapsed())

    x_shadowed = Property(bool, isShadowed, setShadowed)
    x_colored = Property(bool, isColored, setColored)
Exemple #29
0
class XSerialEdit(QtGui.QWidget):
    returnPressed = Signal()

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

        # define custom properties
        self._sectionLength = 5
        self._readOnly = False
        self._editorHandlingBlocked = False

        # set standard values
        layout = QtGui.QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(4)
        self.setLayout(layout)
        self.setSectionCount(4)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding,
                           QtGui.QSizePolicy.Fixed)

    def blockEditorHandling(self, state):
        self._editorHandlingBlocked = state

    def clearSelection(self):
        """
        Clears the selected text for this edit.
        """
        first = None
        editors = self.editors()
        for editor in editors:
            if not editor.selectedText():
                continue

            first = first or editor
            editor.backspace()

        for editor in editors:
            editor.setFocus()

        if first:
            first.setFocus()

    @Slot()
    def copyAll(self):
        """
        Copies all of the text to the clipboard.
        """
        QtGui.QApplication.clipboard().setText(self.text())

    @Slot()
    def copy(self):
        """
        Copies the text from the serial to the clipboard.
        """
        QtGui.QApplication.clipboard().setText(self.selectedText())

    @Slot()
    def cut(self):
        """
        Cuts the text from the serial to the clipboard.
        """
        text = self.selectedText()
        for editor in self.editors():
            editor.cut()

        QtGui.QApplication.clipboard().setText(text)

    def currentEditor(self):
        """
        Returns the current editor or this widget based on the focusing.
        
        :return     <QtGui.QLineEdit>
        """
        for editor in self.editors():
            if editor.hasFocus():
                return editor
        return None

    def editors(self):
        """
        Returns the editors that are associated with this edit.
        
        :return     [<XLineEdit>, ..]
        """
        lay = self.layout()
        return [lay.itemAt(i).widget() for i in range(lay.count())]

    def editorAt(self, index):
        """
        Returns the editor at the given index.
        
        :param      index | <int>
        
        :return     <XLineEdit> || None
        """
        try:
            return self.layout().itemAt(index).widget()
        except AttributeError:
            return None

    def eventFilter(self, object, event):
        """
        Filters the events for the editors to control how the cursor
        flows between them.
        
        :param      object | <QtCore.QObject>
                    event  | <QtCore.QEvent>

        :return     <bool> | consumed
        """
        index = self.indexOf(object)
        pressed = event.type() == event.KeyPress
        released = event.type() == event.KeyRelease

        if index == -1 or \
           not (pressed or released) or \
           self.isEditorHandlingBlocked():
            return super(XSerialEdit, self).eventFilter(object, event)

        text = nativestring(event.text()).strip()

        # handle Ctrl+C (copy)
        if event.key() == QtCore.Qt.Key_C and \
           event.modifiers() == QtCore.Qt.ControlModifier and \
           pressed:
            self.copy()
            return True

        # handle Ctrl+X (cut)
        elif event.key() == QtCore.Qt.Key_X and \
             event.modifiers() == QtCore.Qt.ControlModifier and \
             pressed:
            if not self.isReadOnly():
                self.cut()
            return True

        # handle Ctrl+A (select all)
        elif event.key() == QtCore.Qt.Key_A and \
             event.modifiers() == QtCore.Qt.ControlModifier and \
             pressed:
            self.selectAll()
            return True

        # handle Ctrl+V (paste)
        elif event.key() == QtCore.Qt.Key_V and \
             event.modifiers() == QtCore.Qt.ControlModifier and \
             pressed:
            if not self.isReadOnly():
                self.paste()
            return True

        # ignore tab movements
        elif event.key() in (QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab):
            pass

        # delete all selected text
        elif event.key() == QtCore.Qt.Key_Backspace:
            sel_text = self.selectedText()
            if sel_text and not self.isReadOnly():
                self.clearSelection()
                return True

        # ignore modified keys
        elif not released:
            return super(XSerialEdit, self).eventFilter(object, event)

        # move to the previous editor
        elif object.cursorPosition() == 0:
            if event.key() in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Left):
                self.goBack()

        # move to next editor
        elif object.cursorPosition() == object.maxLength():
            valid_chars = string.ascii_letters + string.digits
            valid_text = text != '' and text in valid_chars

            if valid_text or event.key() == QtCore.Qt.Key_Right:
                self.goForward()

        return super(XSerialEdit, self).eventFilter(object, event)

    def goBack(self):
        """
        Moves the cursor to the end of the previous editor
        """
        index = self.indexOf(self.currentEditor())
        if index == -1:
            return

        previous = self.editorAt(index - 1)
        if previous:
            previous.setFocus()
            previous.setCursorPosition(self.sectionLength())

    def goForward(self):
        """
        Moves the cursor to the beginning of the next editor.
        """
        index = self.indexOf(self.currentEditor())
        if index == -1:
            return

        next = self.editorAt(index + 1)
        if next:
            next.setFocus()
            next.setCursorPosition(0)

    def hint(self):
        """
        Returns the hint that is used for the editors in this widget.
        
        :return     <str>
        """
        texts = []
        for editor in self.editors():
            text = editor.hint()
            if text:
                texts.append(nativestring(text))

        return ' '.join(texts)

    def indexOf(self, editor):
        """
        Returns the index of the inputed editor, or -1 if not found.
        
        :param      editor | <QtGui.QWidget>
        
        :return     <int>
        """
        lay = self.layout()
        for i in range(lay.count()):
            if lay.itemAt(i).widget() == editor:
                return i
        return -1

    def isEditorHandlingBlocked(self):
        return self._editorHandlingBlocked

    def isReadOnly(self):
        """
        Returns whether or not this edit is readonly.
        
        :return     <bool>
        """
        return self._readOnly

    @Slot()
    def paste(self):
        """
        Pastes text from the clipboard into the editors.
        """
        self.setText(QtGui.QApplication.clipboard().text())

    def showEvent(self, event):
        for editor in self.editors():
            editor.setFont(self.font())

        super(XSerialEdit, self).showEvent(event)

    def sectionCount(self):
        """
        Returns the number of editors that are a part of this serial edit.
        
        :return     <int>
        """
        return self.layout().count()

    def sectionLength(self):
        """
        Returns the number of characters available for each editor.
        
        :return     <int>
        """
        return self._sectionLength

    def selectedText(self):
        """
        Returns the selected text from the editors.
        
        :return     <str>
        """
        texts = []
        for editor in self.editors():
            text = editor.selectedText()
            if text:
                texts.append(nativestring(text))

        return ' '.join(texts)

    @Slot()
    def selectAll(self):
        """
        Selects the text within all the editors.
        """
        self.blockEditorHandling(True)
        for editor in self.editors():
            editor.selectAll()
        self.blockEditorHandling(False)

    def setHint(self, text):
        """
        Sets the hint to the inputed text.   The same hint will be used for
        all editors in this widget.
        
        :param      text | <str>
        """
        texts = nativestring(text).split(' ')

        for i, text in enumerate(texts):
            editor = self.editorAt(i)
            if not editor:
                break

            editor.setHint(text)

    def setReadOnly(self, state):
        """
        Sets whether or not this edit is read only.
        
        :param      state | <bool>
        """
        self._readOnly = state

        for editor in self.editors():
            editor.setReadOnly(state)

    def setSectionCount(self, count):
        """
        Sets the number of editors that the serial widget should have.
        
        :param      count | <int>
        """
        # cap the sections at 10
        count = max(1, min(count, 10))

        # create additional editors
        while self.layout().count() < count:
            editor = XLineEdit(self)
            editor.setFont(self.font())
            editor.setReadOnly(self.isReadOnly())
            editor.setHint(self.hint())
            editor.setAlignment(QtCore.Qt.AlignCenter)
            editor.installEventFilter(self)
            editor.setSizePolicy(QtGui.QSizePolicy.Expanding,
                                 QtGui.QSizePolicy.Expanding)
            editor.setMaxLength(self.sectionLength())
            editor.returnPressed.connect(self.returnPressed)
            self.layout().addWidget(editor)

        # remove unnecessary editors
        while count < self.layout().count():
            widget = self.layout().itemAt(0).widget()
            widget.close()
            widget.setParent(None)
            widget.deleteLater()

    def setSectionLength(self, length):
        """
        Sets the number of characters per section that are allowed.
        
        :param      length | <int>
        """
        self._sectionLength = length
        for editor in self.editors():
            editor.setMaxLength(length)

    @Slot()
    def setText(self, text):
        """
        Sets the text for this serial edit to the inputed text.
        
        :param      text | <str>
        """
        texts = nativestring(text).split(' ')

        for i, text in enumerate(texts):
            editor = self.editorAt(i)
            if not editor:
                break

            editor.setText(text)

    def text(self):
        """
        Returns the text from all the serials as text separated by a spacer.
        
        :return     <str>
        """
        texts = []
        for editor in self.editors():
            text = editor.text()
            if text:
                texts.append(nativestring(text))

        return ' '.join(texts)

    x_readOnly = Property(bool, isReadOnly, setReadOnly)
    x_sectionCount = Property(int, sectionCount, setSectionCount)
    x_sectionLength = Property(int, sectionLength, setSectionLength)
    x_hint = Property(str, hint, setHint)
    x_text = Property(str, text, setText)
Exemple #30
0
class XLabel(QLabel):
    aboutToEdit = Signal()
    editingCancelled = Signal()
    editingFinished = Signal(str)

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

        self._editable = False
        self._lineEdit = None
        self._editText = None

    @Slot()
    def acceptEdit(self):
        """
        Accepts the current edit for this label.
        """
        if not self._lineEdit:
            return

        self.setText(self._lineEdit.text())
        self._lineEdit.hide()

        if not self.signalsBlocked():
            self.editingFinished.emit(self._lineEdit.text())

    def beginEdit(self):
        """
        Begins editing for the label.
        """
        if not self._lineEdit:
            return

        self.aboutToEdit.emit()
        self._lineEdit.setText(self.editText())
        self._lineEdit.show()
        self._lineEdit.selectAll()
        self._lineEdit.setFocus()

    def editText(self):
        """
        Returns the edit text for this label.  This will be the text displayed
        in the editing field when editable.  By default, it will be the
        text from the label itself.
        
        :return     <str>
        """
        if self._editText is not None:
            return self._editText
        return self.text()

    def eventFilter(self, object, event):
        """
        Filters the event for the inputed object looking for escape keys.
        
        :param      object | <QObject>
                    event  | <QEvent>
        
        :return     <bool>
        """
        if event.type() == event.KeyPress:
            if event.key() == Qt.Key_Escape:
                self.rejectEdit()
                return True

            elif event.key() in (Qt.Key_Return, Qt.Key_Enter):
                self.acceptEdit()
                return True

        elif event.type() == event.FocusOut:
            self.acceptEdit()

        return False

    def isEditable(self):
        """
        Returns if this label is editable or not.
        
        :return     <bool>
        """
        return self._editable

    def lineEdit(self):
        """
        Returns the line edit instance linked with this label.  This will be
        null if the label is not editable.
        
        :return     <QLineEdit>
        """
        return self._lineEdit

    def mouseDoubleClickEvent(self, event):
        """
        Prompts the editing process if the label is editable.
        
        :param      event | <QMouseDoubleClickEvent>
        """
        if self.isEditable():
            self.beginEdit()

        super(XLabel, self).mouseDoubleClickEvent(event)

    def rejectEdit(self):
        """
        Cancels the edit for this label.
        """
        if self._lineEdit:
            self._lineEdit.hide()
            self.editingCancelled.emit()

    def resizeEvent(self, event):
        """
        Resize the label and the line edit for this label.
        
        :param      event | <QResizeEvent>
        """
        super(XLabel, self).resizeEvent(event)

        if self._lineEdit:
            self._lineEdit.resize(self.size())

    def setEditable(self, state):
        """
        Sets whether or not this label should be editable or not.
        
        :param      state | <bool>
        """
        self._editable = state

        if state and not self._lineEdit:
            self.setLineEdit(QLineEdit(self))

        elif not state and self._lineEdit:
            self._lineEdit.close()
            self._lineEdit.setParent(None)
            self._lineEdit.deleteLater()
            self._lineEdit = None

    def setEditText(self, text):
        """
        Sets the text to be used while editing.
        
        :param      text | <str> || None
        """
        self._editText = text

    def setLineEdit(self, lineEdit):
        """
        Sets the line edit instance for this label.
        
        :param      lineEdit | <QLineEdit>
        """
        self._lineEdit = lineEdit
        if lineEdit:
            lineEdit.setFont(self.font())
            lineEdit.installEventFilter(self)
            lineEdit.resize(self.size())
            lineEdit.hide()

    x_editable = Property(bool, isEditable, setEditable)