class XColorButton(QPushButton): """ The XColorButton class is a simple extension to the standard QPushButton that will control color settings. When teh user clicks on the button, the QColorDialog will be displayed, prompting the user to select a new color. Colors are stored internally can can be accessed by etter and setter methods, as well as the colorChanged signal. As the color is modified, either through code or by a user, the background color for the button will automatically update to match. == Example == |>>> from projexui.widgets.xcolorbutton import XColorButton |>>> import projexui | |>>> # create the widget |>>> btn = projexui.testWidget(XColorButton) | |>>> # click around, change the color |>>> from PyQt4.QtGui import QColor |>>> print btn.color().red(), btn.color().green(), btn.color().blue() |255 170 0 |>>> btn.setColor(QColor('red')) | |>>> # create connections |>>> def printColor(clr): print clr.red(), clr.green(), clr.blue() |>>> btn.colorChanged.connect(printColor) | |>>> # prompt the user to select a color for that button |>>> btn.pickColor() """ colorChanged = qt.Signal(QColor) def __init__(self, parent): super(XColorButton, self).__init__(parent) # initialize the color color = QColor('black') self._color = color palette = self.palette() palette.setColor(palette.Button, color) self.setPalette(palette) # create connections self.clicked.connect(self.pickColor) def color(self): """ Returns the color value for this button. :return <QColor> """ return self._color def pickColor(self): """ Prompts the user to select a color for this button. """ color = QColorDialog.getColor(self.color(), self) if (color.isValid()): self.setColor(color) def setColor(self, color): """ Sets the color value for this button to the given color. :param color | <QColor> """ self._color = color palette = self.palette() palette.setColor(palette.Button, color) self.setPalette(palette) if (not self.signalsBlocked()): self.colorChanged.emit(color) x_color = qt.Property(QColor, color, setColor)
class XUrlWidget(QWidget): urlChanged = qt.Signal(str) urlEdited = qt.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() @qt.Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._urlEdit.setHint(hint) @qt.Slot(str) def setUrl(self, url): """ Sets the url for this widget to the inputed url. :param url | <str> """ self._urlEdit.setText(str(url)) def url(self): """ Returns the current url from the edit. :return <str> """ return str(self._urlEdit.text()) x_hint = qt.Property(str, hint, setHint) x_url = qt.Property(str, url, setUrl)
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 = qt.Property(bool, isChecked, setChecked) x_falseText = qt.Property(str, falseText, setFalseText) x_trueText = qt.Property(str, trueText, setTrueText)
class XTabWidget(QTabWidget): addRequested = qt.Signal(QPoint) optionsRequested = qt.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 = QPushButton(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 = QPushButton(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(): if self.currentIndex() == self.count() - 1: self._addButton.move(tabbar.width() - 2, -1) self._addButton.setFixedHeight(tabbar.height() + 2) else: self._addButton.move(tabbar.width() - 4, 1) self._addButton.setFixedHeight(tabbar.height()) else: self._addButton.move(tabbar.width() + 2, 1) self._addButton.stackUnder(self.currentWidget()) def addButton(self): """ Returns the add button linked with this tab widget. :return <QPushButton> """ 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: point = QCursor.pos() 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: point = QCursor.pos() self.optionsRequested.emit(point) def optionsButton(self): """ Returns the options button linked with this tab widget. :return <QPushButton> """ 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 = qt.Property(bool, showAddButton, setShowAddButton) x_showOptionsButton = qt.Property(bool, showOptionsButton, setShowOptionsButton)
class XLocationWidget(QWidget): locationChanged = qt.Signal(str) locationEdited = qt.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 str(self._locationEdit.text()) @qt.Slot(str) def setHint(self, hint): """ Sets the hint associated with this widget. :param hint | <str> """ self._locationEdit.setHint(hint) @qt.Slot(str) def setLocation(self, location): """ Sets the location for this widget to the inputed location. :param location | <str> """ self._locationEdit.setText(str(location)) def setUrlQueryKey(self, key): """ Sets the key for the URL to the inputed key. :param key | <str> """ self._urlQueryKey = str(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 = str(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 = qt.Property(str, hint, setHint) x_location = qt.Property(str, location, setLocation) x_urlQueryKey = qt.Property(str, urlQueryKey, setUrlQueryKey) x_urlTemplate = qt.Property(str, urlTemplate, setUrlTemplate)
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__ = projexui.resources.find('img/ui/combobox.png') checkedIndexesChanged = qt.Signal(list) checkedItemsChanged = qt.Signal(list) def __init__(self, parent=None): super(XComboBox, self).__init__(parent) # define custom properties self._checkable = False self._hint = '' self._separator = ',' # setup the checkable popup widget self._checkablePopup = None # set default properties self.setLineEdit(XLineEdit(self)) 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() return super(XComboBox, self).currentText() 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 [str(self.itemText(i)) for i in self.checkedIndexes()] 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> """ return self._checkable 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 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 setCheckedIndexes(self, indexes): """ Sets a list of checked indexes for this combobox. :param indexes | [<int>, ..] """ if (not self.isCheckable()): return model = self.model() 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) 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 (str(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) edit.installEventFilter(self) 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) @qt.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 = str(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 self.lineEdit().setText(self.separator().join(self.checkedItems())) 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 = qt.Property(str, hint, setHint) x_checkable = qt.Property(bool, isCheckable, setCheckable) x_separator = qt.Property(str, separator, setSeparator)
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') textEntered = qt.Signal(str) InputFormat = enum('Normal', 'CamelHump', 'Underscore', 'Dash', 'ClassName', 'NoSpaces', 'Capitalize', 'Uppercase', 'Lowercase', 'Pretty') 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._spacer = '_' self._hintColor = hint_clr self._cornerRadius = 0 self._inputFormat = XLineEdit.InputFormat.Normal self._selectAllOnFocus = False self._focusedIn = False self._useHintValue = False 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.setAutoRaise(True) button.setParent(self) 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.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) 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 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 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 = str(self.text()) if (text): 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 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.toUtf8(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() return text def hint(self): """ Returns the hint value for this line edit. :return <str> """ return self._hint 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 painter = QPainter(self) 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 = 5 + left 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 -= sum([btn.width() for btn in self.buttons()]) 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() @qt.Slot(str) def setHint(self, hint): """ Sets the hint text to the inputed value. :param hint | <str> """ self._hint = self.formatText(hint) self.repaint() def setHintColor(self, clr): """ Sets the color for the hint for this edit. :param clr | <QColor> """ self._hintColor = clr 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[str(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(self.formatText(text)) 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 = qt.Property(str, hint, setHint) x_icon = qt.Property('QIcon', icon, setIcon) x_iconSize = qt.Property(QSize, iconSize, setIconSize) x_hintColor = qt.Property('QColor', hintColor, setHintColor) x_cornerRadius = qt.Property(int, cornerRadius, setCornerRadius) x_inputFormatText = qt.Property(str, inputFormatText, setInputFormatText) x_spacer = qt.Property(str, spacer, setSpacer) x_selectAllOnFocus = qt.Property(bool, selectAllOnFocus, setSelectAllOnFocus) x_useHintValue = qt.Property(bool, useHintValue, setUseHintValue) # hack for qt setX_icon = setIcon
class XFilepathEdit(QWidget): """ The XFilepathEdit class provides a common interface to prompt the user to select a filepath from the filesystem. It can be configured to load directories, point to a save file path location, or to an open file path location. It can also be setup to color changed based on the validity of the existance of the filepath. == Example == |>>> from projexui.widgets.xfilepathedit import XFilepathEdit |>>> import projexui | |>>> # create the edit |>>> edit = projexui.testWidget(XFilepathEdit) | |>>> # set the filepath |>>> edit.setFilepath('/path/to/file') | |>>> # prompt the user to select the filepath |>>> edit.pickFilepath() | |>>> # enable the coloring validation |>>> edit.setValidated(True) """ __designer_icon__ = projexui.resources.find('img/file.png') Mode = enum('OpenFile', 'SaveFile', 'Path', 'OpenFiles') filepathChanged = qt.Signal(str) def __init__(self, parent=None): super(XFilepathEdit, self).__init__(parent) # define custom properties self._validated = False self._validForeground = QColor(0, 120, 0) self._validBackground = QColor(0, 120, 0, 100) self._invalidForeground = QColor(255, 0, 0) self._invalidBackground = QColor(255, 0, 0, 100) self._normalizePath = False self._filepathMode = XFilepathEdit.Mode.OpenFile self._filepathEdit = XLineEdit(self) self._filepathButton = QToolButton(self) self._filepathTypes = 'All Files (*.*)' # set default properties ico = projexui.resources.find('img/folder.png') self._filepathEdit.setReadOnly(False) self._filepathButton.setText('...') self._filepathButton.setFixedSize(25, 23) self._filepathButton.setAutoRaise(True) self._filepathButton.setIcon(QIcon(ico)) self._filepathEdit.setContextMenuPolicy(Qt.CustomContextMenu) self.setWindowTitle('Load File') self.setAcceptDrops(True) # define the layout layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._filepathEdit) layout.addWidget(self._filepathButton) self.setLayout(layout) # create connections self._filepathEdit.installEventFilter(self) self._filepathButton.clicked.connect(self.pickFilepath) self._filepathEdit.textChanged.connect(self.emitFilepathChanged) self._filepathEdit.textChanged.connect(self.validateFilepath) self._filepathEdit.customContextMenuRequested.connect(self.showMenu) def autoRaise(self): """ Returns whether or not the tool button will auto raise. :return <bool> """ return self._filepathButton.autoRaise() @qt.Slot() def clearFilepath(self): """ Clears the filepath contents for this path. """ self.setFilepath('') @qt.Slot() def copyFilepath(self): """ Copies the current filepath contents to the current clipboard. """ clipboard = QApplication.instance().clipboard() clipboard.setText(self.filepath()) clipboard.setText(self.filepath(), clipboard.Selection) def dragEnterEvent(self, event): """ Processes drag enter events. :param event | <QDragEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dragMoveEvent(self, event): """ Processes drag move events. :param event | <QDragEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dropEvent(self, event): """ Processes drop event. :param event | <QDropEvent> """ if (event.mimeData().hasUrls()): url = event.mimeData().urls()[0] filepath = url.toLocalFile() if (filepath): self.setFilepath(filepath) def emitFilepathChanged(self): """ Emits the filepathChanged signal for this widget if the signals are \ not being blocked. """ if (not self.signalsBlocked()): self.filepathChanged.emit(self.filepath()) def eventFilter(self, object, event): """ Overloads the eventFilter to look for click events on the line edit. :param object | <QObject> event | <QEvent> """ if ( object == self._filepathEdit and \ self._filepathEdit.isReadOnly() and \ event.type() == event.MouseButtonPress and \ event.button() == Qt.LeftButton ): self.pickFilepath() return False def filepath(self, validated=False): """ Returns the filepath for this widget. If the validated flag is set \ then this method will only return if the file or folder actually \ exists for this path. In the case of a SaveFile, only the base folder \ needs to exist on the system, in other modes the actual filepath must \ exist. If not validated, the text will return whatever is currently \ entered. :return <str> """ paths = self.filepaths() if not paths: return '' if not validated or self.isValid(): return paths[0] return '' def filepaths(self): """ Returns a list of the filepaths for this edit. :return [<str>, ..] """ return str(self._filepathEdit.text()).split(os.path.pathsep) def filepathMode(self): """ Returns the filepath mode for this widget. :return <XFilepathEdit.Mode> """ return self._filepathMode def filepathModeText(self): """ Returns the text representation for this filepath mode. :return <str> """ return XFilepathEdit.Mode[self._filepathMode] def filepathTypes(self): """ Returns the filepath types that will be used for this widget. :return <str> """ return self._filepathTypes def hint(self): """ Returns the hint for this filepath. :return <str> """ return self._filepathEdit.hint() def icon(self): """ Returns the icon that is used for this filepath widget. :return <QIcon> """ return self._filepathButton.icon() def invalidBackground(self): """ Returns the invalid background color for this widget. :return <QColor> """ return self._invalidBackground def invalidForeground(self): """ Returns the invalid foreground color for this widget. :return <QColor> """ return self._invalidForeground def isValid(self): """ Returns whether or not the filepath exists on the system. \ In the case of a SaveFile, only the base folder \ needs to exist on the system, in other modes the actual filepath must \ exist. :return <bool> """ check = str(self._filepathEdit.text()).split(os.path.pathsep)[0] if (self.filepathMode() == XFilepathEdit.Mode.SaveFile): check = os.path.dirname(check) return os.path.exists(check) def isValidated(self): """ Set whether or not to validate the filepath as the user is working \ with it. :return <bool> """ return self._validated def isReadOnly(self): """ Returns if the widget is read only for text editing or not. :return <bool> """ return self._filepathEdit.isReadOnly() def normalizePath(self): """ Returns whether or not the path should be normalized for the current operating system. When off, it will be defaulted to forward slashes (/). :return <bool> """ return self._normalizePath def pickFilepath(self): """ Prompts the user to select a filepath from the system based on the \ current filepath mode. """ mode = self.filepathMode() filepath = '' filepaths = [] curr_dir = str(self._filepathEdit.text()) if (not curr_dir): curr_dir = QDir.currentPath() if mode == XFilepathEdit.Mode.SaveFile: filepath = QFileDialog.getSaveFileName(self, self.windowTitle(), curr_dir, self.filepathTypes()) elif mode == XFilepathEdit.Mode.OpenFile: filepath = QFileDialog.getOpenFileName(self, self.windowTitle(), curr_dir, self.filepathTypes()) elif mode == XFilepathEdit.Mode.OpenFiles: filepaths = QFileDialog.getOpenFileNames(self, self.windowTitle(), curr_dir, self.filepathTypes()) else: filepath = QFileDialog.getExistingDirectory( self, self.windowTitle(), curr_dir) if filepath: if type(filepath) == tuple: filepath = filepath[0] self.setFilepath(str(filepath)) elif filepaths: self.setFilepaths(map(str, filepaths)) def setAutoRaise(self, state): """ Sets whether or not the tool button will auto raise. :param state | <bool> """ self._filepathButton.setAutoRaise(state) @qt.Slot(int) def setFilepathMode(self, mode): """ Sets the filepath mode for this widget to the inputed mode. :param mode | <XFilepathEdit.Mode> """ self._filepathMode = mode @qt.Slot(str) def setFilepathModeText(self, text): """ Sets the filepath mode for this widget based on the inputed text. :param text | <str> :return <bool> | success """ try: self.setFilepathMode(XFilepathEdit.Mode[str(text)]) return True except KeyError: return False @qt.Slot(str) def setFilepathTypes(self, filepathTypes): """ Sets the filepath type string that will be used when looking up \ filepaths. :param filepathTypes | <str> """ self._filepathTypes = filepathTypes @qt.Slot(str) def setFilepath(self, filepath): """ Sets the filepath text for this widget to the inputed path. :param filepath | <str> """ if self.normalizePath(): filepath = os.path.normpath(str(filepath)) else: filepath = os.path.normpath(str(filepath)).replace('\\', '/') self._filepathEdit.setText(filepath) def setFilepaths(self, filepaths): """ Sets the list of the filepaths for this widget to the inputed paths. :param filepaths | [<str>, ..] """ self.setFilepath(os.path.pathsep.join(filepaths)) def setHint(self, hint): """ Sets the hint for this filepath. :param hint | <str> """ if self.normalizePath(): filepath = os.path.normpath(str(hint)) else: filepath = os.path.normpath(str(hint)).replace('\\', '/') self._filepathEdit.setHint(hint) def setIcon(self, icon): """ Sets the icon that will be used for this widget's tool button. :param icon | <QIcon> || <str> """ self._filepathButton.setIcon(QIcon(icon)) def setInvalidBackground(self, bg): """ Sets the invalid background color for this widget to the inputed widget. :param bg | <QColor> """ self._invalidBackground = QColor(bg) def setInvalidForeground(self, fg): """ Sets the invalid foreground color for this widget to the inputed widget. :param fg | <QColor> """ self._invalidForeground = QColor(fg) def setNormalizePath(self, state): """ Sets whether or not the path should be normalized for the current operating system. When off, it will be defaulted to forward slashes (/). :param state | <bool> """ self._normalizePath = state @qt.Slot(bool) def setReadOnly(self, state): """ Sets whether or not this filepath widget is readonly in the text edit. :param state | <bool> """ self._filepathEdit.setReadOnly(state) @qt.Slot(bool) def setValidated(self, state): """ Set whether or not to validate the path as the user edits it. :param state | <bool> """ self._validated = state palette = self.palette() # reset the palette to default, revalidate self._filepathEdit.setPalette(palette) self.validate() def setValidBackground(self, bg): """ Sets the valid background color for this widget to the inputed color. :param bg | <QColor> """ self._validBackground = QColor(bg) def setValidForeground(self, fg): """ Sets the valid foreground color for this widget to the inputed color. :param fg | <QColor> """ self._validForeground = QColor(fg) def showMenu(self, pos): """ Popups a menu for this widget. """ menu = QMenu(self) menu.setAttribute(Qt.WA_DeleteOnClose) menu.addAction('Clear').triggered.connect(self.clearFilepath) menu.addSeparator() menu.addAction('Copy Filepath').triggered.connect(self.copyFilepath) menu.exec_(self.mapToGlobal(pos)) def validBackground(self): """ Returns the valid background color for this widget. :return <QColor> """ return self._validBackground def validForeground(self): """ Returns the valid foreground color for this widget. :return <QColor> """ return self._validForeground def validateFilepath(self): """ Alters the color scheme based on the validation settings. """ if (not self.isValidated()): return valid = self.isValid() if (not valid): fg = self.invalidForeground() bg = self.invalidBackground() else: fg = self.validForeground() bg = self.validBackground() palette = self.palette() palette.setColor(palette.Base, bg) palette.setColor(palette.Text, fg) self._filepathEdit.setPalette(palette) # map Qt properties x_autoRaise = qt.Property(bool, autoRaise, setAutoRaise) x_filepathTypes = qt.Property(str, filepathTypes, setFilepathTypes) x_filepath = qt.Property(str, filepath, setFilepath) x_readOnly = qt.Property(bool, isReadOnly, setReadOnly) x_validated = qt.Property(bool, isValidated, setValidated) x_hint = qt.Property(str, hint, setHint) x_icon = qt.Property('QIcon', icon, setIcon) x_normalizePath = qt.Property(bool, normalizePath, setNormalizePath) x_invalidForeground = qt.Property('QColor', invalidForeground, setInvalidForeground) x_invalidBackground = qt.Property('QColor', invalidBackground, setInvalidBackground) x_validForeground = qt.Property('QColor', validForeground, setValidForeground) x_validBackground = qt.Property('QColor', validBackground, setValidBackground) x_filepathModeText = qt.Property(str, filepathModeText, setFilepathModeText)
class XIconButton(QPushButton): """ """ __designer_icon__ = projexui.resources.find('img/ui/icon.png') filepathChanged = qt.Signal(str) def __init__(self, parent=None): super(XIconButton, self).__init__(parent) # define custom properties self._filepath = '' self._fileTypes = 'PNG Files (*.png);;All Files (*.*)' # set default properties self.setFixedWidth(64) self.setFixedHeight(64) self.setAcceptDrops(True) # create connections self.clicked.connect(self.pickFilepath) def filepath(self): """ Returns the filepath for this button. :return <str> """ return self._filepath def fileTypes(self): """ Returns the file types that will be used to filter this button. :return <str> """ return self._fileTypes def dragEnterEvent(self, event): """ Handles a drag enter event. :param event | <QEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dragMoveEvent(self, event): """ Handles a drag move event. :param event | <QEvent> """ if (event.mimeData().hasUrls()): event.acceptProposedAction() def dropEvent(self, event): """ Handles a drop event. """ url = event.mimeData().urls()[0] url_path = str(url.toString()) # download an icon from the web if (not url_path.startswith('file:')): filename = os.path.basename(url_path) temp_path = os.path.join(str(QDir.tempPath()), filename) try: urllib.urlretrieve(url_path, temp_path) except IOError: return self.setFilepath(temp_path) else: self.setFilepath(url_path.replace('file://', '')) def pickFilepath(self): """ Picks the image file to use for this icon path. """ filepath = QFileDialog.getOpenFileName(self, 'Select Image File', QDir.currentPath(), self.fileTypes()) if type(filepath) == tuple: filepath = str(filepath[0]) if (filepath): self.setFilepath(filepath) def setFilepath(self, filepath): """ Sets the filepath for this button to the inputed path. :param filepath | <str> """ self._filepath = str(filepath) self.setIcon(QIcon(filepath)) if (not self.signalsBlocked()): self.filepathChanged.emit(filepath) def setFileTypes(self, fileTypes): """ Sets the filetypes for this button to the inputed types. :param fileTypes | <str> """ self._fileTypes = fileTypes @staticmethod def buildIcon(icon): """ Builds an icon from the inputed information. :param icon | <variant> """ if icon is None: return QIcon() if type(icon) == buffer: try: icon = QIcon(projexui.generatePixmap(icon)) except: icon = QIcon() else: try: icon = QIcon(icon) except: icon = QIcon() return icon x_filepath = qt.Property(str, filepath, setFilepath) x_fileTypes = qt.Property(str, fileTypes, setFileTypes)
:return <str> """ return ElementTree.tostring(self.toXml()) def toXml(self): """ Saves this profile toolbar as XML information. :return <xml.etree.ElementTree.Element> """ xtoolbar = ElementTree.Element('toolbar') act = self._profileGroup.checkedAction() if (act): xtoolbar.set('current', act.profile().name()) for profile in self.profiles(): profile.toXml(xtoolbar) return xtoolbar def viewWidget(self): """ Returns the view widget linked with this toolbar. :return <projexui.widgets.xviewwidget.XViewWidget> """ return self._viewWidget x_editingEnabled = qt.Property(bool, isEditingEnabled, setEditingEnabled)
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 = str(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 = qt.Property(bool, showRichText, setShowRichText)
class XNodeWidget(QGraphicsView): """ Defines the main widget for creating node graph views. """ __designer_icon__ = projexui.resources.find('img/ui/node.png') zoomAmountChanged = qt.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.setScene(sceneClass(self)) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) self.setContextMenuPolicy(Qt.CustomContextMenu) self.setBaseSize(QSize(300, 250)) 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 @qt.Slot() def autoLayout(self): """ Auto-lays out the whole scene. """ self.scene().autoLayout() @qt.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) @qt.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) @qt.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 @qt.Slot(bool) def setIsolationMode(self, state): """ Sets whether or not the widget is in isolation mode. :param state | <bool> """ self.scene().setIsolationMode(state) @qt.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 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() @qt.Slot() def zoomExtents(self): """ Fits all the nodes in the view. """ rect = self.scene().visibleItemsBoundingRect() vrect = self.viewportRect() if rect.width(): if rect.width() < vrect.width() and rect.height() < vrect.height(): self.centerOn(rect.center()) else: self.fitInView(rect, Qt.KeepAspectRatio) if not self.signalsBlocked(): self.zoomAmountChanged.emit(self.zoomAmount()) @qt.Slot() def zoomIn(self): """ Zooms in for this widget by the scene's zoom step amount. """ self.scene().zoomIn() @qt.Slot() def zoomOut(self): """ Zooms out for this widget by the scene's zoom step amount. """ self.scene().zoomOut() x_isolationMode = qt.Property(bool, isolationMode, setIsolationMode) x_cleanupOnClose = qt.Property(bool, cleanupOnClose, setCleanupOnClose)
class XChart(QFrame): """ """ middleClicked = qt.Signal(QPoint) def __init__(self, parent=None): super(XChart, self).__init__(parent) # load the interface projexui.loadUi(__file__, self) # define custom properties self._renderer = None self._chartTitle = '' self._axes = [] self._datasets = [] self._horizontalAxis = None self._verticalAxis = None self._showDatasetToolbar = True self._showTypeButton = True self._dataChanged = False self._showGrid = True self._showRows = True self._showColumns = True self._showXAxis = True self._showYAxis = True # set default properties self.uiChartVIEW.setScene(XChartScene(self)) self.uiXAxisVIEW.setScene(XChartScene(self)) self.uiYAxisVIEW.setScene(XChartScene(self)) self.uiChartVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiXAxisVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiYAxisVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiDatasetTBAR.setFixedHeight(32) self.uiXAxisVIEW.hide() self.uiYAxisVIEW.hide() self.setAutoFillBackground(True) self.setBackgroundRole(QPalette.Base) self.setStyleSheet(STYLE) self.uiChartVIEW.setMouseTracking(True) self.uiDatasetTBAR.setStyleSheet(TOOLBAR_STYLE) # load renderers for renderer in XChartRenderer.plugins(): act = QAction('%s Chart' % renderer, self) ico = 'img/chart/%s.png' % renderer.lower() act.setIcon(QIcon(resources.find(ico))) self.uiTypeBTN.addAction(act) # assign the default renderer if not self.uiTypeBTN.defaultAction(): self._renderer = XChartRenderer.plugin(renderer) self.uiTypeBTN.setDefaultAction(act) # create connections chart_hbar = self.uiChartVIEW.horizontalScrollBar() chart_vbar = self.uiChartVIEW.verticalScrollBar() x_bar = self.uiXAxisVIEW.horizontalScrollBar() y_bar = self.uiYAxisVIEW.verticalScrollBar() chart_hbar.valueChanged.connect(self.syncScrollbars) chart_hbar.rangeChanged.connect(self.syncScrollbars) y_bar.valueChanged.connect(self.syncScrollbars) y_bar.rangeChanged.connect(self.syncScrollbars) self.uiTypeBTN.triggered.connect(self.assignRenderer) def _addDatasetAction(self, dataset): """ Adds an action for the inputed dataset to the toolbar :param dataset | <XChartDataset> """ # create the toolbar action action = QAction(dataset.name(), self) action.setIcon(XColorIcon(dataset.color())) action.setCheckable(True) action.setChecked(True) action.setData(qt.wrapVariant(dataset)) action.toggled.connect(self.toggleDataset) self.uiDatasetTBAR.addAction(action) def _drawBackground(self, scene, painter, rect): """ Draws the backgroud for a particular scene within the charts. :param scene | <XChartScene> painter | <QPainter> rect | <QRectF> """ rect = scene.sceneRect() if scene == self.uiChartVIEW.scene(): self.renderer().drawGrid(painter, rect, self.showGrid(), self.showColumns(), self.showRows()) elif scene == self.uiXAxisVIEW.scene(): self.renderer().drawAxis(painter, rect, self.horizontalAxis()) elif scene == self.uiYAxisVIEW.scene(): self.renderer().drawAxis(painter, rect, self.verticalAxis()) def addAxis(self, axis): """ Adds a new axis for this chart. Axis can define X & Y data including notch and value processing, as well as define individual lines within the chart that any chart items can reference or use. :param axis | <projexui.widgets.xchart.XChartAxis> """ self._axes.append(axis) def addDataset(self, dataset): """ Adds the given data set to this chart widget. :param dataSet | <XChartDataset> """ self._datasets.append(dataset) self._dataChanged = True self._addDatasetAction(dataset) def addToolbarWidget(self, widget): """ Adds a new widget to the toolbar layout for the chart. :param widget | <QWidget> """ self.uiToolbarHBOX.addWidget(widget) def assignRenderer(self, action): """ Assigns the renderer for this chart to the current selected renderer. """ name = str(action.text()).split(' ')[0] self._renderer = XChartRenderer.plugin(name) self.uiTypeBTN.setDefaultAction(action) self.recalculate() def axes(self): """ Returns all the axes that have been defined for this chart. :return [<projexui.widgets.xchart.XChartAxis>, ..] """ out = self._axes[:] if self._horizontalAxis: out.append(self._horizontalAxis) if self._verticalAxis: out.append(self._verticalAxis) return out def axis(self, name): """ Looks up an axis for this chart by the given name. :return <projexui.widgets.xchart.XChartAxis> || None """ for axis in self.axes(): if axis.name() == name: return axis return None def clear(self): """ Clears out all the dataset information from the chart. """ self.clearAxes() self.clearDatasets() def clearAxes(self): self._axes = [] self._verticalAxis = None self._horizontalAxis = None def clearDatasets(self): self._datasets = [] for act in self.uiDatasetTBAR.actions(): act.deleteLater() self.uiChartVIEW.scene().clear() def chartTitle(self): """ Returns the title for this plot. :return <str> """ return self._title def compareValues(self, a, b): """ Compares two values based on their values for this axis. :param a | <variant> b | <variant> """ values = self.values() try: return cmp(values.index(a), values.index(b)) except ValueError: return cmp(a, b) def datasets(self, visible=True): """ Returns a list of the data sets that are assigned with this chart widget. :param visible | <bool> :return [<XChartDataSet>, ..] """ if visible is not None: return filter(lambda x: x.isVisible(), self._datasets) return self._datasets[:] def showEvent(self, event): super(XChart, self).showEvent(event) self.recalculate() def findRenderer(self, name): """ Returns the renderer based on the inputed name. :return <str> """ return XChartRenderer.plugin(name) def horizontalAxis(self): """ Returns the axis that is assigned to the horizontal direction for this chart. :return <XChartAxis> """ return self._horizontalAxis def insertToolbarWidget(self, index, widget): """ Inserts a new widget to the toolbar layout for the chart. :param index | <int> widget | <QWidget> """ self.uiToolbarHBOX.insertWidget(index, widget) def pointAt(self, **axis_values): """ Returns the point on the chart where the inputed values are located. :return <QPointF> """ scene_point = self.renderer().pointAt(self.axes(), axis_values) chart_point = self.uiChartVIEW.mapFromScene(scene_point) return self.uiChartVIEW.mapToParent(chart_point) def mousePressEvent(self, event): if event.button() == Qt.MidButton: self.middleClicked.emit(event.pos()) super(XChart, self).mousePressEvent(event) def recalculate(self): """ Recalculates the information for this chart. """ if not (self.isVisible() and self.renderer()): return # update dynamic range if self._dataChanged: for axis in self.axes(): if axis.useDynamicRange(): axis.calculateRange(self.values(axis.name())) self._dataChanged = False # recalculate the main grid xaxis = self.horizontalAxis() yaxis = self.verticalAxis() renderer = self.renderer() xvisible = xaxis is not None and self.showXAxis( ) and renderer.showXAxis() yvisible = yaxis is not None and self.showYAxis( ) and renderer.showYAxis() self.uiXAxisVIEW.setVisible(xvisible) self.uiYAxisVIEW.setVisible(yvisible) # calculate the main view view = self.uiChartVIEW chart_scene = view.scene() chart_scene.setSceneRect(0, 0, view.width() - 2, view.height() - 2) rect = renderer.calculate(chart_scene, xaxis, yaxis) # recalculate the xaxis if xaxis and self.showXAxis() and renderer.showXAxis(): view = self.uiXAxisVIEW scene = view.scene() scene.setSceneRect(0, 0, rect.width(), view.height()) scene.invalidate() # render the yaxis if yaxis and self.showYAxis() and renderer.showYAxis(): view = self.uiYAxisVIEW scene = view.scene() scene.setSceneRect(0, 0, view.width(), rect.height()) scene.invalidate() # recalculate the items renderer.calculateDatasets(chart_scene, self.axes(), self.datasets()) chart_scene.invalidate() def removeAxis(self, axis): """ Removes an axis from this chart either by direct reference or by name. :param axis | <projexui.widgets.XChartAxis> || <str> """ if not isinstance(axis, XChartAxis): axis = self.axis(str(axis)) try: self._axes.remove(axis) except ValueError: pass def renderer(self): """ Returns the current renderer associated with this plot. :return <projexui.widgets.xchart.XChartRenderer> """ return self._renderer def renderers(self): """ Returns the renderer instances associated with this chart. :return [<XChartRenderer>, ..] """ return map(XChartRenderer.plugin, XChartRenderer.plugins()) def resizeEvent(self, event): """ Recalculates the chart information when the widget resizes. :param event | <QResizeEvent> """ super(XChart, self).resizeEvent(event) if self.isVisible(): self.recalculate() def restoreXml(self, xchart): """ Restores the xml information for this chart. :param xparent | <xml.etree.ElementTree.Element> """ if xchart is None: return self.setRenderer(xchart.get('renderer', 'Bar')) def saveXml(self, xchart): """ Saves the xml information for this chart to the inputed xml. :param xchart | <xml.etree.ElementTree.Element> """ if xchart is None: return xchart.set('renderer', self.renderer().name()) def setRenderer(self, renderer): """ Sets the current renderer associated with this plot. :param renderer | <XChartRenderer> || <str> :return <bool> | success """ if not isinstance(renderer, XChartRenderer): renderer = XChartRenderer.plugin(str(renderer)) if renderer is None: return False self._renderer = renderer for act in self.uiTypeBTN.actions(): if act.text() == '%s Chart' % renderer.name(): self.uiTypeBTN.setDefaultAction(act) break return True def setAxes(self, axes): """ Sets the axes for this chart to the inputed list of axes. :param axes | [<projexui.widgets.xchart.XChartAxis>, ..] """ self._axes = axes def setDatasets(self, datasets): """ Sets the dataset list for this chart to the inputed data. :param datasets | [<XChartDataset>, ..] """ self.clearDatasets() self._datasets = datasets for dataset in datasets: self._addDatasetAction(dataset) self._dataChanged = True self.recalculate() def setChartTitle(self, title): """ Sets the title for the plot to the inputed title. :param title | <str> """ self._chartTitle = title def setHorizontalAxis(self, axis): """ Sets the horizontal axis for this chart. :param axis | <XChartAxis> """ self._horizontalAxis = axis if axis: axis.setOrientation(Qt.Horizontal) self.uiXAxisVIEW.setFixedHeight(axis.minimumLabelHeight() + 5) self.uiXAxisVIEW.setVisible(axis is not None) def setVerticalAxis(self, axis): """ Sets the vertical axis for this chart. :param axis | <XChartAxis> """ self._verticalAxis = axis if axis: axis.setOrientation(Qt.Vertical) self.uiYAxisVIEW.setFixedWidth(axis.minimumLabelWidth() + 15) self.uiYAxisVIEW.setVisible(axis is not None) def setShowColumns(self, state): """ Sets the whether or not this renderer should draw the grid. :param state | <bool> """ self._showColumns = state def setShowDatasetToolbar(self, state): """ Sets whether or not the dataset toolbar is visible. :param state | <bool> """ self._showDatasetToolbar = state if not state: self.uiDatasetTBAR.hide() else: self.uiDatasetTBAR.show() def setShowGrid(self, state): """ Sets the whether or not this renderer should draw the grid. :param state | <bool> """ self._showGrid = state def setShowRows(self, state): """ Sets the whether or not this renderer should draw the grid. :param state | <bool> """ self._showRows = state def setShowTypeButton(self, state): """ Sets whether or not the type button is visible. :param state | <bool> """ self._showTypeButton = state if not state: self.uiTypeBTN.hide() else: self.uiTypeBTN.show() def setShowXAxis(self, state): """ Sets the whether or not this renderer should draw the x-axis. :param state | <bool> """ self._showXAxis = state def setShowYAxis(self, state): """ Sets the whether or not this renderer should draw the y-axis. :param state | <bool> """ self._showYAxis = state def showColumns(self): """ Returns whether or not this renderer should draw the grid. :return <bool> """ return self._showColumns and self.showXAxis() def showDatasetToolbar(self): """ Returns whether or not the dataset toolbar is visible. :return <bool> """ return self._showDatasetToolbar def showGrid(self): """ Returns whether or not this renderer should draw the grid. :return <bool> """ return self._showGrid def showRows(self): """ Returns whether or not this renderer should draw the grid. :return <bool> """ return self._showRows and self.showYAxis() def showTypeButton(self): """ Returns whether or not the type button is visible. :return <bool> """ return self._showTypeButton def showXAxis(self): """ Returns whether or not this renderer should draw the x-axis. :return <bool> """ return self._showXAxis def showYAxis(self): """ Returns whether or not this renderer should draw the y-axis. :return <bool> """ return self._showYAxis def sizeHint(self): """ Returns the size hint for this chart. """ return QSize(300, 300) def syncScrollbars(self): """ Synchronizes the various scrollbars within this chart. """ chart_hbar = self.uiChartVIEW.horizontalScrollBar() chart_vbar = self.uiChartVIEW.verticalScrollBar() x_hbar = self.uiXAxisVIEW.horizontalScrollBar() x_vbar = self.uiXAxisVIEW.verticalScrollBar() y_hbar = self.uiYAxisVIEW.horizontalScrollBar() y_vbar = self.uiYAxisVIEW.verticalScrollBar() x_hbar.setRange(chart_hbar.minimum(), chart_hbar.maximum()) x_hbar.setValue(chart_hbar.value()) x_vbar.setValue(0) chart_vbar.setRange(y_vbar.minimum(), y_vbar.maximum()) chart_vbar.setValue(y_vbar.value()) y_hbar.setValue(4) def toggleDataset(self, state, dataset=None): """ Toggles the dataset based on the given action or dataset. :param state | <bool> dataset | <XChartDataset> """ if dataset is None and self.sender(): dataset = qt.unwrapVariant(self.sender().data()) dataset.setVisible(state) self._dataChanged = True self.recalculate() def valueAt(self, point): """ Returns the value within the chart for the given point. :param point | <QPoint> :return {<str> axis name: <variant> value, ..} """ chart_point = self.uiChartVIEW.mapFromParent(point) scene_point = self.uiChartVIEW.mapToScene(chart_point) return self.renderer().valueAt(self.axes(), scene_point) def values(self, axis): """ Returns the values of the given axis from all the datasets within this chart. :param axis | <str> :return [<variant>, ..] """ output = [] for dataset in self.datasets(): output += dataset.values(axis) return output def verticalAxis(self): """ Returns the axis that is used for the vertical view for this graph. :return <XChartAxis> """ return self._verticalAxis x_showColumns = qt.Property(bool, showColumns, setShowColumns) x_showRows = qt.Property(bool, showRows, setShowRows) x_showGrid = qt.Property(bool, showGrid, setShowGrid) x_showYAxis = qt.Property(bool, showYAxis, setShowYAxis) x_showXAxis = qt.Property(bool, showXAxis, setShowXAxis) x_showDatasetToolbar = qt.Property(bool, showDatasetToolbar, setShowDatasetToolbar) x_showTypeButton = qt.Property(bool, showTypeButton, setShowTypeButton)