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
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)
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
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()
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
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
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)
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)
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[:]
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)
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()
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)
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)
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
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
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)
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
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
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_()
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)
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)
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)