class XCurrencySpinBox(QDoubleSpinBox): def __init__(self, parent): super(XCurrencySpinBox, self).__init__(parent) # define custom properties self._currency = 'USD' # set default values self.setSingleStep(10) def currency(self): """ Returns the currency for this widget. :return <str> """ return self._currency @Slot(str) def setCurrency(self, currency): """ Sets the currency for this widget. :param currency | <str> """ self._currency = currency self.setValue(self.value()) def textFromValue(self, value): """ Returns the text for this widgets value. :param value | <float> :return <str> """ return projex.money.toString(value, self.currency()) def valueFromText(self, text): """ Returns the value for this widgets text. :param text | <str> :return <float> """ value, currency = projex.money.fromString(text) return value x_currency = Property(str, currency, setCurrency)
class XOrbGridEdit(QWidget): """ """ __designer_group__ = 'ProjexUI - ORB' def __init__( self, parent = None ): super(XOrbGridEdit, self).__init__( parent ) # load the user interface projexui.loadUi(__file__, self) # define custom properties self._queryWidget = XOrbQueryWidget(self) self.uiSaveBTN.hide() self.uiSearchTXT.setIconSize(QSize(28, 28)) self.uiSearchTXT.addButton(self.uiQueryBTN) self.uiQueryBTN.setCentralWidget(self._queryWidget) self.uiQueryBTN.setDefaultAnchor(XPopupWidget.Anchor.TopRight) popup = self.uiQueryBTN.popupWidget() popup.setShowTitleBar(False) # set default properties self.uiRecordTREE.setUserGroupingEnabled(False) self.uiRecordTREE.setGroupingActive(False) self.uiRecordTREE.setEditable(False) self.uiRecordTREE.setPageSize(50) self.uiRecordTREE.setTabKeyNavigation(True) # create connections self.uiRefreshBTN.clicked.connect(self.refresh) self.uiSaveBTN.clicked.connect(self.commit) self.uiQueryBTN.popupAboutToShow.connect(self.loadQuery) self.uiQueryBTN.popupAccepted.connect(self.assignQuery) popup.resetRequested.connect(self._queryWidget.reset) def addWidget(self, widget, align=Qt.AlignLeft, before=None): """ Adds a widget to the grid edit's toolbar with the given alignment. :param widget | <QtGui.QWidget> align | <QtCore.Qt.Alignment> """ # self.uiToolbarLAYOUT points to a deleted C/C++ object in PySide... layout = self.findChild(QHBoxLayout, 'uiToolbarLAYOUT') if before is not None: index = None for i in range(layout.count()): if layout.itemAt(i).widget() == before: index = i break if index is not None: layout.insertWidget(index, widget) else: layout.addWidget(widget) if align == Qt.AlignLeft: layout.insertWidget(0, widget) else: layout.addWidget(widget) def autoloadPages(self): """ Returns whether or not to automatically load pages for this edit. :sa XOrbTreeWidget.autoloadPages :return <bool> """ return self.uiRecordTREE.autoloadPages() def assignQuery(self): """ Assigns the query from the query widget to the edit. """ self.uiRecordTREE.setQuery(self._queryWidget.query(), autoRefresh=True) def currentRecord(self): """ Returns the current record based on the user selection in the tree. :return <orb.Table> || None """ return self.uiRecordTREE.currentRecord() def commit(self): """ Commits changes stored in the interface to the database. """ self.uiRecordTREE.commit() def isEditable(self): """ Returns whether or not this grid edit is editable. :return <bool> """ return self.uiRecordTREE.isEditable() def isPaged(self): """ Returns whether or not to pages the results from the database query. :sa XOrbTreeWidget.isPaged :param state | <bool> """ return self.uiRecordTREE.isPaged() def loadQuery(self): """ Loads the query for the query widget when it is being shown. """ self._queryWidget.setQuery(self.query()) def pageSize(self): """ Returns the number of records that should be loaded per page. :sa XOrbTreeWidget.pageSize :return <int> """ return self.uiRecordTREE.pageSize() def query(self): """ Returns the query that is being represented by the current results. :return <orb.Query> """ return self.uiRecordTREE.query() def records(self): """ Returns the records that are currently assigned to this widget. :return <orb.RecordSet> """ return self.uiRecordTREE.records() def refresh(self): """ Commits changes stored in the interface to the database. """ table = self.tableType() if table: table.markTableCacheExpired() self.uiRecordTREE.searchRecords(self.uiSearchTXT.text()) def restoreXml(self, xml): """ Restores the settings for this edit from xml. :param xml | <xml.etree.ElementTree> """ self.uiRecordTREE.restoreXml(xml.find('tree')) # restore the query xquery = xml.find('query') if xquery is not None: self.setQuery(Q.fromXml(xquery[0])) def saveXml(self, xml): """ Saves the settings for this edit to the xml parent. :param xparent | <xml.etree.ElementTree> """ # save grouping xtree = ElementTree.SubElement(xml, 'tree') self.uiRecordTREE.saveXml(xtree) # save the query query = self.query() if query: query.toXml(ElementTree.SubElement(xml, 'query')) def searchWidget(self): """ Returns the search text edit for this grid edit. :return <XLineEdit> """ return self.uiSearchTXT def setAutoloadPages(self, state): """ Sets whether or not to automatically load pages for this edit. :sa XOrbTreeWidget.setAutoloadPages :param state | <bool> """ return self.uiRecordTREE.setAutoloadPages(state) def setCurrentRecord(self, record): """ Sets the current record based on the user selection in the tree. :param record | <orb.Table> || None """ self.uiRecordTREE.setCurrentRecord(record) def setEditable(self, state): """ Sets the editable state for this grid widget. :param state | <bool> """ self.uiRecordTREE.setEditable(state) self.uiSaveBTN.setVisible(state) def setQuery(self, query, autoRefresh=True): """ Sets the query for this edit to the inputed query. :param query | <orb.Query> """ self.uiRecordTREE.setQuery(query, autoRefresh=autoRefresh) def setPaged(self, state): """ Sets whether or not to pages the results from the database query. :sa XOrbTreeWidget.setPaged :param state | <bool> """ return self.uiRecordTREE.setPaged(state) def setPageSize(self, size): """ Sets the number of records that should be loaded per page. :sa XOrbTreeWidget.setPageSize :param size | <int> """ return self.uiRecordTREE.setPageSize(size) def setRecords(self, records): """ Sets the records for this widget to the inputed records. :param records | [<orb.Table>, ..] || <orb.RecordSet> """ self.uiRecordTREE.setRecords(records) def setTableType(self, tableType, autoRefresh=True): """ Sets the table type associated with this edit. :param tableType | <subclass of orb.Table> """ self.uiRecordTREE.setTableType(tableType) self._queryWidget.setTableType(tableType) if autoRefresh: self.setQuery(Q()) def setUserGroupingEnabled(self, state=True): """ Sets whether or not to allow the user to manipulating grouping. :param state | <bool> """ self.uiRecordTREE.setUserGroupingEnabled(state) def tableType(self): """ Returns the table type associated with this edit. :return <subclass of orb.Table> """ return self.uiRecordTREE.tableType() def treeWidget(self): """ Returns the tree widget that is for editing records for this grid. :return <XOrbTreeWidget> """ return self.uiRecordTREE def userGroupingEnabled(self): """ Returns whether or not user grouping is enabled. :return <bool> """ return self.uiRecordTREE.userGroupingEnabled() x_autoloadPages = Property(bool, autoloadPages, setAutoloadPages) x_paged = Property(bool, isPaged, setPaged) x_pageSize = Property(int, pageSize, setPageSize) x_editable = Property(bool, isEditable, setEditable) x_userGroupingEnabled = Property(bool, userGroupingEnabled, setUserGroupingEnabled)
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 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 XGroupBox(QGroupBox): """ Extends the base QGroupBox class to support some additional features like setting collapsing on toggle. """ __designer_icon__ = projexui.resources.find('img/ui/groupbox.png') __designer_container__ = True def __init__(self, *args): super(XGroupBox, self).__init__(*args) self._collapsible = False self._collapsedHeight = 18 self._inverted = False self.toggled.connect(self.matchCollapsedState) def collapsedHeight(self): """ Returns the collapsed height for this object. :return <int> """ return self._collapsedHeight def isCollapsed(self): """ Returns whether or not this group box is collapsed. :return <bool> """ if not self.isCollapsible(): return False if self._inverted: return self.isChecked() return not self.isChecked() def isCollapsible(self): """ Returns whether or not this group box is collapsiible. :return <bool> """ return self._collapsible def isInverted(self): """ Returns whether or not this widget is in an inverted state, by which unchecking the group box will force it collapsed. :return <bool> """ return self._inverted def paintEvent(self, event): """ Overloads the paint event for this group box if it is currently collpased. :param event | <QPaintEvent> """ if (self.isCollapsed()): self.setFlat(True) elif (self.isCollapsible()): self.setFlat(False) super(XGroupBox, self).paintEvent(event) def matchCollapsedState(self): """ Matches the collapsed state for this groupbox. """ collapsed = not self.isChecked() if self._inverted: collapsed = not collapsed if (not self.isCollapsible() or not collapsed): for child in self.children(): if (not isinstance(child, QWidget)): continue child.show() self.setMaximumHeight(MAX_INT) self.adjustSize() if (self.parent()): self.parent().adjustSize() else: self.setMaximumHeight(self.collapsedHeight()) for child in self.children(): if (not isinstance(child, QWidget)): continue child.hide() def setCollapsed(self, state): """ Sets whether or not this group box is collapsed. :param state | <bool> """ self.setCollapsible(True) if not self._inverted: self.setChecked(not state) else: self.setChecked(state) def setCollapsible(self, state): """ Sets whether or not this groupbox will be collapsible when toggled. :param state | <bool> """ self._collapsible = state self.matchCollapsedState() def setCollapsedHeight(self, height): """ Sets the height that will be used when this group box is collapsed. :param height | <int> """ self._collapsedHeight = height self.matchCollapsedState() def setInverted(self, state): """ Sets whether or not to invert the check state for collapsing. :param state | <bool> """ collapsed = self.isCollapsed() self._inverted = state if self.isCollapsible(): self.setCollapsed(collapsed) # create Qt properties x_collapsible = Property(bool, isCollapsible, setCollapsible) x_collapsed = Property(bool, isCollapsed, setCollapsed) x_collapsedHeight = Property(int, collapsedHeight, setCollapsedHeight) x_inverted = Property(bool, isInverted, setInverted)
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 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 XSplitButton(QWidget): """ ~~>[img:widgets/xsplitbutton.png] The XSplitButton class provides a simple class for creating a multi-checkable tool button based on QActions and QActionGroups. === Example Usage === |>>> from projexui.widgets.xsplitbutton import XSplitButton |>>> import projexui | |>>> # create the widget |>>> widget = projexui.testWidget(XSplitButton) | |>>> # add some actions (can be text or a QAction) |>>> widget.addAction('Day') |>>> widget.addAction('Month') |>>> widget.addAction('Year') | |>>> # create connections |>>> def printAction(act): print act.text() |>>> widget.actionGroup().triggered.connect(printAction) """ __designer_icon__ = projexui.resources.find('img/ui/multicheckbox.png') clicked = Signal() currentActionChanged = Signal(object) hovered = Signal(object) triggered = Signal(object) def __init__( self, parent = None ): super(XSplitButton, self).__init__( parent ) # define custom properties self._actionGroup = QActionGroup(self) self._padding = 5 self._cornerRadius = 10 #self._currentAction = None self._checkable = True # set default properties layout = QBoxLayout(QBoxLayout.LeftToRight) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setLayout(layout) self.clear() # create connections self._actionGroup.hovered.connect(self.emitHovered) self._actionGroup.triggered.connect(self.emitTriggered) def actions(self): """ Returns a list of the actions linked with this widget. :return [<QAction>, ..] """ return self._actionGroup.actions() def actionTexts(self): """ Returns a list of the action texts for this widget. :return [<str>, ..] """ return map(lambda x: x.text(), self._actionGroup.actions()) def actionGroup( self ): """ Returns the action group linked with this widget. :return <QActionGroup> """ return self._actionGroup def addAction(self, action, checked=None, autoBuild=True): """ Adds the inputed action to this widget's action group. This will auto-\ create a new group if no group is already defined. :param action | <QAction> || <str> :return <QAction> """ # clear the holder actions = self._actionGroup.actions() if actions and actions[0].objectName() == 'place_holder': self._actionGroup.removeAction(actions[0]) actions[0].deleteLater() # create an action from the name if not isinstance(action, QAction): action_name = str(action) action = QAction(action_name, self) action.setObjectName(action_name) action.setCheckable(self.isCheckable()) # auto-check the first option if checked or (not self._actionGroup.actions() and checked is None): action.setChecked(True) self._actionGroup.addAction(action) if autoBuild: self.rebuild() return action def clear(self, autoBuild=True): """ Clears the actions for this widget. """ for action in self._actionGroup.actions(): self._actionGroup.removeAction(action) action = QAction('', self) action.setObjectName('place_holder') # self._currentAction = None self._actionGroup.addAction(action) if autoBuild: self.rebuild() def cornerRadius( self ): """ Returns the corner radius for this widget. :return <int> """ return self._cornerRadius def count(self): """ Returns the number of actions associated with this button. :return <int> """ actions = self._actionGroup.actions() if len(actions) == 1 and actions[0].objectName() == 'place_holder': return 0 return len(actions) def currentAction( self ): """ Returns the action that is currently checked in the system. :return <QAction> || None """ return self._actionGroup.checkedAction() def direction( self ): """ Returns the direction for this widget. :return <QBoxLayout::Direction> """ return self.layout().direction() def emitClicked(self): """ Emits the clicked signal whenever any of the actions are clicked. """ if not self.signalsBlocked(): self.clicked.emit() def emitHovered(self, action): """ Emits the hovered action for this widget. :param action | <QAction> """ if not self.signalsBlocked(): self.hovered.emit(action) def emitTriggered(self, action): """ Emits the triggered action for this widget. :param action | <QAction> """ # if action != self._currentAction: # self._currentAction = action # self.currentActionChanged.emit(action) # self._currentAction = action if not self.signalsBlocked(): self.triggered.emit(action) def findAction( self, text ): """ Looks up the action based on the inputed text. :return <QAction> || None """ for action in self.actionGroup().actions(): if ( text in (action.objectName(), action.text()) ): return action return None def isCheckable(self): """ Returns whether or not the actions within this button should be checkable. :return <bool> """ return self._checkable def padding( self ): """ Returns the button padding amount for this widget. :return <int> """ return self._padding def rebuild( self ): """ Rebuilds the user interface buttons for this widget. """ self.setUpdatesEnabled(False) # sync up the toolbuttons with our actions actions = self._actionGroup.actions() btns = self.findChildren(QToolButton) horiz = self.direction() in (QBoxLayout.LeftToRight, QBoxLayout.RightToLeft) # remove unnecessary buttons if len(actions) < len(btns): rem_btns = btns[len(actions)-1:] btns = btns[:len(actions)] for btn in rem_btns: btn.close() btn.setParent(None) btn.deleteLater() # create new buttons elif len(btns) < len(actions): for i in range(len(btns), len(actions)): btn = QToolButton(self) btn.setAutoFillBackground(True) btns.append(btn) self.layout().addWidget(btn) btn.clicked.connect(self.emitClicked) # determine coloring options palette = self.palette() checked = palette.color(palette.Highlight) checked_fg = palette.color(palette.HighlightedText) unchecked = palette.color(palette.Button) unchecked_fg = palette.color(palette.ButtonText) border = palette.color(palette.Mid) # define the stylesheet options options = {} options['top_left_radius'] = 0 options['top_right_radius'] = 0 options['bot_left_radius'] = 0 options['bot_right_radius'] = 0 options['border_color'] = border.name() options['checked_fg'] = checked_fg.name() options['checked_bg'] = checked.name() options['checked_bg_alt'] = checked.darker(120).name() options['unchecked_fg'] = unchecked_fg.name() options['unchecked_bg'] = unchecked.name() options['unchecked_bg_alt'] = unchecked.darker(120).name() options['padding_top'] = 1 options['padding_bottom'] = 1 options['padding_left'] = 1 options['padding_right'] = 1 if horiz: options['x1'] = 0 options['y1'] = 0 options['x2'] = 0 options['y2'] = 1 else: options['x1'] = 0 options['y1'] = 0 options['x2'] = 1 options['y2'] = 1 # sync up the actions and buttons count = len(actions) palette = self.palette() font = self.font() for i, action in enumerate(actions): btn = btns[i] # assign the action for this button if btn.defaultAction() != action: # clear out any existing actions for act in btn.actions(): btn.removeAction(act) # assign the given action btn.setDefaultAction(action) options['top_left_radius'] = 1 options['bot_left_radius'] = 1 options['top_right_radius'] = 1 options['bot_right_radius'] = 1 if horiz: options['padding_left'] = self._padding options['padding_right'] = self._padding else: options['padding_top'] = self._padding options['padding_bottom'] = self._padding if not i: if horiz: options['top_left_radius'] = self.cornerRadius() options['bot_left_radius'] = self.cornerRadius() options['padding_left'] += self.cornerRadius() / 3.0 else: options['top_left_radius'] = self.cornerRadius() options['top_right_radius'] = self.cornerRadius() options['padding_top'] += self.cornerRadius() / 3.0 if i == count - 1: if horiz: options['top_right_radius'] = self.cornerRadius() options['bot_right_radius'] = self.cornerRadius() options['padding_right'] += self.cornerRadius() / 3.0 else: options['bot_left_radius'] = self.cornerRadius() options['bot_right_radius'] = self.cornerRadius() options['padding_bottom'] += self.cornerRadius() / 3.0 btn.setFont(font) btn.setPalette(palette) btn.setStyleSheet(TOOLBUTTON_STYLE % options) if horiz: btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) else: btn.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.setUpdatesEnabled(True) def setActions(self, actions): """ Sets the actions for this widget to th inputed list of actions. :param [<QAction>, ..] """ self.clear(autoBuild=False) for action in actions: self.addAction(action, autoBuild=False) self.rebuild() def setActionTexts(self, names): """ Convenience method for auto-generating actions based on text names, sets the list of actions for this widget to the inputed list of names. :param names | [<str>, ..] """ self.setActions(names) def setActionGroup( self, actionGroup ): """ Sets the action group for this widget to the inputed action group. :param actionGroup | <QActionGroup> """ self._actionGroup = actionGroup self.rebuild() def setCheckable(self, state): """ Sets whether or not the actions within this button should be checkable. :param state | <bool> """ self._checkable = state for act in self._actionGroup.actions(): act.setCheckable(state) def setCornerRadius( self, radius ): """ Sets the corner radius value for this widget to the inputed radius. :param radius | <int> """ self._cornerRadius = radius def setCurrentAction(self, action): """ Sets the current action for this button to the inputed action. :param action | <QAction> || <str> """ self._actionGroup.blockSignals(True) for act in self._actionGroup.actions(): act.setChecked(act == action or act.text() == action) self._actionGroup.blockSignals(False) def setDirection( self, direction ): """ Sets the direction that this group widget will face. :param direction | <QBoxLayout::Direction> """ self.layout().setDirection(direction) self.rebuild() def setFont(self, font): """ Sets the font for this widget and propogates down to the buttons. :param font | <QFont> """ super(XSplitButton, self).setFont(font) self.rebuild() def setPadding( self, padding ): """ Sets the padding amount for this widget's button set. :param padding | <int> """ self._padding = padding self.rebuild() def setPalette(self, palette): """ Rebuilds the buttons for this widget since they use specific palette options. :param palette | <QPalette> """ super(XSplitButton, self).setPalette(palette) self.rebuild() def sizeHint(self): """ Returns the base size hint for this widget. :return <QSize> """ return QSize(35, 22) x_actionTexts = Property(QStringList, actionTexts, setActionTexts) x_checkable = Property(bool, isCheckable, setCheckable)
class XTabWidget(QTabWidget): addRequested = Signal(QPoint) optionsRequested = Signal(QPoint) def __init__(self, *args): super(XTabWidget, self).__init__(*args) # create the tab bar self.setTabBar(XTabBar(self)) # create custom properties self._showAddButton = True self._showOptionsButton = True # create the add button self._addButton = XTabButton(self) self._addButton.setIcon(QIcon(resources.find('img/tab/add.png'))) self._addButton.setFixedSize(18, 18) self._addButton.setIconSize(QSize(10, 10)) # create the option button self._optionsButton = XTabButton(self) self._optionsButton.setFixedSize(22, 18) self._optionsButton.setIcon(QIcon(resources.find('img/tab/gear.png'))) self._optionsButton.setIconSize(QSize(10, 10)) # create connection self.connect(self.tabBar(), SIGNAL('currentChanged(int)'), self.adjustButtons) self.connect(self.tabBar(), SIGNAL('resized()'), self.adjustButtons) self.connect(self._optionsButton, SIGNAL('clicked()'), self.emitOptionsRequested) self.connect(self._addButton, SIGNAL('clicked()'), self.emitAddRequested) def __nonzero__(self): """ At somepoint, QTabWidget's nonzero became linked to whether it had children vs. whether it was none. This returns the original functionality. """ return self is not None def adjustButtons(self): """ Updates the position of the buttons based on the current geometry. """ tabbar = self.tabBar() tabbar.adjustSize() w = self.width() - self._optionsButton.width() - 2 self._optionsButton.move(w, 0) if self.count(): need_update = self._addButton.property('alone') != False if need_update: self._addButton.setProperty('alone', False) self._addButton.move(tabbar.width(), 1) self._addButton.setFixedHeight(tabbar.height()) else: need_update = self._addButton.property('alone') != True if need_update: self._addButton.setProperty('alone', True) self._addButton.move(tabbar.width() + 2, 1) self._addButton.stackUnder(self.currentWidget()) # force refresh on the stylesheet (Qt limitation for updates) if need_update: app = QApplication.instance() app.setStyleSheet(app.styleSheet()) def addButton(self): """ Returns the add button linked with this tab widget. :return <XTabButton> """ return self._addButton def emitAddRequested(self, point=None): """ Emitted when the option menu button is clicked provided the signals \ are not being blocked for this widget. :param point | <QPoint> """ if self.signalsBlocked(): return if not point: btn = self._addButton point = btn.mapToGlobal(QPoint(btn.width(), 0)) self.addRequested.emit(point) def emitOptionsRequested(self, point=None): """ Emitted when the option menu button is clicked provided the signals \ are not being blocked for this widget. :param point | <QPoint> """ if self.signalsBlocked(): return if not point: btn = self._optionsButton point = btn.mapToGlobal(QPoint(0, btn.height())) self.optionsRequested.emit(point) def optionsButton(self): """ Returns the options button linked with this tab widget. :return <XTabButton> """ return self._optionsButton def paintEvent(self, event): if not self.count(): return super(XTabWidget, self).paintEvent(event) def resizeEvent(self, event): """ Updates the position of the additional buttons when this widget \ resizes. :param event | <QResizeEvet> """ super(XTabWidget, self).resizeEvent(event) self.adjustButtons() def setShowAddButton(self, state): """ Sets whether or not the add button is visible. :param state | <bool> """ self._showAddButton = state self._addButton.setVisible(state) def setShowOptionsButton(self, state): """ Sets whether or not the option button is visible. :param state | <bool> """ self._showOptionsButton = state self._optionsButton.setVisible(state) def showAddButton(self): """ Returns whether or not the add button is visible. :return <bool> """ return self._showAddButton def showOptionsButton(self): """ Returns whether or not the option button should be visible. :return <bool> """ return self._showOptionsButton x_showAddButton = Property(bool, showAddButton, setShowAddButton) x_showOptionsButton = Property(bool, showOptionsButton, setShowOptionsButton)
class XMultiTagEdit(XListWidget): tagCreated = Signal(str) tagRemoved = Signal(str) __designer_icon__ = resources.find('img/ui/tags.png') def __init__(self, parent=None): super(XMultiTagEdit, self).__init__(parent) # define custom properties self._createItem = None self._completer = None self._itemsRemovable = True self._unique = True self._highlightColor = QColor(181, 209, 244) self._tagColor = QColor(181, 209, 244, 50) self._borderColor = QColor(150, 178, 213) self._options = None self._insertAllowed = False self._editable = True # make sure the highlighting works palette = self.palette() palette.setColor(palette.Highlight, self._highlightColor) self.setPalette(palette) # setup default options self.setItemDelegate(XMultiTagDelegate(self)) self.setMinimumHeight(28) self.setMovement(XListWidget.Static) self.setResizeMode(XListWidget.Adjust) self.setDragDropMode(XListWidget.DragOnly) # handling drops internally self.setViewMode(XListWidget.IconMode) self.setEditTriggers(XListWidget.DoubleClicked | \ XListWidget.SelectedClicked | \ XListWidget.AnyKeyPressed ) self.setSpacing(3) self.setAcceptDrops(True) self.clear() # track changes to items self.itemChanged.connect(self.handleTagChange) @Slot(str) def addTag(self, tag): """ Adds a new tag to the edit. :param tag | <str> :return <bool> """ if (not (tag and self.isTagValid(tag))): return False self.blockSignals(True) create_item = self.createItem() if create_item: self.insertItem(self.row(create_item), XMultiTagItem(tag, self)) create_item.setText('') else: self.addItem(XMultiTagItem(tag, self)) self.blockSignals(False) if (not self.signalsBlocked()): self.tagCreated.emit(tag) return False def borderColor(self): """ Returns the color used for the tag border. :return <QColor> """ return self._borderColor def clear(self): """ Clears the items for this edit. """ super(XMultiTagEdit, self).clear() self._createItem = None def completer(self): """ Returns the completer instance linked with this edit. :return <QCompleter> || None """ return self._completer def copy(self): """ Copies the selected items to the clipboard. """ text = [] for item in self.selectedItems(): text.append(str(item.text())) QApplication.clipboard().setText(','.join(text)) def createItem(self): """ Returns a reference to the create item that is used for this edit. :return <XMultiTagCreateItem> """ if not self.isEditable(): return None if self._createItem is None: self.blockSignals(True) self._createItem = XMultiTagCreateItem(self) self.addItem(self._createItem) self.blockSignals(False) return self._createItem def dragEnterEvent(self, event): """ Handles the drag enter event. :param event | <QDragEvent> """ tags = str(event.mimeData().text()) if (event.source() == self): event.acceptProposedAction() elif (tags): event.acceptProposedAction() else: super(XMultiTagEdit, self).dragEnterEvent(event) def dragMoveEvent(self, event): """ Handles the drag move event. :param event | <QDragEvent> """ tags = str(event.mimeData().text()) if (event.source() == self): event.acceptProposedAction() elif (tags): event.acceptProposedAction() else: super(XMultiTagEdit, self).dragMoveEvent(event) def dropEvent(self, event): """ Handles the drop event. :param event | <QDropEvent> """ tags = str(event.mimeData().text()) # handle an internal move if event.source() == self: curr_item = self.selectedItems()[0] create_item = self.createItem() # don't allow moving of the creation item if curr_item == create_item: return targ_item = self.itemAt(event.pos()) if not targ_item: targ_item = create_item curr_idx = self.row(curr_item) targ_idx = self.row(targ_item) if (targ_idx == self.count() - 1): targ_idx -= 1 # don't bother moving the same item if (curr_idx == targ_idx): return self.takeItem(self.row(curr_item)) self.insertItem(targ_idx, curr_item) self.setCurrentItem(curr_item) elif (tags): for tag in tags.split(','): tag = tag.strip() if (self.isTagValid(tag)): self.addTag(tag) else: event.accept() def eventFilter(self, object, event): """ Filters the key press event on the editor object to look for backspace key strokes on blank editors to remove previous tags. :param object | <QObject> event | <QEvent> :return <bool> | consumed """ if event.type() == event.KeyPress: if event.key() == Qt.Key_Backspace: is_line_edit = isinstance(object, QLineEdit) if not (is_line_edit and object.text()): if self.count() > 1: self.takeItem(self.count() - 2) object.setFocus() elif event.key() in (Qt.Key_Return, Qt.Key_Enter): self.finishEditing(object.text()) return True return False def finishEditing(self, tag): """ Finishes editing the current item. """ curr_item = self.currentItem() create_item = self.createItem() self.closePersistentEditor(curr_item) if curr_item == create_item: self.addTag(tag) elif self.isTagValid(tag): curr_item.setText(tag) def handleTagChange(self, item): """ Handles the tag change information for this widget. :param item | <QListWidgetItem> """ # :note PySide == operator not defined for QListWidgetItem. In this # in this case, we're just checking if the object is the exact # same, so 'is' works just fine. create_item = self.createItem() if item is create_item: self.addTag(create_item.text()) elif self.isTagValid(item.text()): item.setText(item.text()) def hasTag(self, tag): """ Returns whether or not the inputed tag exists in this collection. :return <bool> """ return str(tag) in self.tags() def highlightColor(self): """ Returns the highlight color for this edit. :return <QColor> """ return self._highlightColor def isEditable(self): """ Returns whether or not the user can edit the items in the list by typing. :return <bool> """ return self._editable def isInsertAllowed(self): """ Returns the whether or not a user is able to insert new tags besides the ones in the options. :return <bool> """ return self._insertAllowed def isTagValid(self, tag): """ Checks to see if the inputed tag is valid or not. :param tag | <str> :return <bool> """ if ( self._options is not None and \ not str(tag) in self._options \ and not self.isInsertAllowed() ): return False elif (self.isUnique() and self.hasTag(tag)): return False return True def isUnique(self): """ Returns whether or not the tags for this edit should be unique. :return <bool> """ return self._unique def itemFromTag(self, tag): """ Returns the item assigned to the given tag. :param tag | <str> :return <XMultiTagItem> || None """ for row in range(self.count() - 1): item = self.item(row) if (item and item.text() == tag): return item return None def itemsRemovable(self): """ Returns whether or not the items are able to be removed by the user. :return <bool> """ return self._itemsRemovable def keyPressEvent(self, event): """ Handles the Ctrl+C/Ctrl+V events for copy & paste. :param event | <QKeyEvent> """ if ( event.key() == Qt.Key_C and \ event.modifiers() == Qt.ControlModifier ): self.copy() event.accept() return elif ( event.key() == Qt.Key_V and \ event.modifiers() == Qt.ControlModifier ): self.paste() event.accept() return elif (event.key() == Qt.Key_Delete): indexes = map(self.row, self.selectedItems()) for index in reversed(sorted(indexes)): self.takeItem(index) event.accept() return elif event.key() == Qt.Key_Backspace: if self.count() > 1: self.takeItem(self.count() - 2) self.setFocus() super(XMultiTagEdit, self).keyPressEvent(event) def mimeData(self, items): """ Creates the mime data for the different items. :param items | [<QListWidgetItem>, ..] :return <QMimeData> """ text = [] for item in items: text.append(str(item.text())) data = QMimeData() data.setText(','.join(text)) return data def mousePressEvent(self, event): """ Make sure on a mouse release event that we have a current item. If no item is current, then our edit item will become current. :param event | <QMouseReleaseEvent> """ item = self.itemAt(event.pos()) # set the tag creation item as active if item is None: create_item = self.createItem() if create_item: self.setCurrentItem(create_item) self.editItem(create_item) # check to see if we're removing a tag else: rect = self.visualItemRect(item) if (rect.right() - 14 < event.pos().x()): # make sure the item is allowed to be removed via the widget if (self.itemsRemovable()): self.takeItem(self.row(item)) # emit the removed signal if (not self.signalsBlocked()): self.tagRemoved.emit(item.text()) event.ignore() return super(XMultiTagEdit, self).mousePressEvent(event) def options(self): """ Returns the list of options that are valid for this tag edit. :return [<str>, ..] """ if self._options is None: return [] return self._options def paste(self): """ Pastes text from the clipboard. """ text = str(QApplication.clipboard().text()) for tag in text.split(','): tag = tag.strip() if (self.isTagValid(tag)): self.addTag(tag) def resizeEvent(self, event): """ Overloads the resize event to control if we are still editing. If we are resizing, then we are no longer editing. """ curr_item = self.currentItem() self.closePersistentEditor(curr_item) super(XMultiTagEdit, self).resizeEvent(event) def setCompleter(self, completer): """ Sets the text completer for this tag widget to the inputed completer. :param completer | <QCompleter> """ if (self._completer == completer): return elif (self._completer): self._completer.activated.disconnect(self.finishEditing) self._completer = completer if (completer): completer.activated.connect(self.finishEditing) def setEditable(self, state): """ Sets whether or not the user can edit the items in the list by typing. :param state | <bool> """ self._editable = state if state: self.setEditTriggers(self.AllEditTriggers) else: self.setEditTriggers(self.NoEditTriggers) def setInsertAllowed(self, state): """ Sets whether or not the insertion is allowed for tags not defined in the options. :param state | <bool> """ self._insertAllowed = state def setItemsRemovable(self, state): """ Sets whether or not the items should be allowed to be removed by the user. :param state | <bool> """ self._itemsRemovable = state def setOptions(self, options): """ Sets the tag option list for this widget. If used, tags need to be found within the list of options when added. :param options | [<str>, ..] """ self._options = map(str, options) if (options): completer = QCompleter(options, self) completer.setCompletionMode(QCompleter.InlineCompletion) self.setCompleter(completer) else: self.setCompleter(None) def setTags(self, tags): """ Sets the tags assigned to this item to the inputed list of tags. :param tags | [<str>, ..] """ self.setUpdatesEnabled(False) self.blockSignals(True) self.clear() for tag in tags: self.addItem(XMultiTagItem(tag, self)) self.blockSignals(False) self.setUpdatesEnabled(True) def setUnique(self, state): """ Sets whether or not the tags on this edit should be unique. :param state | <bool> """ self._unique = state def setViewMode(self, viewMode): """ Sets the view mode for this widget to the inputed mode. :param viewMode | <QListWidget.ViewMode> """ ddrop_mode = self.dragDropMode() super(XMultiTagEdit, self).setViewMode(viewMode) self.setDragDropMode(ddrop_mode) def tagColor(self): """ Returns the color used for the tags of this edit. :return <QColor> """ return self._tagColor def tagItems(self): """ Returns a list of all the tag items assigned to this widget. :return [<XMultiTagItem>, ..] """ return [self.item(row) for row in range(self.count() - 1)] def tags(self): """ Returns a list of all the tags assigned to this widget. :return [<str>, ..] """ item = self.item(self.count() - 1) count = self.count() if (item is self._createItem): count -= 1 return [str(self.item(row).text()) for row in range(count)] def takeTag(self, tag): """ Removes the inputed tag from the system. :param tag | <str> :return <XMultiTagItem> || None """ for row in range(self.count() - 1): item = self.item(row) if (item and item.text() == tag): self.takeItem(row) if (not self.signalsBlocked()): self.tagRemoved.emit(tag) return item return None x_editable = Property(bool, isEditable, setEditable) x_unique = Property(bool, isUnique, setUnique) x_insertAllowed = Property(bool, isInsertAllowed, setInsertAllowed) x_itemsRemovable = Property(bool, itemsRemovable, setItemsRemovable)
class XRichTextEdit(QWidget): """ """ copyAvailable = Signal(bool) currentCharFormatChanged = Signal(QTextCharFormat) cursorPositionChanged = Signal() redoAvailable = Signal(bool) selectionChanged = Signal() textChanged = Signal() undoAvailable = Signal(bool) def __init__(self, parent=None): super(XRichTextEdit, self).__init__( parent ) # load the user interface projexui.loadUi(__file__, self) # define custom properties # set default properties self.setFocusProxy(self.uiEditTXT) self.uiFindWGT.setTextEdit(self.uiEditTXT) self.uiFindWGT.hide() self.editor().setTabStopWidth(16) self.editor().document().setIndentWidth(24) self.editor().installEventFilter(self) self.editor().setRichTextEditEnabled(True) # create the font picker widget self._fontPickerWidget = XFontPickerWidget(self) self.uiFontBTN.setDefaultAnchor(XPopupWidget.Anchor.TopLeft) self.uiFontBTN.setCentralWidget(self._fontPickerWidget) popup = self.uiFontBTN.popupWidget() popup.setResizable(False) popup.setShowTitleBar(False) self._fontPickerWidget.accepted.connect(popup.accept) # generate actions for this editor based on the toolbar buttons self._actions = {} for mapping in (('bold', self.uiBoldBTN, 'Ctrl+B'), ('italic', self.uiItalicBTN, 'Ctrl+I'), ('underline', self.uiUnderlineBTN, 'Ctrl+U'), ('strikeOut', self.uiStrikeoutBTN, ''), ('unordered', self.uiUnorderedBTN, ''), ('ordered', self.uiOrderedBTN, ''), ('table', self.uiTableBTN, ''), ('align_left', self.uiAlignLeftBTN, ''), ('align_right', self.uiAlignRightBTN, ''), ('align_center', self.uiAlignCenterBTN, ''), ('align_justify', self.uiAlignJustifyBTN, ''), ('font_color', self.uiFontColorBTN, ''), ('bg_color', self.uiBackgroundColorBTN, '')): name, btn, shortcut = mapping act = QAction(self) act.setObjectName(name) act.setToolTip(btn.toolTip()) act.setIcon(btn.icon()) act.setShortcut(QKeySequence(shortcut)) act.setCheckable(btn.isCheckable()) act.setChecked(btn.isChecked()) act.setShortcutContext(Qt.WidgetWithChildrenShortcut) btn.setDefaultAction(act) self._actions[name] = act self.addAction(act) # create the action groupings popup.resetRequested.connect(self.updateFontPicker) popup.aboutToShow.connect(self.updateFontPicker) popup.accepted.connect(self.assignFont) align_group = QActionGroup(self) align_group.addAction(self._actions['align_left']) align_group.addAction(self._actions['align_right']) align_group.addAction(self._actions['align_center']) align_group.addAction(self._actions['align_justify']) align_group.triggered.connect(self.assignAlignment) self._actions['align_left'].setChecked(True) # create connections self._actions['bold'].toggled.connect(self.setFontBold) self._actions['italic'].toggled.connect(self.setFontItalic) self._actions['underline'].toggled.connect(self.setFontUnderline) self._actions['strikeOut'].toggled.connect(self.setFontStrikeOut) self._actions['ordered'].triggered.connect(self.insertOrderedList) self._actions['unordered'].triggered.connect(self.insertUnorderedList) self._actions['table'].triggered.connect(self.insertTable) self._actions['font_color'].triggered.connect(self.pickTextColor) self._actions['bg_color'].triggered.connect(self.pickTextBackgroundColor) # link signals from the editor to the system for signal in ('copyAvailable', 'currentCharFormatChanged', 'cursorPositionChanged', 'redoAvailable', 'selectionChanged', 'textChanged', 'undoAvailable'): from_ = getattr(self.uiEditTXT, signal) to_ = getattr(self, signal) from_.connect(to_) self.cursorPositionChanged.connect(self.refreshAlignmentUi) self.currentCharFormatChanged.connect(self.refreshUi) def alignment(self): """ Returns the current alignment for the editor. :return <Qt.Alignment> """ return self.editor().alignment() def assignAlignment(self, action): """ Sets the current alignment for the editor. """ if self._actions['align_left'] == action: self.setAlignment(Qt.AlignLeft) elif self._actions['align_right'] == action: self.setAlignment(Qt.AlignRight) elif self._actions['align_center'] == action: self.setAlignment(Qt.AlignHCenter) else: self.setAlignment(Qt.AlignJustify) def assignFont(self): """ Assigns the font family and point size settings from the font picker widget. """ font = self.currentFont() font.setFamily(self._fontPickerWidget.currentFamily()) font.setPointSize(self._fontPickerWidget.pointSize()) self.setCurrentFont(font) def blockSignals(self, state): """ Propagates the block signals for this editor down to the text editor. :param state | <bool> """ super(XRichTextEdit, self).blockSignals(state) self.uiEditTXT.blockSignals(state) def currentFont(self): """ Returns the current font for this editor. :return <QFont> """ return self.editor().currentFont() def document(self): """ Returns the text document assigned to this edit. :return <QTextDocument> """ return self.editor().document() def documentMargin(self): """ Returns the margins used for the document edges. :return <int> """ return self.document().documentMargin() def editor(self): """ Returns the text editor that is linked with this rich text editor. :return <XTextEdit> """ return self.uiEditTXT def encoding(self): """ Returns the text encoding type for this edit. :return <str> """ return self.uiEditTXT.encoding() def eventFilter(self, object, event): """ Listens for tab/backtab to modify list information. :param event | <QKeyPressEvent> """ if event.type() != event.KeyPress: return super(XRichTextEdit, self).eventFilter(object, event) cursor = object.textCursor() curr_list = cursor.currentList() # make sure we're in a current list if not curr_list: return super(XRichTextEdit, self).eventFilter(object, event) # unindent for Backtab (Shift+Tab) if event.key() == Qt.Key_Backtab: delta = -1 # indent for Tab elif event.key() == Qt.Key_Tab: delta = 1 # otherwise, don't bother calculating else: return super(XRichTextEdit, self).eventFilter(object, event) # look for the proper list to move to curr_block = cursor.block() curr_indent = curr_list.format().indent() curr_style = curr_list.format().style() prev_block = curr_block next_block = curr_block add_to_list = None add_to_indent = curr_indent + delta while prev_block or next_block: if prev_block: prev_block = prev_block.previous() prev_list = prev_block.textList() if not prev_list: prev_block = None else: prev_indent = prev_list.format().indent() if prev_indent == add_to_indent: add_to_list = prev_list break if next_block: next_block = next_block.next() next_list = next_block.textList() if not next_list: next_block = None else: next_indent = next_list.format().indent() if next_indent == add_to_indent: add_to_list = next_list break if add_to_list is None and 0 < delta: if curr_style in (QTextListFormat.ListCircle, QTextListFormat.ListDisc, QTextListFormat.ListSquare): self.insertUnorderedList() else: self.insertOrderedList() elif add_to_list: add_to_list.add(curr_block) return True def fontBold(self): """ Returns whether or not the current text is bold. :return <bool> """ return self.editor().fontWeight() == QFont.Bold def fontFamily(self): """ Returns the family name of the current font for this editor. :return <str> """ return self.editor().fontFamily() def fontItalic(self): """ Returns whether or not the editor is currently in italics. :return <bool> """ return self.editor().fontItalic() def fontPointSize(self, pointSize): """ Returns the current font's point size. :return <int> """ return self.editor().fontPointSize() def fontStrikeOut(self): """ Returns whether or not the current font is in strike out mode. :return <bool> """ return self.currentFont().strikeOut() def fontUnderline(self): """ Returns whether or not the editor is in underline state. :return <bool> """ return self.editor().fontUnderline() def fontWeight(self): """ Returns the current font weight of the editor. :return <QFont.Weight> """ return self.editor().fontWeight() @Slot() def insertOrderedList(self): """ Inserts an ordered list into the editor. """ cursor = self.editor().textCursor() currlist = cursor.currentList() new_style = QTextListFormat.ListDecimal indent = 1 if currlist: format = currlist.format() indent = format.indent() + 1 style = format.style() if style == QTextListFormat.ListDecimal: new_style = QTextListFormat.ListLowerRoman elif style == QTextListFormat.ListLowerRoman: new_style = QTextListFormat.ListUpperAlpha elif style == QTextListFormat.ListUpperAlpha: new_style = QTextListFormat.ListLowerAlpha new_format = QTextListFormat() new_format.setStyle(new_style) new_format.setIndent(indent) new_list = cursor.createList(new_format) self.editor().setFocus() return new_list @Slot() def insertTable(self): """ Inserts a table into the editor. """ self.editor().textCursor().insertTable(3, 3) self.editor().setFocus() @Slot() def insertUnorderedList(self): """ Inserts an ordered list into the editor. """ cursor = self.editor().textCursor() currlist = cursor.currentList() new_style = QTextListFormat.ListDisc indent = 1 if currlist: format = currlist.format() indent = format.indent() + 1 style = format.style() if style == QTextListFormat.ListDisc: new_style = QTextListFormat.ListCircle elif style == QTextListFormat.ListCircle: new_style = QTextListFormat.ListSquare new_format = QTextListFormat() new_format.setStyle(new_style) new_format.setIndent(indent) new_list = cursor.createList(new_format) self.editor().setFocus() return new_list @Slot() def pickTextBackgroundColor(self): """ Prompts the user to select a text color. """ clr = QColorDialog.getColor(self.textBackgroundColor(), self.window(), 'Pick Background Color') if clr.isValid(): self.setTextBackgroundColor(clr) @Slot() def pickTextColor(self): """ Prompts the user to select a text color. """ clr = QColorDialog.getColor(self.textColor(), self.window(), 'Pick Text Color') if clr.isValid(): self.setTextColor(clr) def refreshUi(self): """ Matches the UI state to the current cursor positioning. """ font = self.currentFont() for name in ('underline', 'bold', 'italic', 'strikeOut'): getter = getattr(font, name) act = self._actions[name] act.blockSignals(True) act.setChecked(getter()) act.blockSignals(False) def refreshAlignmentUi(self): """ Refreshes the alignment UI information. """ align = self.alignment() for name, value in (('align_left', Qt.AlignLeft), ('align_right', Qt.AlignRight), ('align_center', Qt.AlignHCenter), ('align_justify', Qt.AlignJustify)): act = self._actions[name] act.blockSignals(True) act.setChecked(value == align) act.blockSignals(False) def setAlignment(self, align): """ Sets the current alignment for this editor. :param align | <Qt.Align> """ self.blockSignals(True) self.editor().setAlignment(align) self.blockSignals(False) self.refreshAlignmentUi() @Slot(QFont) def setCurrentFont(self, font): """ Sets the current font for the editor to the inputed font. :param font | <QFont> """ self.blockSignals(True) self.editor().setCurrentFont(font) self.blockSignals(False) self.refreshUi() @Slot(int) def setDocumentMargin(self, margin): """ Sets the document margins for this editor. :param margin | <int> """ self.document().setDocumentMargin(margin) def setEncoding(self, encoding): """ Sets the encoding type for this editor to the inputed encoding. :param encoding | <str> """ self.uiEditTXT.setEncoding(encoding) @Slot(bool) def setFontBold(self, state): """ Toggles whether or not the text is currently bold. :param state | <bool> """ if state: weight = QFont.Bold else: weight = QFont.Normal self.setFontWeight(weight) @Slot(str) def setFontFamily(self, family): """ Sets the current font family to the inputed family. :param family | <str> """ self.blockSignals(True) self.editor().setFontFamily(family) self.blockSignals(False) @Slot(bool) def setFontItalic(self, state): """ Toggles whehter or not the text is currently italic. :param state | <bool> """ font = self.currentFont() font.setItalic(state) self.setCurrentFont(font) @Slot(int) def setFontPointSize(self, size): """ Sets the point size of the current font to the inputed size. :param size | <int> """ self.blockSignals(True) self.editor().setFontPointSize(size) self.blockSignals(False) @Slot(bool) def setFontStrikeOut(self, strikeOut): """ Sets whether or not this editor is currently striking out the text. :param strikeOut | <bool> """ font = self.currentFont() font.setStrikeOut(strikeOut) self.setCurrentFont(font) @Slot(bool) def setFontUnderline(self, state): """ Sets whether or not this editor is currently in underline state. :param state | <bool> """ font = self.currentFont() font.setUnderline(state) self.setCurrentFont(font) @Slot(QFont.Weight) def setFontWeight(self, weight): """ Sets the font weight for this editor to the inputed weight. :param weight | <QFont.Weight> """ font = self.currentFont() font.setWeight(weight) self.setCurrentFont(font) @Slot(str) def setText(self, text): """ Sets the text for this editor. :param text | <str> """ self.editor().setText(text) @Slot(QColor) def setTextBackgroundColor(self, color): """ Sets the text background color for this instance to the inputed color. :param color | <QColor> """ self.editor().setTextBackgroundColor(QColor(color)) @Slot(QColor) def setTextColor(self, color): """ Sets the text color for this instance to the inputed color. :param color | <QColor> """ self.editor().setTextColor(QColor(color)) def textBackgroundColor(self): """ Returns the background color that is current in the document. :return <QColor> """ return self.editor().textBackgroundColor() def textColor(self): """ Returns the text color that is current in the document. :return <QColor> """ return self.editor().textColor() def toDiv(self, style='document'): """ Returns the text as paragaphed HTML vs. a full HTML document page. :return <str> """ if not self.editor().toPlainText(): return '' html = self.editor().document().toHtml(self.encoding()) html = projex.text.encoded(html, self.encoding()) html = html.replace('style="-qt-paragraph-type:empty', 'class="paragraph_empty" style="-qt-paragraph-type:empty') # strip out any existing style's because we want to control this # via style sheets results = re.findall(r'\<(\w+)\s+(style="[^"]*")', html) for tag, tag_style in results: # keep any span stylings as these are inline additions if tag == 'span': continue html = html.replace(tag_style, '') start = '<body ' end = '</body>' start_i = html.find(start) end_i = html.find(end) stripped = html[start_i+len(start):end_i] return '<div class="%s" %s</div>' % (style, stripped) def toHtml(self): """ Returns the text as HTML. :return <str> """ if self.editor().toPlainText(): return self.editor().toHtml() return '' def toPlainText(self): """ Returns the text as plain text. :return <str> """ return self.editor().toPlainText() def updateFontPicker(self): """ Updates the font picker widget to the current font settings. """ font = self.currentFont() self._fontPickerWidget.setPointSize(font.pointSize()) self._fontPickerWidget.setCurrentFamily(font.family()) x_encoding = Property(str, encoding, setEncoding)
class XConsoleEdit(XLoggerWidget): __designer_icon__ = projexui.resources.find('img/ui/console.png') executeRequested = Signal(str) def __init__(self, parent): super(XConsoleEdit, self).__init__(parent) # create custom properties self._scope = __main__.__dict__ self._initialized = False self._completerTree = None self._commandStack = [] self._history = [] self._currentHistoryIndex = 0 self._waitingForInput = False self._commandLineInteraction = False self._highlighter = XPythonHighlighter(self.document()) # setup the look for the console self.setReadOnly(False) self.waitForInput() self.setConfigurable(False) def _information(self, msg): locker = QMutexLocker(self._mutex) msg = projex.text.nativestring(msg) self.moveCursor(QTextCursor.End) self.setCurrentMode(logging.INFO) self.insertPlainText(msg) self.scrollToEnd() def _error(self, msg): locker = QMutexLocker(self._mutex) msg = projex.text.nativestring(msg) self.moveCursor(QTextCursor.End) self.setCurrentMode(logging.ERROR) self.insertPlainText(msg) self.scrollToEnd() if not self._waitingForInput: self._waitingForInput = True QTimer.singleShot(50, self.waitForInput) def acceptCompletion(self): """ Accepts the current completion and inserts the code into the edit. :return <bool> accepted """ tree = self._completerTree if not tree: return False tree.hide() item = tree.currentItem() if not item: return False # clear the previously typed code for the block cursor = self.textCursor() text = cursor.block().text() col = cursor.columnNumber() end = col while col: col -= 1 if text[col] in ('.', ' '): col += 1 break # insert the current text cursor.setPosition(cursor.position() - (end - col), cursor.KeepAnchor) cursor.removeSelectedText() self.insertPlainText(item.text(0)) return True def applyCommand(self): """ Applies the current line of code as an interactive python command. """ # generate the command information cursor = self.textCursor() cursor.movePosition(cursor.EndOfLine) line = projex.text.nativestring(cursor.block().text()) at_end = cursor.atEnd() modifiers = QApplication.instance().keyboardModifiers() mod_mode = at_end or modifiers == Qt.ShiftModifier # test the line for information if mod_mode and line.endswith(':'): cursor.movePosition(cursor.EndOfLine) line = re.sub('^>>> ', '', line) line = re.sub('^\.\.\. ', '', line) count = len(line) - len(line.lstrip()) + 4 self.insertPlainText('\n... ' + count * ' ') return False elif mod_mode and line.startswith('...') and \ (line.strip() != '...' or not at_end): cursor.movePosition(cursor.EndOfLine) line = re.sub('^\.\.\. ', '', line) count = len(line) - len(line.lstrip()) self.insertPlainText('\n... ' + count * ' ') return False # if we're not at the end of the console, then add it to the end elif line.startswith('>>>') or line.startswith('...'): # move to the top of the command structure line = projex.text.nativestring(cursor.block().text()) while line.startswith('...'): cursor.movePosition(cursor.PreviousBlock) line = projex.text.nativestring(cursor.block().text()) # calculate the command cursor.movePosition(cursor.EndOfLine) line = projex.text.nativestring(cursor.block().text()) ended = False lines = [] while True: # add the new block lines.append(line) if cursor.atEnd(): ended = True break # move to the next line cursor.movePosition(cursor.NextBlock) cursor.movePosition(cursor.EndOfLine) line = projex.text.nativestring(cursor.block().text()) # check for a new command or the end of the command if not line.startswith('...'): break command = '\n'.join(lines) # if we did not end up at the end of the command block, then # copy it for modification if not (ended and command): self.waitForInput() self.insertPlainText(command.replace('>>> ', '')) cursor.movePosition(cursor.End) return False else: self.waitForInput() return False self.executeCommand(command) return True def cancelCompletion(self): """ Cancels the current completion. """ if self._completerTree: self._completerTree.hide() def clear(self): """ Clears the current text and starts a new input line. """ super(XConsoleEdit, self).clear() self.waitForInput() def commandLineInteraction(self): """ Returns whether or not the console is using interaction like the command line. :return <bool> """ return self._commandLineInteraction def completerTree(self): """ Returns the completion tree for this instance. :return <QTreeWidget> """ if not self._completerTree: self._completerTree = QTreeWidget(self) self._completerTree.setWindowFlags(Qt.Popup) self._completerTree.setAlternatingRowColors(True) self._completerTree.installEventFilter(self) self._completerTree.itemClicked.connect(self.acceptCompletion) self._completerTree.setRootIsDecorated(False) self._completerTree.header().hide() return self._completerTree def eventFilter(self, obj, event): """ Filters particular events for a given QObject through this class. \ Will use this to intercept events to the completer tree widget while \ filtering. :param obj | <QObject> event | <QEvent> :return <bool> consumed """ if not obj == self._completerTree: return False if event.type() != event.KeyPress: return False if event.key() == Qt.Key_Escape: QToolTip.hideText() self.cancelCompletion() return False elif event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab): self.acceptCompletion() return False elif event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): return False else: self.keyPressEvent(event) # update the completer cursor = self.textCursor() text = projex.text.nativestring(cursor.block().text()) text = text[:cursor.columnNumber()].split(' ')[-1] text = text.split('.')[-1] self._completerTree.blockSignals(True) self._completerTree.setUpdatesEnabled(False) self._completerTree.setCurrentItem(None) for i in range(self._completerTree.topLevelItemCount()): item = self._completerTree.topLevelItem(i) if projex.text.nativestring(item.text(0)).startswith(text): self._completerTree.setCurrentItem(item) break self._completerTree.blockSignals(False) self._completerTree.setUpdatesEnabled(True) return True def executeCommand(self, command): """ Executes the inputed command in the global scope. :param command | <unicode> :return <variant> """ if not command.strip(): return self.waitForInput() # store the current block self._history.append(command) self._currentHistoryIndex = len(self._history) lines = [] for line in command.split('\n'): line = re.sub('^>>> ', '', line) line = re.sub('^\.\.\. ', '', line) lines.append(line) command = '\n'.join(lines) # ensure we are at the end self.moveCursor(QTextCursor.End) self.scrollToEnd() self.insertPlainText('\n') cmdresult = None try: cmdresult = eval(command, self.scope(), self.scope()) except SyntaxError: exec(command) in self.scope(), self.scope() else: if cmdresult is not None: # check to see if the command we executed actually caused # the destruction of this object -- if it did, then # the commands below will error if self.isDestroyed(): return try: result = projex.text.nativestring(repr(cmdresult)) except: result = '<<< error formatting result to utf-8 >>>' self.information(result) finally: self.waitForInput() def dragEnterEvent(self, event): if event.keyboardModifiers() == Qt.ShiftModifier: event.acceptProposedAction() else: super(XConsoleEdit, self).dragEnterEvent(event) def dragMoveEvent(self, event): if event.keyboardModifiers() == Qt.ShiftModifier: event.acceptProposedAction() else: super(XConsoleEdit, self).dragMoveEvent(event) def dropEvent(self, event): if event.keyboardModifiers() == Qt.ShiftModifier: self.insertPlainText('\n' + projexui.formatDropEvent(event)) else: super(XConsoleEdit, self).dropEvent(event) def highlighter(self): """ Returns the console highlighter for this widget. :return <XPythonHighlighter> """ return self._highlighter def gotoHome(self): """ Navigates to the home position for the edit. """ mode = QTextCursor.MoveAnchor # select the home if QApplication.instance().keyboardModifiers() == Qt.ShiftModifier: mode = QTextCursor.KeepAnchor cursor = self.textCursor() block = projex.text.nativestring(cursor.block().text()) cursor.movePosition(QTextCursor.StartOfBlock, mode) if block.startswith('>>> '): cursor.movePosition(QTextCursor.Right, mode, 4) elif block.startswith('... '): match = re.match('...\s*', block) cursor.movePosition(QTextCursor.Right, mode, match.end()) self.setTextCursor(cursor) def insertFromMimeData(self, source): """ Inserts the information from the inputed source. :param source | <QMimeData> """ lines = projex.text.nativestring(source.text()).splitlines() for i in range(1, len(lines)): if not lines[i].startswith('... '): lines[i] = '... ' + lines[i] if len(lines) > 1: lines.append('... ') self.insertPlainText('\n'.join(lines)) def insertNextCommand(self): """ Inserts the previous command from history into the line. """ self._currentHistoryIndex += 1 if 0 <= self._currentHistoryIndex < len(self._history): cmd = self._history[self._currentHistoryIndex] else: cmd = '>>> ' self._currentHistoryIndex = -1 self.replaceCommand(cmd) def insertPreviousCommand(self): """ Inserts the previous command from history into the line. """ self._currentHistoryIndex -= 1 if 0 <= self._currentHistoryIndex < len(self._history): cmd = self._history[self._currentHistoryIndex] else: cmd = '>>> ' self._currentHistoryIndex = len(self._history) self.replaceCommand(cmd) def keyPressEvent(self, event): """ Overloads the key press event to control keystroke modifications for \ the console widget. :param event | <QKeyEvent> """ # enter || return keys will apply the command if event.key() in (Qt.Key_Return, Qt.Key_Enter): self.applyCommand() event.accept() # home key will move the cursor to the home position elif event.key() == Qt.Key_Home: self.gotoHome() event.accept() elif event.key() in (Qt.Key_Backspace, Qt.Key_Delete): super(XConsoleEdit, self).keyPressEvent(event) # update the completer cursor = self.textCursor() text = projex.text.nativestring(cursor.block().text()) text = text[:cursor.columnNumber()].split(' ')[-1] if not '.' in text: self.cancelCompletion() # period key will trigger a completion popup elif event.key() == Qt.Key_Period or \ (Qt.Key_A <= event.key() <= Qt.Key_Z): super(XConsoleEdit, self).keyPressEvent(event) self.startCompletion(force=event.key() == Qt.Key_Period) # space, tab, backspace and delete will cancel the completion elif event.key() == Qt.Key_Space: self.cancelCompletion() super(XConsoleEdit, self).keyPressEvent(event) # left parenthesis will start method help elif event.key() == Qt.Key_ParenLeft: self.cancelCompletion() self.showMethodToolTip() super(XConsoleEdit, self).keyPressEvent(event) # Ctrl+Up will load previous commands elif event.key() == Qt.Key_Up: if self.commandLineInteraction() or \ event.modifiers() & Qt.ControlModifier: self.insertPreviousCommand() event.accept() else: super(XConsoleEdit, self).keyPressEvent(event) # Ctrl+Down will load future commands elif event.key() == Qt.Key_Down: if self.commandLineInteraction() or \ event.modifiers() & Qt.ControlModifier: self.insertNextCommand() event.accept() else: super(XConsoleEdit, self).keyPressEvent(event) # otherwise, handle the event like normal else: super(XConsoleEdit, self).keyPressEvent(event) def objectAtCursor(self): """ Returns the python object that the text is representing. :return <object> || None """ # determine the text block cursor = self.textCursor() text = projex.text.nativestring(cursor.block().text()) position = cursor.positionInBlock() - 1 if not text: return (None, '') symbol = '' for match in re.finditer('[\w\.]+', text): if match.start() <= position <= match.end(): symbol = match.group() break if not symbol: return (None, '') parts = symbol.split('.') if len(parts) == 1: return (self.scope(), parts[0]) part = parts[0] obj = self.scope().get(part) for part in parts[1:-1]: try: obj = getattr(obj, part) except AttributeError: return (None, '') return (obj, parts[-1]) def restoreSettings(self, settings): hist = [] settings.beginGroup('console') for key in sorted(settings.childKeys()): hist.append(unwrapVariant(settings.value(key))) settings.endGroup() self._history = hist def saveSettings(self, settings): settings.beginGroup('console') for i, text in enumerate(self._history): settings.setValue('command_{0}'.format(i), wrapVariant(text)) settings.endGroup() def showEvent(self, event): super(XConsoleEdit, self).showEvent(event) if not self._initialized: self._initialized = True # create connections if os.environ.get('XUI_DISABLE_CONSOLE') != '1': hook = XIOHook.instance() hook.printed.connect(self._information) hook.errored.connect(self._error) # setup the header opts = {'version': sys.version, 'platform': sys.platform} self.setText(HEADER.format(**opts)) self.waitForInput() def showMethodToolTip(self): """ Pops up a tooltip message with the help for the object under the \ cursor. :return <bool> success """ self.cancelCompletion() obj, _ = self.objectAtCursor() if not obj: return False docs = inspect.getdoc(obj) if not docs: return False # determine the cursor position rect = self.cursorRect() cursor = self.textCursor() point = QPoint(rect.left(), rect.top() + 18) QToolTip.showText(self.mapToGlobal(point), docs, self) return True def replaceCommand(self, cmd): # move to the top of the command structure self.moveCursor(QTextCursor.End) cursor = self.textCursor() line = projex.text.nativestring(cursor.block().text()) while line.startswith('...'): cursor.movePosition(cursor.PreviousBlock) line = projex.text.nativestring(cursor.block().text()) # calculate the command cursor.movePosition(cursor.StartOfLine) cursor.movePosition(cursor.End, cursor.KeepAnchor) cursor.removeSelectedText() cursor.insertText(cmd) self.moveCursor(cursor.End) def scope(self): """ Returns the dictionary scope that will be used when working with this editor. :return <dict> """ return self._scope def setCommandLineInteraction(self, state=True): """ Sets whether or not the interaction should follow command-line standards (Up/Down navigation) or not (CTRL+Up/Down). :param state | <bool> """ self._commandLineInteraction = state def setScope(self, scope): """ Sets the scope that will be used for this editor. :param scope | <dict> """ self._scope = scope def startCompletion(self, force=False): """ Starts a new completion popup for the current object. :return <bool> success """ # add the top level items tree = self.completerTree() if not force and tree.isVisible(): return tree.clear() # make sure we have a valid object obj, remain = self.objectAtCursor() if obj is None: tree.hide() return # determine the cursor position rect = self.cursorRect() cursor = self.textCursor() point = QPoint(rect.left(), rect.top() + 18) # compare the ids since some things might overload the __eq__ # comparator if id(obj) == id(self._scope): o_keys = obj.keys() elif obj is not None: o_keys = dir(obj) keys = [key for key in sorted(o_keys) if not key.startswith('_')] if id(obj) == id(self._scope): if not remain: return False else: keys = filter(lambda x: x.startswith(remain[0]), keys) if not keys: return False for key in keys: tree.addTopLevelItem(QTreeWidgetItem([key])) tree.move(self.mapToGlobal(point)) tree.show() return True def waitForInput(self): """ Inserts a new input command into the console editor. """ self._waitingForInput = False try: if self.isDestroyed() or self.isReadOnly(): return except RuntimeError: return self.moveCursor(QTextCursor.End) if self.textCursor().block().text() == '>>> ': return # if there is already text on the line, then start a new line newln = '>>> ' if projex.text.nativestring(self.textCursor().block().text()): newln = '\n' + newln # insert the text self.setCurrentMode('standard') self.insertPlainText(newln) self.scrollToEnd() self._blankCache = '' x_commandLineInteraction = Property(bool, commandLineInteraction, setCommandLineInteraction)
class XBoolComboBox(XComboBox): def __init__(self, parent=None): super(XBoolComboBox, self).__init__(parent) # setup properties self.addItem('True') self.addItem('False') def falseText(self): """ Returns the text that will be shown for a false state. :return <str> """ return self.itemText(0) def isChecked(self): """ Returns whether or not this combo box is in a checked (True) state. :return <bool> """ return self.currentIndex() == 0 def setChecked(self, state): """ Sets whether or not this combo box is in a checked (True) state. :param state | <bool> """ if state: index = 0 else: index = 1 self.setCurrentIndex(index) def setFalseText(self, text): """ Sets the text that will be shown for a false state. :param text | <str> """ self.setItemText(1, text) def setTrueText(self, text): """ Sets the text that will be shown for a true state. :param text | <str> """ self.setItemText(0, text) def trueText(self): """ Returns the text that will be shown for a true state. :return <str> """ return self.itemText(0) x_checked = Property(bool, isChecked, setChecked) x_falseText = Property(str, falseText, setFalseText) x_trueText = Property(str, trueText, setTrueText)
class XOrbQuickFilterWidget(QWidget): """ """ __designer_group__ = 'ProjexUI - ORB' queryEntered = Signal(object) def __init__(self, parent=None): super(XOrbQuickFilterWidget, self).__init__(parent) # define custom properties self._tableType = None self._plugins = [] self._filterFormat = '' self._pluginFactory = XOrbQueryPluginFactory() self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout = QVBoxLayout() layout.setContentsMargins(0, 3, 0, 3) self.setLayout(layout) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showMenu) def filterFormat(self): """ Returns the text that defines the filtering options for this widget. :return <str> """ return self._filterFormat def pluginFactory(self): """ Returns the plugin for this filter widget. :return <XOrbQueryPluginFactory> """ return self._pluginFactory def query(self): """ Builds the query for this quick filter. :return <orb.Query> """ output = Query() for column, op, plugin, editor in self._plugins: query = Query(column) if plugin.setupQuery(query, op, editor): output &= query return output def keyPressEvent(self, event): """ Listens for the enter event to check if the query is setup. """ if event.key() in (Qt.Key_Enter, Qt.Key_Return): self.queryEntered.emit(self.query()) super(XOrbQuickFilterWidget, self).keyPressEvent(event) def rebuild(self): """ Rebuilds the data associated with this filter widget. """ table = self.tableType() form = nativestring(self.filterFormat()) if not table and form: if self.layout().count() == 0: self.layout().addWidget(QLabel(form, self)) else: self.layout().itemAt(0).widget().setText(form) return elif not form: return for child in self.findChildren(QWidget): child.close() child.setParent(None) child.deleteLater() self.setUpdatesEnabled(False) schema = table.schema() vlayout = self.layout() for i in range(vlayout.count()): vlayout.takeAt(0) self._plugins = [] for line in form.split('\n'): row = QHBoxLayout() row.setContentsMargins(0, 0, 0, 0) row.setSpacing(0) for label, lookup in FORMAT_SPLITTER.findall(line): # create the label lbl = QLabel(label, self) row.addWidget(lbl) # create the query plugin opts = lookup.split(':') if len(opts) == 1: opts.append('is') column = schema.column(opts[0]) if not column: continue plugin = self.pluginFactory().plugin(column) if not plugin: continue editor = plugin.createEditor(self, column, opts[1], None) if editor: editor.setObjectName(opts[0]) row.addWidget(editor) self._plugins.append((opts[0], opts[1], plugin, editor)) row.addStretch(1) vlayout.addLayout(row) self.setUpdatesEnabled(True) self.adjustSize() def showMenu(self, point): """ Displays the menu for this filter widget. """ menu = QMenu(self) acts = {} acts['edit'] = menu.addAction('Edit quick filter...') trigger = menu.exec_(self.mapToGlobal(point)) if trigger == acts['edit']: text, accepted = XTextEdit.getText(self.window(), 'Edit Format', 'Format:', self.filterFormat(), wrapped=False) if accepted: self.setFilterFormat(text) def setQuery(self, query): """ Sets the query information for this filter widget. :param query | <orb.Query> || None """ if query is None: return count = {} for widget in self.findChildren(QWidget): column = nativestring(widget.objectName()) count.setdefault(column, 0) count[column] += 1 success, value, _ = query.findValue(column, count[column]) if success: projexui.setWidgetValue(widget, value) def setPluginFactory(self, pluginFactory): """ Sets the plugin factory for this widget to the inputed factory. :param pluginFactory | <XOrbPluginFactory> """ self._pluginFactory = pluginFactory def setFilterFormat(self, format): """ Sets the text that defines the filtering options for this widget. :param filterFormat | <str> """ self._filterFormat = format self.rebuild() def setTableType(self, tableType): """ Sets the table type associated with this filter widget. :param tableType | <subclass of orb.TableType> """ self._tableType = tableType self.rebuild() def tableType(self): """ Returns the table type associated with this filter widget. :return <subclass of orb.TableType> """ return self._tableType x_filterFormat = Property(str, filterFormat, setFilterFormat)
class XEnumBox(XComboBox): """ Creates a widget for editing enumerated types easily. By default, this will be a single selection widget, but if you want a bit or operation for the value, set the checkable state to True. This class inherits the [[./xcombobox-XComboBox|XComboBox]] class and utilizes the enum class from the [[$API/projex/enum-enum|projex.enum]] module. == Example == |>>> from projexui.widgets.xenumbox import XEnumBox |>>> import projexui | |>>> # create the enum box |>>> combo = projexui.testWidget(XEnumBox) | |>>> # create the enum type |>>> from projex.enum import enum |>>> Type = enum('Normal', 'Rounded', 'Smooth') | |>>> # link the enum to the combo |>>> combo.setEnum(Type) | |>>> # set the enum value |>>> combo.setCurrentValue(Type.Smooth) | |>>> # set the combobox enum values |>>> combo.setCheckable(True) | |>>> # set multiple values at once |>>> combo.setCurrentValue(Type.Smooth | Type.Rounded) | |>>> # retrieve the current value |>>> combo.currentValue() |4 | |>>> # connect to signals |>>> def printValue(value): print value |>>> combo.valueChanged.connect(printValue) """ valueChanged = Signal(int) def __init__(self, parent=None): super(XEnumBox, self).__init__(parent) # define custom properties self._enum = None self._required = False self._sortByKey = True # set default properties # create connections self.currentIndexChanged.connect(self.emitValueChanged) self.checkedIndexesChanged.connect(self.emitValueChanged) def currentValue(self): """ Returns the current value for the widget. If this widget is checkable then the bitor value for all checked items is returned, otherwise, the selected value is returned. :return <int> """ enum = self.enum() if (self.isCheckable()): value = 0 for i in self.checkedIndexes(): value |= enum[nativestring(self.itemText(i))] return value else: try: return enum[nativestring(self.itemText(self.currentIndex()))] except KeyError: return 0 def enum(self): """ Returns the enum type that is linked with this widget. :return <projex.enum.enum> """ return self._enum def emitValueChanged(self): """ Emits the value changed signal with the current value if the signals for this widget aren't currently being blocked. """ if not self.signalsBlocked(): self.valueChanged.emit(self.currentValue()) def isRequired(self): """ Returns whether or not a value is required for this enumeration. :return <bool> """ return self._required def reload(self): """ Reloads the contents for this box. """ enum = self._enum if not enum: return self.clear() if not self.isRequired(): self.addItem('') if self.sortByKey(): self.addItems(sorted(enum.keys())) else: items = enum.items() items.sort(key=lambda x: x[1]) self.addItems(map(lambda x: x[0], items)) def setEnum(self, enum): """ Sets the enum to the inputed enumerated value. :param enum | <projex.enum.enum> """ self._enum = enum self.reload() def setRequired(self, state): """ Sets whether or not the value is required for this enumeration. :param state | <bool> """ self._required = state def setCurrentValue(self, value): """ Sets the value for the combobox to the inputed value. If the combobox is in a checkable state, then the values will be checked, otherwise, the value will be selected. :param value | <int> """ enum = self.enum() if not enum: return if self.isCheckable(): indexes = [] for i in range(self.count()): try: check_value = enum[nativestring(self.itemText(i))] except KeyError: continue if check_value & value: indexes.append(i) self.setCheckedIndexes(indexes) else: try: text = enum[value] except (AttributeError, KeyError): return self.setCurrentIndex(self.findText(text)) def setSortByKey(self, state): """ Returns whether or not this enum box should sort by key. If False, then the values will be entered based on the value. :param state | <bool> """ self._sortByKey = state def sortByKey(self): """ Returns whether or not this enum box should sort by key. If False, then the values will be entered based on the value. :return <bool> """ return self._sortByKey x_required = Property(bool, isRequired, setRequired) x_sortByKey = Property(bool, sortByKey, setSortByKey)
class XToolButton(QtGui.QToolButton): def __init__(self, *args): super(XToolButton, self).__init__(*args) # define custom properties self._colored = False self._shadowed = False self._shadowRadius = 20 self._clickable = True self._angle = 0 self._flipVertical = False self._flipHorizontal = False self._movie = None self._blinking = False self._blinkInterval = 500 # msecs self._hoverable = False self._hoverIcon = None # assign this toolbutton on a XToolBar class if len(args) > 0: parent = args[0] if isinstance(parent, xtoolbar.XToolBar): palette = self.parent().palette() self.setPalette(palette) self.setToolButtonStyle(parent.toolButtonStyle()) self.triggered.connect(parent.actionTriggered) self.setShadowed(parent.isShadowed()) self.setColored(parent.isColored()) # update the ui when it is toggled self.toggled.connect(self.updateUi) def _updateFrame(self): """ Sets the icon for this button to the frame at the given number. """ self.setIcon(QtGui.QIcon(self._movie.currentPixmap())) def angle(self): """ Returns the angle that this button should be rotated. :return <int> """ return self._angle def blink(self, state=True): """ Starts or stops the blinking state for this button. This only works for when the toolbutton is in Shadowed or Colored mode. :param state | <bool> :return <bool> | success """ if self._blinking == state: return True elif not self.graphicsEffect(): return False else: self._blinking = state if state: self.startTimer(self.blinkInterval()) def blinkInterval(self): """ Returns the number of milliseconds that this button will blink for when it is in the blinking state. :return <int> """ return self._blinkInterval def cleanup(self): """ Cleanup references to the movie when this button is destroyed. """ if self._movie is not None: self._movie.frameChanged.disconnect(self._updateFrame) self._movie = None def enterEvent(self, event): if self.isHoverable(): super(XToolButton, self).setIcon(self._hoverIcon) if self.isShadowed(): if self.isClickable() and self.isEnabled(): effect = self.graphicsEffect() palette = self.palette() clr = palette.color(palette.Shadow) effect.setColor(clr) elif self.isColored(): if self.isClickable() and self.isEnabled(): effect = self.graphicsEffect() effect.setStrength(1) else: super(XToolButton, self).enterEvent(event) def flipHorizontal(self): """ Returns whether or not the button should be flipped horizontally. :return <bool> """ return self._flipHorizontal def flipVertical(self): """ Returns whether or not the button should be flipped horizontally. :return <bool> """ return self._flipVertical def movie(self): """ Returns the movie instance associated with this button. :return <QtGui.QMovie> || None """ return self._movie def isBlinking(self): """ Returns whether or not this button is currently blinking. :return <bool> """ return self._blinking def isClickable(self): return self._clickable def isColored(self): return self._colored def isHoverable(self): """ Returns whether or not this button should hide its icon when not hovered. :return <bool> """ return self._hoverable def isShadowed(self): return self._shadowed def leaveEvent(self, event): if self.isHoverable(): super(XToolButton, self).setIcon(QtGui.QIcon()) if self.isShadowed() or self.isColored(): self.updateUi() else: super(XToolButton, self).leaveEvent(event) def paintEvent(self, event): """ Overloads the paint even to render this button. """ if self.isHoverable() and self.icon().isNull(): return # initialize the painter painter = QtGui.QStylePainter() painter.begin(self) try: option = QtGui.QStyleOptionToolButton() self.initStyleOption(option) # generate the scaling and rotating factors x_scale = 1 y_scale = 1 if self.flipHorizontal(): x_scale = -1 if self.flipVertical(): y_scale = -1 center = self.rect().center() painter.translate(center.x(), center.y()) painter.rotate(self.angle()) painter.scale(x_scale, y_scale) painter.translate(-center.x(), -center.y()) painter.drawComplexControl(QtGui.QStyle.CC_ToolButton, option) finally: painter.end() def setAngle(self, angle): """ Sets the angle that this button should be rotated. :param angle | <int> """ self._angle = angle def setBlinkInterval(self, msecs): """ Sets the number of milliseconds that this button will blink for when it is in the blinking state. :param msecs | <int> """ self._blinkInterval = msecs def setClickable(self, state): self._clickable = state if not state: self.setStyleSheet(UNCLICKABLE_SHEET) elif self.isShadowed() or self.isColored(): self.setStyleSheet(CLICKABLE_SHEET) else: self.setStyleSheet('') def setColored(self, state): self._colored = state if state: self._shadowed = False palette = self.palette() effect = QtGui.QGraphicsColorizeEffect(self) effect.setStrength(0) effect.setColor(palette.color(palette.Highlight)) self.setGraphicsEffect(effect) if self.isClickable(): self.setStyleSheet(CLICKABLE_SHEET) else: self.setStyleSheet(UNCLICKABLE_SHEET) self.updateUi() else: self.setStyleSheet('') self.setGraphicsEffect(None) self.blink(False) def setHoverable(self, state): """ Sets whether or not this is a hoverable button. When in a hoverable state, the icon will only be visible when the button is hovered on. :param state | <bool> """ self._hoverable = state self._hoverIcon = self.icon() def setIcon(self, icon): super(XToolButton, self).setIcon(icon) if self.isHoverable(): self._hoverIcon = icon def setEnabled(self, state): """ Updates the drop shadow effect for this widget on enable/disable state change. :param state | <bool> """ super(XToolButton, self).setEnabled(state) self.updateUi() def setFlipHorizontal(self, state): """ Sets whether or not the button should be flipped horizontally. :param state | <bool> """ self._flipHorizontal = state def setFlipVertical(self, state): """ Sets whether or not the button should be flipped vertically. :param state | <bool> """ self._flipVertical = state def setMovie(self, movie): """ Sets the movie instance for this button. :param movie | <QtGui.QMovie> """ if self._movie is not None: self._movie.frameChanged.disconnect(self._updateFrame) self._movie = movie if movie is not None: self._updateFrame() self._movie.frameChanged.connect(self._updateFrame) self.destroyed.connect(self.cleanup) def setPalette(self, palette): """ Sets the palette for this button to the inputed palette. This will update the drop shadow to the palette's Shadow color property if the shadowed mode is on. :param palette | <QtGui.QPalette> """ super(XToolButton, self).setPalette(palette) self.updateUi() def setShadowRadius(self, radius): self._shadowRadius = radius def setShadowed(self, state): self._shadowed = state if state: self._colored = False effect = QtGui.QGraphicsDropShadowEffect(self) effect.setColor(QtGui.QColor(0, 0, 0, 0)) effect.setOffset(0, 0) effect.setBlurRadius(self.shadowRadius()) self.setGraphicsEffect(effect) if self.isClickable(): self.setStyleSheet(CLICKABLE_SHEET) else: self.setStyleSheet(UNCLICKABLE_SHEET) self.updateUi() else: self.setStyleSheet('') self.setGraphicsEffect(None) self.blink(False) def shadowRadius(self): return self._shadowRadius def showEvent(self, event): super(XToolButton, self).showEvent(event) if self.isHoverable(): super(XToolButton, self).setIcon(QtGui.QIcon()) def timerEvent(self, event): effect = self.graphicsEffect() if not (effect and self.isBlinking()): self.killTimer(event.timerId()) elif isinstance(effect, QtGui.QGraphicsDropShadowEffect): palette = self.palette() transparent = QtGui.QColor(0, 0, 0, 0) clr = palette.color(palette.Shadow) if effect.color() == transparent: effect.setColor(clr) else: effect.setColor(transparent) elif isinstance(effect, QtGui.QGraphicsColorizeEffect): effect.setStrength(int(not effect.strength())) def updateUi(self): if not self.isClickable(): return effect = self.graphicsEffect() if isinstance(effect, QtGui.QGraphicsDropShadowEffect): palette = self.palette() transparent = QtGui.QColor(0, 0, 0, 0) clr = palette.color(palette.Shadow) show = self.isChecked() and self.isEnabled() effect.setColor(transparent if not show else clr) elif isinstance(effect, QtGui.QGraphicsColorizeEffect): effect.setStrength(1 if self.isChecked() else 0) # define properties x_angle = Property(int, angle, setAngle) x_clickable = Property(bool, isClickable, setClickable) x_colored = Property(bool, isColored, setColored) x_flipHorizontal = Property(bool, flipHorizontal, setFlipHorizontal) x_flipVertical = Property(bool, flipVertical, setFlipVertical) x_shadowRadius = Property(int, shadowRadius, setShadowRadius) x_shadowed = Property(bool, isShadowed, setShadowed)
class XUrlWidget(QWidget): urlChanged = Signal(str) urlEdited = Signal() def __init__(self, parent): super(XUrlWidget, self).__init__(parent) # define the interface self._urlEdit = XLineEdit(self) self._urlButton = QToolButton(self) self._urlButton.setAutoRaise(True) self._urlButton.setIcon(QIcon(resources.find('img/web.png'))) self._urlButton.setToolTip('Browse Link') self._urlButton.setFocusPolicy(Qt.NoFocus) self._urlEdit.setHint('http://') layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._urlEdit) layout.addWidget(self._urlButton) self.setLayout(layout) self.setFocusPolicy(Qt.StrongFocus) # create connections self._urlEdit.textChanged.connect(self.urlChanged) self._urlEdit.textEdited.connect(self.urlEdited) self._urlButton.clicked.connect(self.browse) def blockSignals(self, state): """ Blocks the signals for this widget and its sub-parts. :param state | <bool> """ super(XUrlWidget, self).blockSignals(state) self._urlEdit.blockSignals(state) self._urlButton.blockSignals(state) def browse(self): """ Brings up a web browser with the address in a Google map. """ webbrowser.open(self.url()) def hint(self): """ Returns the hint associated with this widget. :return <str> """ return self._urlEdit.hint() def lineEdit(self): """ Returns the line edit linked with this widget. :return <XLineEdit> """ return self._urlEdit def setFocus(self): """ Sets the focus for this widget on its line edit. """ self._urlEdit.setFocus() @Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._urlEdit.setHint(hint) @Slot(str) def setUrl(self, url): """ Sets the url for this widget to the inputed url. :param url | <str> """ self._urlEdit.setText(nativestring(url)) def url(self): """ Returns the current url from the edit. :return <str> """ return nativestring(self._urlEdit.text()) x_hint = Property(str, hint, setHint) x_url = Property(str, url, setUrl)
class XOrbColumnEdit(QWidget): """ """ __designer_group__ = 'ProjexUI - ORB' def __init__( self, parent = None ): super(XOrbColumnEdit, self).__init__( parent ) # define custom properties # set default properties self._columnType = None self._columnName = '' self._options = None self._editor = None # set the layout for this object layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) if ( ColumnType ): self.setColumnType(ColumnType.String) # create connections def columnName( self ): """ Returns the column name that will be used for this record edit. :return <str> """ return self._columnName def columnType( self ): """ Returns the column type that is linked with this widget. :return <orb.ColumnType> """ return self._columnType def columnTypeText( self ): """ Returns the string representation of the current column type. :return <str> """ if ( ColumnType ): try: return ColumnType[self._columnType] except KeyError: return 0 return 0 def editor( self ): """ Returns the editor instance being used for this widget. :return <QWidget> || None """ return self._editor def label( self ): """ Returns the label for this widget. Varies per type, not all types have labels. :return <str> """ if ( self._editor and hasattr(self._editor, 'label') ): return self._editor.label() return '' def isReadOnly( self ): """ Returns the read only for this widget from the editor. Differs per type, not all types support read only. :param text | <str> """ if ( self._editor and hasattr(self._editor, 'isReadOnly') ): return self._editor.isReadOnly() return False def rebuild( self ): """ Clears out all the child widgets from this widget and creates the widget that best matches the column properties for this edit. """ plugins.init() self.blockSignals(True) self.setUpdatesEnabled(False) # clear the old editor if ( self._editor ): self._editor.close() self._editor.setParent(None) self._editor.deleteLater() self._editor = None # create a new widget plugin_class = plugins.widgets.get(self._columnType) if ( plugin_class ): self._editor = plugin_class(self) self.layout().addWidget(self._editor) self.blockSignals(False) self.setUpdatesEnabled(True) def setColumn( self, column ): """ Sets the column instance for this edit to the given column. :param column | <orb.Column> """ if ( not column ): return self._columnName = column.name() if ( column.columnType() != ColumnType.ForeignKey ): return if ( self._editor ): self._editor.setTableType(column.referenceModel()) self._editor.setRequired(column.required()) def setColumnName( self, columnName ): """ Returns the column name that will be used for this edit. :param columnName | <str> """ self._columnName = str(columnName) @Slot(int) def setColumnType( self, columnType ): """ Sets the column type for this widget to the inputed type value. This will reset the widget to use one of the plugins for editing the value of the column. :param columnType | <orb.ColumnType> """ if ( columnType == self._columnType ): return False self._columnType = columnType self.rebuild() return True @Slot(str) def setColumnTypeText( self, columnTypeText ): """ Sets the column type for this widget based on the inputed text. :param columnTypeText | <str> """ if ( not ColumnType ): return False try: columnType = ColumnType[str(columnTypeText)] except KeyError: return False return self.setColumnType(columnType) def setLabel( self, text ): """ Sets the label for this widget to the inputed text. Differs per type. :param text | <str> """ if ( self._editor and hasattr(self._editor, 'setLabel') ): self._editor.setLabel(text) return True return False def setReadOnly( self, state ): """ Sets the read only for this widget to the inputed state. Differs per type, not all types support read only. :param text | <str> """ if ( self._editor and hasattr(self._editor, 'setReadOnly') ): self._editor.setReadOnly(state) return True return False @Slot(PyObject) def setValue( self, value ): """ Sets the value for this edit to the inputed value. :param value | <variant> """ if ( self._editor ): self._editor.setValue(value) return True return False def value( self ): """ Returns the current value for this widget. :return <variant> """ if ( self._editor ): return self._editor.value() return None x_columnTypeText = Property(str, columnTypeText, setColumnTypeText) x_columnName = Property(str, columnName, setColumnName) x_label = Property(str, label, setLabel) x_readOnly = Property(bool, isReadOnly, setReadOnly)
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 XOrbGridEdit(QWidget): """ """ __designer_group__ = 'ProjexUI - ORB' def __init__(self, parent=None): super(XOrbGridEdit, self).__init__(parent) # load the user interface projexui.loadUi(__file__, self) # define custom properties self._queryWidget = XOrbQueryWidget(self) self.uiSearchTXT.setIconSize(QSize(28, 28)) self.uiSearchTXT.addButton(self.uiQueryBTN) self.uiQueryBTN.setCentralWidget(self._queryWidget) self.uiQueryBTN.setDefaultAnchor(XPopupWidget.Anchor.TopRight) popup = self.uiQueryBTN.popupWidget() popup.setShowTitleBar(False) # set default properties self.uiRecordTREE.setGroupingEnabled(False) self.uiRecordTREE.setEditable(True) self.uiRecordTREE.setPageSize(50) self.uiRecordTREE.setTabKeyNavigation(True) # create connections self.uiRefreshBTN.clicked.connect(self.refresh) self.uiSaveBTN.clicked.connect(self.commit) self.uiQueryBTN.popupAboutToShow.connect(self.loadQuery) self.uiQueryBTN.popupAccepted.connect(self.assignQuery) self.uiRecordTREE.headerMenuAboutToShow.connect(self.updateMenu) popup.resetRequested.connect(self._queryWidget.reset) def autoloadPages(self): """ Returns whether or not to automatically load pages for this edit. :sa XOrbTreeWidget.autoloadPages :return <bool> """ return self.uiRecordTREE.autoloadPages() def assignQuery(self): """ Assigns the query from the query widget to the edit. """ self.uiRecordTREE.setQuery(self._queryWidget.query(), autoRefresh=True) def commit(self): """ Commits changes stored in the interface to the database. """ self.uiRecordTREE.commit() def disableGrouping(self): """ Disables the grouping component for this edit. """ self.uiRecordTREE.disableGrouping() self.refresh() def enableGrouping(self): """ Disables the grouping component for this edit. """ self.uiRecordTREE.enableGrouping() self.refresh() def groupByHeaderIndex(self): """ Groups the records based on the last selected header index. """ self.uiRecordTREE.groupByHeaderIndex() self.refresh() def isEditable(self): """ Returns whether or not this grid edit is editable. :return <bool> """ return self.uiRecordTREE.isEditable() def isPaged(self): """ Returns whether or not to pages the results from the database query. :sa XOrbTreeWidget.isPaged :param state | <bool> """ return self.uiRecordTREE.isPaged() def loadQuery(self): """ Loads the query for the query widget when it is being shown. """ self._queryWidget.setQuery(self.query()) def pageSize(self): """ Returns the number of records that should be loaded per page. :sa XOrbTreeWidget.pageSize :return <int> """ return self.uiRecordTREE.pageSize() def query(self): """ Returns the query that is being represented by the current results. :return <orb.Query> """ return self.uiRecordTREE.query() def records(self): """ Returns the records that are currently assigned to this widget. :return <orb.RecordSet> """ return self.uiRecordTREE.records() def refresh(self): """ Commits changes stored in the interface to the database. """ self.uiRecordTREE.refresh(reloadData=True) def restoreXml(self, xml): """ Restores the settings for this edit from xml. :param xml | <xml.etree.ElementTree> """ self.uiRecordTREE.restoreXml(xml.find('tree')) # restore the query xquery = xml.find('query') if xquery is not None: self.setQuery(Q.fromXml(xquery[0])) def saveXml(self, xml): """ Saves the settings for this edit to the xml parent. :param xparent | <xml.etree.ElementTree> """ # save grouping xtree = ElementTree.SubElement(xml, 'tree') self.uiRecordTREE.saveXml(xtree) # save the query query = self.query() if query: query.toXml(ElementTree.SubElement(xml, 'query')) def searchWidget(self): """ Returns the search text edit for this grid edit. :return <XLineEdit> """ return self.uiSearchTXT def setAutoloadPages(self, state): """ Sets whether or not to automatically load pages for this edit. :sa XOrbTreeWidget.setAutoloadPages :param state | <bool> """ return self.uiRecordTREE.setAutoloadPages(state) def setEditable(self, state): """ Sets the editable state for this grid widget. :param state | <bool> """ self.uiRecordTREE.setEditable(state) self.uiSaveBTN.setVisible(state) def setQuery(self, query, autoRefresh=True): """ Sets the query for this edit to the inputed query. :param query | <orb.Query> """ self.uiRecordTREE.setQuery(query, autoRefresh=autoRefresh) def setPaged(self, state): """ Sets whether or not to pages the results from the database query. :sa XOrbTreeWidget.setPaged :param state | <bool> """ return self.uiRecordTREE.setPaged(state) def setPageSize(self, size): """ Sets the number of records that should be loaded per page. :sa XOrbTreeWidget.setPageSize :param size | <int> """ return self.uiRecordTREE.setPageSize(size) def setRecords(self, records): """ Sets the records for this widget to the inputed records. :param records | [<orb.Table>, ..] || <orb.RecordSet> """ self.uiRecordTREE.setRecords(records) def setTableType(self, tableType, autoRefresh=True): """ Sets the table type associated with this edit. :param tableType | <subclass of orb.Table> """ self.uiRecordTREE.setTableType(tableType) self._queryWidget.setTableType(tableType) if autoRefresh: self.setQuery(Q()) def showAdvancedGroupingOptions(self): """ Shows the advanced grouping options for the grid edit. """ pass def tableType(self): """ Returns the table type associated with this edit. :return <subclass of orb.Table> """ return self.uiRecordTREE.tableType() def treeWidget(self): """ Returns the tree widget that is for editing records for this grid. :return <XOrbTreeWidget> """ return self.uiRecordTREE def updateMenu(self, menu, index): tree = self.uiRecordTREE first_action = menu.actions()[1] column = tree.columnOf(index) enable_action = QAction(menu) enable_action.setText('Enable Grouping') disable_action = QAction(menu) disable_action.setText('Disable Grouping') quick_action = QAction(menu) quick_action.setText('Group by "%s"' % column) adv_action = QAction(menu) adv_action.setText('More Grouping Options...') menu.insertSeparator(first_action) menu.insertAction(first_action, enable_action) menu.insertAction(first_action, disable_action) menu.insertSeparator(first_action) menu.insertAction(first_action, quick_action) menu.insertAction(first_action, adv_action) quick_action.triggered.connect(self.groupByHeaderIndex) adv_action.triggered.connect(self.showAdvancedGroupingOptions) enable_action.triggered.connect(self.enableGrouping) disable_action.triggered.connect(self.disableGrouping) x_autoloadPages = Property(bool, autoloadPages, setAutoloadPages) x_paged = Property(bool, isPaged, setPaged) x_pageSize = Property(int, pageSize, setPageSize) x_editable = Property(bool, isEditable, setEditable)
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 XStackedWidget(QStackedWidget): __designer_container__ = True __designer_xml__ = """\ <widget class="XStackedWidget" name="stackedWidget"> <property name="geometry"> <rect> <x>100</x> <y>60</y> <width>321</width> <height>391</height> </rect> </property> <widget class="QWidget" name="page"/> <widget class="QWidget" name="page_2"/> </widget>""" animationFinished = Signal() Direction = enum('LeftToRight', 'RightToLeft', 'TopToBottom', 'BottomToTop', 'Automatic') def __init__(self, parent=None): super(XStackedWidget, self).__init__(parent) # define custom properties self._animationType = QEasingCurve.Linear self._vertical = False self._wrap = False self._active = False self._speed = 250 self._nextIndex = 0 self._lastIndex = 0 self._lastPoint = None def _finishAnimation(self): """ Cleans up post-animation. """ self.setCurrentIndex(self._nextIndex) self.widget(self._lastIndex).hide() self.widget(self._lastIndex).move(self._lastPoint) self._active = False if not self.signalsBlocked(): self.animationFinished.emit() def animationType(self): """ Returns the animation curve type for this widget. :return <QEasingCurve.Type> """ return self._animationType def clear(self): """ Clears out the widgets from this stack. """ for i in range(self.count() - 1, -1, -1): w = self.widget(i) if w: self.removeWidget(w) w.close() w.deleteLater() def isVerticalMode(self): """ Returns whether or not the animation will play vertically or not. :return <bool> """ return self._vertical @Slot(QEasingCurve.Type) def setAnimationType(self, animationType): """ Sets the animation curve type for this widget. :param animationType | <QEasingCurve.Type> """ self._animationType = animationType @Slot(int) def setSpeed(self, speed): """ Sets the speed for this widget. :param speed | <int> """ self._speed = speed @Slot(bool) def setVerticalMode(self, state=True): """ Sets whether or not the animation will play vertically or not. :param state | <bool> """ self._vertical = state @Slot(bool) def setWrap(self, state=True): """ Sets whether or not the stacked widget will wrap during the animation. :param state | <bool> """ self._wrap = state @Slot(int) def slideIn(self, index, direction=Direction.Automatic): """ Slides in the panel at the inputed index in the given direction for this widget. :param index | <int> direction | <XStackedWidget.Direction> :return <bool> | success """ # do not allow multiple slides while it is active if self._active: return False # determine the proper index to calculate invert = False if self.count() <= index: if not self.wrap(): return False index = self.count() % index invert = True elif index < 0: if not self.wrap(): return False index = self.count() + index invert = True # define the direction information if index == self.currentIndex(): return False elif self.currentIndex() < index: if direction == XStackedWidget.Direction.Automatic: if self.isVerticalMode(): direction = XStackedWidget.Direction.BottomToTop else: direction = XStackedWidget.Direction.RightToLeft else: if direction == XStackedWidget.Direction.Automatic: if self.isVerticalMode(): direction = XStackedWidget.Direction.TopToBottom else: direction = XStackedWidget.Direction.LeftToRight # invert the animation if we are wrapping if invert: if direction == XStackedWidget.Direction.BottomToTop: direction = XStackedWidget.Direction.TopToBottom elif direction == XStackedWidget.Direction.TopToBottom: direction = XStackedWidget.Direction.BottomToTop elif direction == XStackedWidget.Direction.LeftToRight: direction = XStackedWidget.Direction.RightToLeft else: direction = XStackedWidget.Direction.LeftToRight self._active = True offset_x = self.frameRect().width() offset_y = self.frameRect().height() next_widget = self.widget(index) curr_widget = self.widget(self.currentIndex()) next_widget.setGeometry(0, 0, offset_x, offset_y) if direction == XStackedWidget.Direction.BottomToTop: offset_x = 0 offset_y = -offset_y elif direction == XStackedWidget.Direction.TopToBottom: offset_x = 0 elif direction == XStackedWidget.Direction.RightToLeft: offset_x = -offset_x offset_y = 0 elif direction == XStackedWidget.Direction.LeftToRight: offset_y = 0 next_point = next_widget.pos() curr_point = curr_widget.pos() self._nextIndex = index self._lastIndex = self.currentIndex() self._lastPoint = QPoint(curr_point) next_widget.move(next_point.x() - offset_x, next_point.y() - offset_y) next_widget.raise_() next_widget.show() curr_anim = QPropertyAnimation(curr_widget, 'pos') curr_anim.setDuration(self.speed()) curr_anim.setEasingCurve(self.animationType()) curr_anim.setStartValue(curr_point) curr_anim.setEndValue( QPoint(curr_point.x() + offset_x, curr_point.y() + offset_y)) next_anim = QPropertyAnimation(next_widget, 'pos') next_anim.setDuration(self.speed()) next_anim.setEasingCurve(self.animationType()) next_anim.setStartValue( QPoint(next_point.x() - offset_x, next_point.y() - offset_y)) next_anim.setEndValue(next_point) anim_group = QParallelAnimationGroup(self) anim_group.addAnimation(curr_anim) anim_group.addAnimation(next_anim) anim_group.finished.connect(self._finishAnimation) anim_group.finished.connect(anim_group.deleteLater) anim_group.start() return True @Slot() def slideInNext(self): """ Slides in the next slide for this widget. :return <bool> | success """ return self.slideIn(self.currentIndex() + 1) @Slot() def slideInPrev(self): """ Slides in the previous slide for this widget. :return <bool> | success """ return self.slideIn(self.currentIndex() - 1) def speed(self): """ Returns the speed property for this stacked widget. :return <int> """ return self._speed def wrap(self): """ Returns whether or not the stacked widget will wrap during the animation. :return <bool> """ return self._wrap x_animationType = Property(QEasingCurve.Type, animationType, setAnimationType) x_speed = Property(int, speed, setSpeed) x_verticalMode = Property(bool, isVerticalMode, setVerticalMode) x_wrap = Property(bool, wrap, setWrap)
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 XPushButton(QPushButton): def __init__(self, *args, **kwds): super(XPushButton, self).__init__(*args, **kwds) # sets whether or not this button will display rich text self._showRichText = False self._richTextLabel = None self._text = '' def eventFilter(self, object, event): """ Ignore all events for the text label. :param object | <QObject> event | <QEvent> """ if object == self._richTextLabel: if event.type() in (event.MouseButtonPress, event.MouseMove, event.MouseButtonRelease, event.MouseButtonDblClick): event.ignore() return True return False def resizeEvent(self, event): """ Overloads the resize event to auto-resize the rich text label to the size of this QPushButton. :param event | <QResizeEvent> """ super(XPushButton, self).resizeEvent(event) if self._richTextLabel: self._richTextLabel.resize(event.size()) def richTextLabel(self): """ Returns the label that is used for drawing the rich text to this button. :return <QLabel> """ if not self._richTextLabel: self._richTextLabel = QLabel(self) self._richTextLabel.installEventFilter(self) self._richTextLabel.setMargin(10) return self._richTextLabel def setShowRichText(self, state): """ Sets whether or not to display rich text for this button. :param state | <bool> """ self._showRichText = state text = self.text() if state: label = self.richTextLabel() label.setText(text) label.show() super(XPushButton, self).setText('') else: if self._richTextLabel: self._richTextLabel.hide() super(XPushButton, self).setText(text) def setText(self, text): """ Sets the text for this button. If it is set to show rich text, then it will update the label text, leaving the root button text blank, otherwise it will update the button. :param text | <str> """ self._text = nativestring(text) if self.showRichText(): self.richTextLabel().setText(text) else: super(XPushButton, self).setText(text) def showRichText(self): """ Returns whether or not rich text is visible for this button. :return <bool> """ return self._showRichText def text(self): """ Returns the source text for this button. :return <str> """ return self._text x_showRichText = Property(bool, showRichText, setShowRichText)
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)
class XLocationWidget(QWidget): locationChanged = Signal(str) locationEdited = Signal() def __init__(self, parent): super(XLocationWidget, self).__init__(parent) # define the interface self._locationEdit = XLineEdit(self) self._locationButton = QToolButton(self) self._urlTemplate = 'http://maps.google.com/maps?%(params)s' self._urlQueryKey = 'q' self._locationButton.setAutoRaise(True) self._locationButton.setIcon(QIcon(resources.find('img/map.png'))) self._locationEdit.setHint('no location set') layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._locationEdit) layout.addWidget(self._locationButton) self.setLayout(layout) # create connections self._locationEdit.textChanged.connect(self.locationChanged) self._locationEdit.textEdited.connect(self.locationEdited) self._locationButton.clicked.connect(self.browseMaps) def blockSignals(self, state): """ Blocks the signals for this widget and its sub-parts. :param state | <bool> """ super(XLocationWidget, self).blockSignals(state) self._locationEdit.blockSignals(state) self._locationButton.blockSignals(state) def browseMaps(self): """ Brings up a web browser with the address in a Google map. """ url = self.urlTemplate() params = urllib.urlencode({self.urlQueryKey(): self.location()}) url = url % {'params': params} webbrowser.open(url) def hint(self): """ Returns the hint associated with this widget. :return <str> """ return self._locationEdit.hint() def lineEdit(self): """ Returns the line edit linked with this widget. :return <XLineEdit> """ return self._locationEdit def location(self): """ Returns the current location from the edit. :return <str> """ return nativestring(self._locationEdit.text()) @Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._locationEdit.setHint(hint) @Slot(str) def setLocation(self, location): """ Sets the location for this widget to the inputed location. :param location | <str> """ self._locationEdit.setText(nativestring(location)) def setUrlQueryKey(self, key): """ Sets the key for the URL to the inputed key. :param key | <str> """ self._urlQueryKey = nativestring(key) def setUrlTemplate(self, templ): """ Sets the URL path template that will be used when looking up locations on the web. :param templ | <str> """ self._urlQueryTemplate = nativestring(templ) def urlQueryKey(self): """ Returns the query key that will be used for this location. :return <str> """ return self._urlQueryKey def urlTemplate(self): """ Returns the url template that will be used when mapping this location. :return <str> """ return self._urlTemplate x_hint = Property(str, hint, setHint) x_location = Property(str, location, setLocation) x_urlQueryKey = Property(str, urlQueryKey, setUrlQueryKey) x_urlTemplate = Property(str, urlTemplate, setUrlTemplate)
class XNodeWidget(QGraphicsView): """ Defines the main widget for creating node graph views. """ __designer_icon__ = projexui.resources.find('img/ui/node.png') maxZoomAmountChanged = Signal(int) minZoomAmountChanged = Signal(int) zoomAmountChanged = Signal(int) def __init__(self, parent, sceneClass=None): # initialize the super class super(XNodeWidget, self).__init__(parent) # set the scene if not sceneClass: sceneClass = XNodeScene self._cleanupOnClose = True self._initialized = False self.setScene(sceneClass(self)) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) self.setContextMenuPolicy(Qt.CustomContextMenu) def __dir__(self): out = set(self.__dict__.keys()) out.update(dir(self.scene())) return list(out) def __getattr__(self, key): return getattr(self.scene(), key) def _runLayoutTest(self, layoutName): """ Runs a layout test for this widget for the inputed layout plugin name. :param layoutName | <str> :return <bool> | success """ layout = XNodeLayout.plugin(layoutName) if not layout: return False layout.runTest(self.scene()) return True @Slot() def autoLayout(self): """ Auto-lays out the whole scene. """ self.scene().autoLayout() @Slot() def autoLayoutSelected(self): """ Auto-lays out the selected items. """ self.scene().autoLayoutSelected() def centerOn(self, *args): """ Updates the center on method to ensure the viewport is updated. :param *args | <variant> """ super(XNodeWidget, self).centerOn(*args) for con in self.connections(): con.setPath(con.rebuild()) con.update() def centerOnAnimated(self, centerOn, animate=0): """ Animates the centering options over a given number of seconds. :param centerOn | <QRectF> | <QPointF> | <XNode> animate | <float> | seconds """ if isinstance(centerOn, XNode): center = centerOn.sceneRect().center() elif isinstance(centerOn, QRectF): center = centerOn.center() elif isinstance(centerOn, QPointF): center = centerOn else: return anim = XObjectAnimation(self, 'centerOn', self) anim.setStartValue(self.viewportRect().center()) anim.setEndValue(center) anim.setDuration(1000 * animate) anim.start() anim.finished.connect(anim.deleteLater) def centerOnItems(self, items=None): """ Centers on the given items, if no items are supplied, then all items will be centered on. :param items | [<QGraphicsItem>, ..] """ if not items: rect = self.scene().visibleItemsBoundingRect() if not rect.width(): rect = self.scene().sceneRect() self.centerOn(rect.center()) else: self.centerOn(self.scene().calculateBoundingRect(items).center()) def centerOnSelection(self): """ Centers on the selected items. :sa centerOnItems """ self.centerOnItems(self.scene().selectedItems()) def cleanupOnClose(self): """ Sets whether or not this widget should clean up its scene before closing. :return <bool> """ return self._cleanupOnClose def closeEvent(self, event): """ Cleans up the scene before closing. :param event | <QEvent> """ if (self.cleanupOnClose()): scene = self.scene() scene.cleanup() self.setScene(None) super(XNodeWidget, self).closeEvent(event) @Slot() def disableViewMode(self): """ Sets the node widget into selection mode which allows the user to select vs. pan and zoom. """ self.scene().setViewMode(False) @Slot() def enableViewMode(self): """ Sets the node widget into view mode which allows the user to pan and zoom vs. select. """ self.scene().setViewMode(True) def findNodeByRegex(self, nodeRegex): """ Returns the first node that matches the inputed regular expression. :param nodeRegex | <str> :return <XNode> || None """ return self.scene().findNodeByRegex(nodeRegex) def findNode(self, nodeName): """ Returns the node for the given node name. :param nodeName | <str> :return <XNode> || None """ return self.scene().findNode(nodeName) def isolationMode(self): """ Returns whether or not this widget is in isolation mode. :return <bool> """ return self.scene().isolationMode() def setCleanupOnClose(self, state): """ Sets whether or not the scene should be cleaned up before closing. :param state | <bool> """ self._cleanupOnClose = state @Slot(bool) def setIsolationMode(self, state): """ Sets whether or not the widget is in isolation mode. :param state | <bool> """ self.scene().setIsolationMode(state) @Slot(int) def setZoomAmount(self, amount): """ Sets the zoom amount for this widget to the inputed amount. :param amount | <int> """ self.scene().setZoomAmount(amount) def showEvent(self, event): super(XNodeWidget, self).showEvent(event) if not self._initialized: self._initialized = True self.centerOnItems() def viewportRect(self): """ Returns the QRectF that represents the visible viewport rect for the current view. :return <QRectF> """ w = self.width() h = self.height() vbar = self.verticalScrollBar() hbar = self.horizontalScrollBar() if vbar.isVisible(): w -= vbar.width() if hbar.isVisible(): h -= hbar.height() top_l = self.mapToScene(QPoint(0, 0)) bot_r = self.mapToScene(QPoint(w, h)) return QRectF(top_l.x(), top_l.y(), bot_r.x() - top_l.x(), bot_r.y() - top_l.y()) def zoomAmount(self): """ Returns the zoom amount for this widget to the inputed amount. :param amount | <int> """ return self.scene().zoomAmount() @Slot() def zoomExtents(self): """ Fits all the nodes in the view. """ rect = self.scene().visibleItemsBoundingRect() vrect = self.viewportRect() if rect.width(): changed = False scene_rect = self.scene().sceneRect() if scene_rect.width() < rect.width(): scene_rect.setWidth(rect.width() + 150) scene_rect.setX(-scene_rect.width() / 2.0) changed = True if scene_rect.height() < rect.height(): scene_rect.setHeight(rect.height() + 150) scene_rect.setY(-scene_rect.height() / 2.0) changed = True if changed: self.scene().setSceneRect(scene_rect) self.fitInView(rect, Qt.KeepAspectRatio) if not self.signalsBlocked(): self.zoomAmountChanged.emit(self.zoomAmount()) @Slot() def zoomIn(self): """ Zooms in for this widget by the scene's zoom step amount. """ self.scene().zoomIn() @Slot() def zoomOut(self): """ Zooms out for this widget by the scene's zoom step amount. """ self.scene().zoomOut() x_isolationMode = Property(bool, isolationMode, setIsolationMode) x_cleanupOnClose = Property(bool, cleanupOnClose, setCleanupOnClose)
class XPagesWidget(QWidget): """ """ currentPageChanged = Signal(int) pageSizeChanged = Signal(int) pageCountChanged = Signal(int) def __init__(self, parent=None): super(XPagesWidget, self).__init__(parent) # define custom properties self._currentPage = 1 self._pageCount = 10 self._itemCount = 0 self._pageSize = 50 self._itemsTitle = 'items' self._pagesSpinner = QSpinBox() self._pagesSpinner.setMinimum(1) self._pagesSpinner.setMaximum(10) self._pageSizeCombo = XComboBox(self) self._pageSizeCombo.setHint('all') self._pageSizeCombo.addItems(['', '25', '50', '75', '100']) self._pageSizeCombo.setCurrentIndex(2) self._nextButton = QToolButton(self) self._nextButton.setAutoRaise(True) self._nextButton.setArrowType(Qt.RightArrow) self._nextButton.setFixedWidth(16) self._prevButton = QToolButton(self) self._prevButton.setAutoRaise(True) self._prevButton.setArrowType(Qt.LeftArrow) self._prevButton.setFixedWidth(16) self._prevButton.setEnabled(False) self._pagesLabel = QLabel('of 10 for ', self) self._itemsLabel = QLabel(' items per page', self) # define the interface layout = QHBoxLayout() layout.addWidget(QLabel('Page', self)) layout.addWidget(self._prevButton) layout.addWidget(self._pagesSpinner) layout.addWidget(self._nextButton) layout.addWidget(self._pagesLabel) layout.addWidget(self._pageSizeCombo) layout.addWidget(self._itemsLabel) layout.addStretch(1) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) # create connections self._pageSizeCombo.currentIndexChanged.connect(self.pageSizePicked) self._nextButton.clicked.connect(self.gotoNext) self._prevButton.clicked.connect(self.gotoPrevious) self._pagesSpinner.editingFinished.connect(self.assignCurrentPage) def assignCurrentPage(self): """ Assigns the page for the spinner to be current. """ self.setCurrentPage(self._pagesSpinner.value()) def currentPage(self): """ Reutrns the current page for this widget. :return <int> """ return self._currentPage @Slot() def gotoFirst(self): """ Goes to the first page. :sa setCurrentPage """ self.setCurrentPage(1) @Slot() def gotoLast(self): """ Goes to the last page. :sa setCurrentPage """ self.setCurrentPage(self.pageCount()) @Slot() def gotoNext(self): """ Goes to the next page. :sa setCurrentPage """ next_page = self.currentPage() + 1 if (next_page > self.pageCount()): return self.setCurrentPage(next_page) @Slot() def gotoPrevious(self): """ Goes to the previous page. :sa setCurrentPage """ prev_page = self.currentPage() - 1 if (prev_page == 0): return self.setCurrentPage(prev_page) def itemCount(self): """ Returns the total number of items this widget holds. If no item count is defined, it will not be displayed in the label, otherwise it will show. :return <int> """ return self._itemCount def itemsTitle(self): """ Returns the items title for this instance. :return <str> """ return self._itemsTitle def pageCount(self): """ Returns the number of pages that this widget holds. :return <int> """ return self._pageCount def pageSize(self): """ Returns the number of items that should be visible in a page. :return <int> """ return self._pageSize def pageSizeOptions(self): """ Returns the list of options that will be displayed for this default size options. :return [<str>, ..] """ return map(str, self._pageSizeCombo.items()) def pageSizePicked(self, pageSize): """ Updates when the user picks a page size. :param pageSize | <str> """ try: pageSize = int(self._pageSizeCombo.currentText()) except ValueError: pageSize = 0 self.setPageSize(pageSize) self.pageSizeChanged.emit(pageSize) def refreshLabels(self): """ Refreshes the labels to display the proper title and count information. """ itemCount = self.itemCount() title = self.itemsTitle() if (not itemCount): self._itemsLabel.setText(' %s per page' % title) else: msg = ' %s per page, %i %s total' % (title, itemCount, title) self._itemsLabel.setText(msg) @Slot(int) def setCurrentPage(self, pageno): """ Sets the current page for this widget to the inputed page. :param pageno | <int> """ if (pageno == self._currentPage): return if (pageno <= 0): pageno = 1 self._currentPage = pageno self._prevButton.setEnabled(pageno > 1) self._nextButton.setEnabled(pageno < self.pageCount()) self._pagesSpinner.blockSignals(True) self._pagesSpinner.setValue(pageno) self._pagesSpinner.blockSignals(False) if (not self.signalsBlocked()): self.currentPageChanged.emit(pageno) @Slot(int) def setItemCount(self, itemCount): """ Sets the item count for this page to the inputed value. :param itemCount | <int> """ self._itemCount = itemCount self.refreshLabels() @Slot(str) def setItemsTitle(self, title): """ Sets the title that will be displayed when the items labels are rendered :param title | <str> """ self._itemsTitle = nativestring(title) self.refreshLabels() @Slot(int) def setPageCount(self, pageCount): """ Sets the number of pages that this widget holds. :param pageCount | <int> """ if (pageCount == self._pageCount): return pageCount = max(1, pageCount) self._pageCount = pageCount self._pagesSpinner.setMaximum(pageCount) self._pagesLabel.setText('of %i for ' % pageCount) if (pageCount and self.currentPage() <= 0): self.setCurrentPage(1) elif (pageCount < self.currentPage()): self.setCurrentPage(pageCount) if (not self.signalsBlocked()): self.pageCountChanged.emit(pageCount) self._prevButton.setEnabled(self.currentPage() > 1) self._nextButton.setEnabled(self.currentPage() < pageCount) @Slot(int) def setPageSize(self, pageSize): """ Sets the number of items that should be visible in a page. Setting the value to 0 will use all sizes :return <int> """ if self._pageSize == pageSize: return self._pageSize = pageSize # update the display size ssize = nativestring(pageSize) if (ssize == '0'): ssize = '' self._pageSizeCombo.blockSignals(True) index = self._pageSizeCombo.findText(ssize) self._pageSizeCombo.setCurrentIndex(index) self._pageSizeCombo.blockSignals(False) def setPageSizeOptions(self, options): """ Sets the options that will be displayed for this default size. :param options | [<str>,. ..] """ self._pageSizeCombo.blockSignals(True) self._pageSizeCombo.addItems(options) ssize = nativestring(self.pageSize()) if (ssize == '0'): ssize = '' index = self._pageSizeCombo.findText() self._pageSizeCombo.setCurrentIndex(index) self._pageSizeCombo.blockSignals(False) x_itemsTitle = Property(str, itemsTitle, setItemsTitle) x_pageCount = Property(int, pageCount, setPageCount) x_pageSize = Property(int, pageSize, setPageSize) x_pageSizeOptions = Property(list, pageSizeOptions, setPageSizeOptions)
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 XOrbQueryWidget(XStackedWidget): """ """ __designer_group__ = 'ProjexUI - ORB' def __init__(self, parent=None): super(XOrbQueryWidget, self).__init__(parent) # define custom properties self._pluginFactory = XOrbQueryPluginFactory() self._loadQuery = None self._tableType = None self._compoundStack = [] self._initialized = False self._showReferencePlugins = True self.clear() self.setMinimumWidth(575) self.setMinimumHeight(145) # create connections self.animationFinished.connect(self.cleanupContainers) def addContainer(self, query): """ Creates a new query container widget object and slides it into the frame. :return <XOrbQueryContainer> """ self.setUpdatesEnabled(False) self.blockSignals(True) container = XOrbQueryContainer(self) # setup properties container.setShowBack(self.count() > 0) # create connections container.enterCompoundRequested.connect(self.enterContainer) container.exitCompoundRequested.connect(self.exitContainer) # show the widget self.addWidget(container) self.setUpdatesEnabled(True) self.blockSignals(False) container.setQuery(query) self.slideInNext() return container def clear(self): """ Clears all the container for this query widget. """ for i in range(self.count()): widget = self.widget(i) if widget is not None: widget.close() widget.setParent(None) widget.deleteLater() def cleanupContainers(self): """ Cleans up all containers to the right of the current one. """ for i in range(self.count() - 1, self.currentIndex(), -1): widget = self.widget(i) widget.close() widget.setParent(None) widget.deleteLater() def containerFor(self, entry): """ Returns a container for the inputed entry widget. :param entry | <XOrbQueryEntryWidget> :return <XOrbQueryContainer> || None """ try: index = self._compoundStack.index(entry) except ValueError: return None return self.widget(index + 1) def currentContainer(self): """ Returns the current query container. :return <XOrbQueryContainer> """ return self.currentWidget() def currentQuery(self): """ Returns the current query for the active container. This will reflect what is currently visible to the user. :return <orb.Query> """ container = self.currentContainer() if container: return container.query() return Query() def enterContainer(self, entry, query): """ Enters a new container for the given entry widget. :param entry | <XOrbQueryEntryWidget> || None """ self._compoundStack.append(entry) self.addContainer(query) def exitContainer(self): """ Removes the current query container. """ try: entry = self._compoundStack.pop() except IndexError: return container = self.currentContainer() entry.setQuery(container.query()) self.slideInPrev() def query(self): """ Returns the full query for this widget. This will reflect the complete combined query for all containers within this widget. :return <orb.Query> """ if self._loadQuery is not None: return self._loadQuery container = self.widget(0) if container: query = container.query() else: query = Query() return query def pluginFactory(self): """ Returns the plugin factory that will be used to generate plugins for the query selector. You can subclass the XOrbQueryPlugin and XOrbQueryPluginFactory to create custom plugins for schemas and widgets. :return <XOrbQueryPluginFactory> """ return self._pluginFactory def reset(self): """ Resets this query builder to a blank query. """ self.setQuery(Query()) def setCurrentQuery(self, query): """ Sets the query for the current container widget. This will only change the active container, not parent containers. You should use the setQuery method to completely assign a query to this widget. :param query | <orb.Query> """ container = self.currentContainer() if container: container.setQuery(query) def setQuery(self, query): """ Sets the query for this widget to the inputed query. This will clear completely the current inforamation and reest all containers to the inputed query information. :param query | <orb.Query> """ if not self.isVisible(): self._loadQuery = query else: self._loadQuery = None if self._initialized and hash(query) == hash(self.query()): return self._initialized = True self.clear() self.addContainer(query) def setPluginFactory(self, factory): """ Assigns the plugin factory for this query widget to the inputed factory. You can use this to create custom handlers for columns when your schema is being edited. :param factory | <XOrbQueryPluginFactory> """ self._pluginFactory = factory def setShowReferencePlugins(self, state=True): """ Sets whether or not reference based plugins will be displayed to the user. :param state | <bool> """ self._showReferencePlugins = state def showReferencePlugins(self): """ Returns whether or not reference based plugins will be displayed to the user. :return <bool> """ return self._showReferencePlugins def showEvent(self, event): if self._loadQuery is not None: self.setQuery(self._loadQuery) elif not self._initialized: self.setQuery(Query()) def setTableType(self, tableType): """ Sets the table type for this instance to the given type. :param tableType | <orb.Table> """ if tableType == self._tableType: return self._initialized = False self._tableType = tableType self.setQuery(Query()) def tableType(self): """ Returns the table type instance for this widget. :return <subclass of orb.Table> """ return self._tableType x_showReferencePlugins = Property(bool, showReferencePlugins, setShowReferencePlugins)