class XOrbQueryPlugin(object): Flags = enum('ReferenceRequired') def __init__(self): self._operatorMap = OrderedDict() def createEditor(self, parent, column, op, value): """ Creates a new editor for the parent based on the plugin parameters. :param parent | <QWidget> :return <QWidget> || None """ try: cls = self._operatorMap[nativestring(op)].cls except KeyError, AttributeError: return None # create the new editor if cls: widget = cls(parent) widget.setAttribute(Qt.WA_DeleteOnClose) projexui.setWidgetValue(widget, value) return widget return None
class XColorIcon(QIcon): Style = enum('Plain') def __init__(self, icon, style=Style.Plain): if type(icon) in (str, unicode): icon = QColor(icon) if isinstance(icon, QColor): icon = self.generatePixmap(icon, style) super(XColorIcon, self).__init__(icon) @staticmethod def generatePixmap(color, style=Style.Plain, size=None): """ Generates a new pixmap for the inputed color and style. If no size is provided, then the default 32x32 will be used. :return <QPixmap> """ if size is None: size = QSize(32, 32) pixmap = QPixmap(size) # create a plain pixmap if style == XColorIcon.Style.Plain: pixmap.fill(color) return pixmap
class QueryAggregate(object): Type = enum('Count', 'Maximum', 'Minimum', 'Sum') def __init__(self, typ, table, **options): self._type = typ self._table = table self._column = options.get('column', None) self._lookupOptions = orb.LookupOptions(**options) def columns(self): """ Returns the column associated with this aggregate. :return [<orb.Column>, ..] """ if self._column: if not isinstance(self._column, orb.Column): col = self._table.schema().column(self._column) else: col = self._column return col, else: return self._table.schema().primaryColumns() def lookupOptions(self): """ Returns the lookup options instance for this aggregate. :return <orb.LookupOptions> """ return self._lookupOptions def table(self): """ Returns the table associated with this aggregate. :return <orb.Table> """ return self._table def type(self): """ Returns the type for this aggregate. :return <str> """ return self._type
class TestAllColumns(orb.Table): Test = enum('Ok') # boolean bool = orb.BooleanColumn() # data binary = orb.BinaryColumn() json = orb.JSONColumn() query = orb.QueryColumn() yaml = orb.YAMLColumn() # datetime date = orb.DateColumn() datetime = orb.DatetimeColumn() datetime_tz = orb.DatetimeWithTimezoneColumn() interval = orb.IntervalColumn() time = orb.TimeColumn() timestamp = orb.TimestampColumn() utc_datetime = orb.UTC_DatetimeColumn() utc_timestamp = orb.UTC_TimestampColumn() # numeric id = orb.IdColumn() decimal = orb.DecimalColumn(scale=2) float = orb.FloatColumn() integer = orb.IntegerColumn() long = orb.LongColumn() enum = orb.EnumColumn() # reference reference = orb.ReferenceColumn(reference='TestReference') # string string = orb.StringColumn() text = orb.TextColumn() color = orb.ColorColumn() directory = orb.DirectoryColumn() email = orb.EmailColumn() filepath = orb.FilepathColumn() html = orb.HtmlColumn() password = orb.PasswordColumn(default='T3st1ng!') token = orb.TokenColumn() url = orb.UrlColumn() xml = orb.XmlColumn()
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 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 XExporter(object): Flags = enum('SupportsTree') _plugins = [] def __init__(self, name, filetype): self._name = name self._filetype = filetype self._flags = 0 @abstractmethod() def exportTree(self, tree, filename): """ Exports the tree information to the given filename. :param tree | <QTreeWidget> filename | <str> :return <bool> | success """ return False def filetype(self): """ Returns the filetype associated with this exporter. :return <str> """ return self._filetype def flags(self): """ Returns the flags associated with this plugin. :return <XExporter.Flags> """ return self._flags def name(self): """ Returns the name of this exporter. :return <str> """ return self._name def setFlag(self, flag, state=True): """ Sets whether or not the given flag is enabled or disabled. :param flag | <XExporter.Flags> """ has_flag = self.testFlag(flag) if has_flag and not state: self.setFlags(self.flags() ^ flag) elif not has_flag and state: self.setFlags(self.flags() | flag) def setFlags(self, flags): """ Sets the flags for this plugin. :param flags | <XExporter.Flags> """ self._flags = flags def testFlag(self, flag): """ Tests to see if the inputed flag is applied for this plugin. :param flag | <XExporter.Flags> """ return (self.flags() & flag) != 0 @staticmethod def init(): """ Initializes the exporter system. """ if XExporter._plugins: return from projexui.exporters.xexcelexporter import XExcelExporter @staticmethod def plugins(flags=None): """ Returns the plugins registered for the exporter. If the optional flags are set then only plugins with the inputed flags will be returned. :param flags | <XExporter.Flags> """ XExporter.init() plugs = XExporter._plugins[:] if flags is not None: return filter(lambda x: x.testFlag(flags), plugs) return plugs @staticmethod def register(plugin): """ Registers the inputed plugin to the system. :param <XExporter> """ XExporter._plugins.append(plugin)
class XChartScene(QGraphicsScene): Type = enum('Bar', 'Pie', 'Line') chartTypeChanged = Signal() def __init__(self, chartWidget): super(XChartScene, self).__init__(chartWidget) # create custom properties self._chartWidget = chartWidget self._minimumWidth = -1 self._minimumHeight = -1 self._maximumWidth = -1 self._maximumHeight = -1 self._horizontalPadding = 6 self._verticalPadding = 6 self._showGrid = True self._showRows = True self._showColumns = True self._trackingEnabled = True self._chartType = XChartScene.Type.Line self._trackerItem = None # used with pie charts self._pieAxis = Qt.YAxis self._pieAlignment = Qt.AlignCenter self._horizontalRuler = XChartRuler(XChartRuler.Type.Number) self._verticalRuler = XChartRuler(XChartRuler.Type.Number) self._font = QApplication.font() self._alternatingColumnColors = False self._alternatingRowColors = True self._dirty = False self._buildData = {} palette = QApplication.palette() self._axisColor = palette.color(palette.Mid).darker(125) self._baseColor = palette.color(palette.Base) self._alternateColor = palette.color(palette.Base).darker(104) self._borderColor = palette.color(palette.Mid) # create custom properties chartWidget.installEventFilter(self) self.chartTypeChanged.connect(self.update) def alternateColor(self): """ Returns the color to be used for the alternate background. :return <QColor> """ return self._alternateColor def alternatingColumnColors(self): """ Returns whether or not to display alternating column colors. :return <bool> """ return self._alternatingColumnColors def alternatingRowColors(self): """ Returns whehter or not to display alternating row colors. :return <bool> """ return self._alternatingRowColors def axisColor(self): """ Returns the axis color for this chart. :return <QColor> """ return self._axisColor def baseColor(self): """ Returns the color to be used for the primary background. :return <QColor> """ return self._baseColor def borderColor(self): """ Returns the color to be used for the chart borders. :return <QColor> """ return self._borderColor def chartWidget(self): """ Returns the chart widget this scene is linked to. :return <XChartWidget> """ return self._chartWidget def chartItems(self): """ Returns the chart items that are found within this scene. :return [<XChartWidgetItem>, ..] """ from projexui.widgets.xchartwidget import XChartWidgetItem return filter(lambda x: isinstance(x, XChartWidgetItem), self.items()) def chartType(self): """ Returns the chart type for this scene. :return <XChartScene.Type> """ return self._chartType def drawBackground(self, painter, rect): """ Draws the backgrounds for the different chart types. :param painter | <QPainter> rect | <QRect> """ if (self._dirty): self.rebuild() if (self.showGrid()): self.drawGrid(painter) def drawForeground(self, painter, rect): """ Draws the foreground for the different chart types. :param painter | <QPainter> rect | <QRect> """ if (self.showGrid()): self.drawAxis(painter) def drawGrid(self, painter): """ Draws the rulers for this scene. :param painter | <QPainter> """ # draw the minor grid lines pen = QPen(self.borderColor()) painter.setPen(pen) painter.setBrush(self.baseColor()) # draw the grid data painter.drawRect(self._buildData['grid_rect']) painter.setBrush(self.alternateColor()) painter.setPen(Qt.NoPen) if (self.alternatingRowColors()): for alt_rect in self._buildData['grid_h_alt']: painter.drawRect(alt_rect) if (self.alternatingColumnColors()): for alt_rect in self._buildData['grid_v_alt']: painter.drawRect(alt_rect) if (self.showGrid()): painter.setPen(pen) grid = [] if (self.showRows()): grid += self._buildData['grid_h_lines'] if (self.showColumns()): grid += self._buildData['grid_v_lines'] painter.drawLines(grid) def drawAxis(self, painter): """ Draws the axis for this system. """ # draw the axis lines pen = QPen(self.axisColor()) pen.setWidth(4) painter.setPen(pen) painter.drawLines(self._buildData['axis_lines']) # draw the notches for rect, text in self._buildData['grid_h_notches']: painter.drawText(rect, Qt.AlignTop | Qt.AlignRight, text) for rect, text in self._buildData['grid_v_notches']: painter.drawText(rect, Qt.AlignCenter, text) def enterEvent(self, event): """ Toggles the display for the tracker item. """ item = self.trackerItem() if (item): item.setVisible(True) def eventFilter(self, object, event): """ Filters the chart widget for the resize event to modify this scenes rect. :param object | <QObject> event | <QEvent> """ if (event.type() != event.Resize): return False size = event.size() w = size.width() h = size.height() hpolicy = Qt.ScrollBarAlwaysOff vpolicy = Qt.ScrollBarAlwaysOff if (self._minimumHeight != -1 and h < self._minimumHeight): h = self._minimumHeight vpolicy = Qt.ScrollBarAsNeeded if (self._maximumHeight != -1 and self._maximumHeight < h): h = self._maximumHeight vpolicy = Qt.ScrollBarAsNeeded if (self._minimumWidth != -1 and w < self._minimumWidth): w = self._minimumWidth hpolicy = Qt.ScrollBarAsNeeded if (self._maximumWidth != -1 and self._maximumWidth < w): w = self._maximumWidth hpolicy = Qt.ScrollBarAsNeeded hruler = self.horizontalRuler() vruler = self.verticalRuler() hlen = hruler.minLength(Qt.Horizontal) vlen = hruler.minLength(Qt.Vertical) offset_w = 0 offset_h = 0 # if ( hlen > w ): # w = hlen # hpolicy = Qt.ScrollBarAlwaysOn # offset_h = 25 # # if ( vlen > h ): # h = vlen # vpolicy = Qt.ScrollBarAlwaysOn # offset_w = 25 self.setSceneRect(0, 0, w - offset_w, h - offset_h) object.setVerticalScrollBarPolicy(vpolicy) object.setHorizontalScrollBarPolicy(hpolicy) return False def font(self): """ Returns the font for this scene. :return <QFont> """ return self._font def gridRect(self): """ Returns the grid rect for this chart. :return <QRectF> """ if (self._dirty): self.rebuild() return self._buildData['grid_rect'] def horizontalPadding(self): """ Returns the horizontal padding for this scene. :return <int> """ return self._horizontalPadding def horizontalRuler(self): """ Returns the horizontal (x-axis) ruler for this scene. """ return self._horizontalRuler def isTrackingEnabled(self): """ Returns whether or not tracking is enabled for this chart. :return <bool> """ return self._trackingEnabled def leaveEvent(self, event): """ Toggles the display for the tracker item. """ item = self.trackerItem() if (item): item.setVisible(False) def mapFromChart(self, x, y): """ Maps a chart point to a pixel position within the grid based on the rulers. :param x | <variant> y | <variant> :return <QPointF> """ grid = self.gridRect() hruler = self.horizontalRuler() vruler = self.verticalRuler() xperc = hruler.percentAt(x) yperc = vruler.percentAt(y) xoffset = grid.width() * xperc yoffset = grid.height() * yperc xpos = grid.left() + xoffset ypos = grid.bottom() - yoffset return QPointF(xpos, ypos) def mouseMoveEvent(self, event): """ Overloads the moving event to move the tracker item. :param event | <QEvent> """ super(XChartScene, self).mouseMoveEvent(event) self.updateTrackerItem(event.scenePos()) def pieAxis(self): """ Returns the axis that will be used when calculating percentages for the pie chart. :return <Qt.Axis> """ return self._pieAxis def pieAlignment(self): """ Returns the alignment location to be used for the chart pie. :return <Qt.Alignment> """ return self._pieAlignment def rebuild(self): """ Rebuilds the data for this scene to draw with. """ global XChartWidgetItem if (XChartWidgetItem is None): from projexui.widgets.xchartwidget.xchartwidgetitem \ import XChartWidgetItem self._buildData = {} # build the grid location x = 8 y = 8 w = self.sceneRect().width() h = self.sceneRect().height() hpad = self.horizontalPadding() vpad = self.verticalPadding() hmax = self.horizontalRuler().maxNotchSize(Qt.Horizontal) left_offset = hpad + self.verticalRuler().maxNotchSize(Qt.Vertical) right_offset = left_offset + hpad top_offset = vpad bottom_offset = top_offset + vpad + hmax left = x + left_offset right = w - right_offset top = y + top_offset bottom = h - bottom_offset rect = QRectF() rect.setLeft(left) rect.setRight(right) rect.setBottom(bottom) rect.setTop(top) self._buildData['grid_rect'] = rect # rebuild the ruler data self.rebuildGrid() self._dirty = False # rebuild all the items padding = self.horizontalPadding() + self.verticalPadding() grid = self.sceneRect() filt = lambda x: isinstance(x, XChartWidgetItem) items = filter(filt, self.items()) height = float(grid.height()) if height == 0: ratio = 1 else: ratio = grid.width() / height count = len(items) if (not count): return if (ratio >= 1): radius = (grid.height() - padding * 2) / 2.0 x = rect.center().x() y = rect.center().y() dx = radius * 2.5 dy = 0 else: radius = (grid.width() - padding * 2) / 2.0 x = rect.center().x() y = rect.center().y() dx = 0 dy = radius * 2.5 for item in items: item.setPieCenter(QPointF(x, y)) item.setRadius(radius) item.rebuild() x += dx y += dy if (self._trackerItem and self._trackerItem()): self._trackerItem().rebuild(self._buildData['grid_rect']) def rebuildGrid(self): """ Rebuilds the ruler data. """ vruler = self.verticalRuler() hruler = self.horizontalRuler() rect = self._buildData['grid_rect'] # process the vertical ruler h_lines = [] h_alt = [] h_notches = [] vpstart = vruler.padStart() vnotches = vruler.notches() vpend = vruler.padEnd() vcount = len(vnotches) + vpstart + vpend deltay = rect.height() / max((vcount - 1), 1) y = rect.bottom() alt = False for i in range(vcount): h_lines.append(QLineF(rect.left(), y, rect.right(), y)) # store alternate color if (alt): alt_rect = QRectF(rect.left(), y, rect.width(), deltay) h_alt.append(alt_rect) # store notch information nidx = i - vpstart if (0 <= nidx and nidx < len(vnotches)): notch = vnotches[nidx] notch_rect = QRectF(0, y - 3, rect.left() - 3, deltay) h_notches.append((notch_rect, notch)) y -= deltay alt = not alt self._buildData['grid_h_lines'] = h_lines self._buildData['grid_h_alt'] = h_alt self._buildData['grid_h_notches'] = h_notches # process the horizontal ruler v_lines = [] v_alt = [] v_notches = [] hpstart = hruler.padStart() hnotches = hruler.notches() hpend = hruler.padEnd() hcount = len(hnotches) + hpstart + hpend deltax = rect.width() / max((hcount - 1), 1) x = rect.left() alt = False for i in range(hcount): v_lines.append(QLineF(x, rect.top(), x, rect.bottom())) # store alternate info if (alt): alt_rect = QRectF(x - deltax, rect.top(), deltax, rect.height()) v_alt.append(alt_rect) # store notch information nidx = i - hpstart if (0 <= nidx and nidx < len(hnotches)): notch = hnotches[nidx] notch_rect = QRectF(x - (deltax / 2.0), rect.bottom() + 3, deltax, 13) v_notches.append((notch_rect, notch)) x += deltax alt = not alt self._buildData['grid_v_lines'] = v_lines self._buildData['grid_v_alt'] = v_alt self._buildData['grid_v_notches'] = v_notches # draw the axis lines axis_lines = [] axis_lines.append( QLineF(rect.left(), rect.top(), rect.left(), rect.bottom())) axis_lines.append( QLineF(rect.left(), rect.bottom(), rect.right(), rect.bottom())) self._buildData['axis_lines'] = axis_lines def setBarType(self): self.setChartType(XChartScene.Type.Bar) def setDirty(self, state=True): """ Marks the scene as dirty and needing a rebuild. :param state | <bool> """ self._dirty = state def setChartType(self, chartType): """ Sets the chart type for this scene to the inputed type. :param chartType | <XChartScene.Type> """ self._chartType = chartType self.setDirty() # setup default options if (chartType == XChartScene.Type.Pie): self.setShowGrid(False) self.horizontalRuler().setPadStart(0) self.horizontalRuler().setPadEnd(0) elif (chartType == XChartScene.Type.Bar): self.setShowGrid(True) self.setShowColumns(False) self.setShowRows(True) self.horizontalRuler().setPadStart(1) self.horizontalRuler().setPadEnd(1) else: self.setShowGrid(True) self.setShowColumns(True) self.setShowRows(True) self.horizontalRuler().setPadStart(0) self.horizontalRuler().setPadEnd(0) if (not self.signalsBlocked()): self.chartTypeChanged.emit() def setFont(self, font): """ Sets the font for this scene. :param font | <QFont> """ self._font = font def setHorizontalPadding(self, padding): """ Sets the horizontal padding amount for this chart to the given value. :param padding | <int> """ self._horizontalPadding = padding def setHorizontalRuler(self, ruler): """ Sets the horizontal ruler for this chart to the inputed ruler. :param ruler | <XChartRuler> """ self._horizontalRuler = ruler def setLineType(self): self.setChartType(XChartScene.Type.Line) def setPieAlignment(self, alignment): """ Sets the alignment to be used when rendering a pie chart. :param alignment | <Qt.Alignment> """ self._alignment = alignment def setPieAxis(self, axis): """ Sets the axis to be used when calculating pie chart information. :param axis | <Qt.Axis> """ self._pieAxis = axis def setPieType(self): self.setChartType(XChartScene.Type.Pie) def setSceneRect(self, *args): """ Overloads the set scene rect to handle rebuild information. """ super(XChartScene, self).setSceneRect(*args) self._dirty = True def setShowColumns(self, state): """ Sets whether or not to display the columns for this chart. :param state | <bool> """ self._showColumns = state def setShowGrid(self, state): """ Sets whether or not the grid should be visible. :param state | <bool> """ self._showGrid = state def setShowRows(self, state): """ Sets whether or not to display the rows for this chart. :param state | <bool> """ self._showRows = state def setTrackingEnabled(self, state): """ Sets whether or not information tracking is enabled for this chart. :param state | <bool> """ self._trackingEnabled = state self.updateTrackerItem() def setVerticalPadding(self, padding): """ Sets the vertical padding amount for this chart to the given value. :param padding | <int> """ self._verticalPadding = padding def setVerticalRuler(self, ruler): """ Sets the vertical ruler for this chart to the inputed ruler. :param ruler | <XChartRuler> """ self._verticalRuler = ruler def showColumns(self): """ Returns whether or not to show columns for this scene. :return <bool> """ return self._showColumns def showGrid(self): """ Sets whether or not the grid should be visible for this scene. :return <bool> """ return self._showGrid def showRows(self): """ Returns whether or not to show rows for this scene. :return <bool> """ return self._showRows def trackerItem(self): """ Returns the tracker item for this chart. :return <XChartTrackerItem> || None """ # check for the tracking enabled state if not self.isTrackingEnabled(): return None # generate a new tracker item if not (self._trackerItem and self._trackerItem()): item = XChartTrackerItem() self.addItem(item) self._trackerItem = weakref.ref(item) return self._trackerItem() def updateTrackerItem(self, point=None): """ Updates the tracker item information. """ item = self.trackerItem() if not item: return gridRect = self._buildData.get('grid_rect') if (not (gridRect and gridRect.isValid())): item.setVisible(False) return if (point is not None): item.setPos(point.x(), gridRect.top()) if (not gridRect.contains(item.pos())): item.setVisible(False) return if (self.chartType() != self.Type.Line): item.setVisible(False) return if (not self.isTrackingEnabled()): item.setVisible(False) return item.rebuild(gridRect) def valueAt(self, point): """ Returns the X, Y value for the given point. :param point | <QPoint> :return (<variant> x, <variant> y) """ x = point.x() y = point.y() hruler = self.horizontalRuler() vruler = self.verticalRuler() grid = self._buildData.get('grid_rect') if (not grid): return (None, None) x_perc = 1 - ((grid.right() - x) / grid.width()) y_perc = ((grid.bottom() - y) / grid.height()) return (hruler.valueAt(x_perc), vruler.valueAt(y_perc)) def verticalPadding(self): """ Returns the vertical padding amount for this chart. :return <int> """ return self._verticalPadding def verticalRuler(self): """ Returns the vertical (y-axis) ruler for this chart. :return <XChartRuler> """ return self._verticalRuler
class XView(QWidget): activated = Signal() deactivated = Signal() currentStateChanged = Signal(bool) windowTitleChanged = Signal(str) sizeConstraintChanged = Signal() initialized = Signal() poppedOut = Signal() shown = Signal() hidden = Signal() visibleStateChanged = Signal(bool) _registry = {} _globals = {} # define static globals _dispatcher = None _dispatch = {} __designer_icon__ = resources.find('img/ui/view.png') SignalPolicy = enum('BlockIfNotCurrent', 'BlockIfNotInGroup', 'BlockIfNotVisible', 'BlockIfNotInitialized', 'NeverBlock') def __init__(self, parent): super(XView, self).__init__(parent) # define custom properties self._current = False self._initialized = False self._viewWidget = None self._viewingGroup = 0 self._signalPolicy = XView.SignalPolicy.BlockIfNotInitialized | \ XView.SignalPolicy.BlockIfNotVisible | \ XView.SignalPolicy.BlockIfNotInGroup self._visibleState = False # storing this state for knowing if a # widget WILL be visible once Qt finishes # processing for purpose of signal # validation. # setup default properties self.setAutoFillBackground(True) self.setFocusPolicy(Qt.StrongFocus) self.setWindowTitle(self.viewName()) self.setFocus() def canClose(self): """ Virtual method to determine whether or not this view can properly close. :return <bool> """ return True def closeEvent(self, event): """ Determines whether or not this widget should be deleted after close. :param event | <QtCore.QCloseEvent> """ if not self.canClose(): event.ignore() return elif not self.isViewSingleton(): self.setAttribute(Qt.WA_DeleteOnClose) else: self.setParent( self.window() ) # attach the hidden singleton instance to the window vs. anything in the view super(XView, self).closeEvent(event) def dispatchConnect(self, signal, slot): """ Connect the slot for this view to the given signal that gets emitted by the XView.dispatch() instance. :param signal | <str> slot | <callable> """ XView.dispatch().connect(signal, slot) def dispatchEmit(self, signal, *args): """ Emits the given signal via the XView dispatch instance with the given arguments. :param signal | <str> args | <tuple> """ XView.setGlobal('emitGroup', self.viewingGroup()) XView.dispatch().emit(signal, *args) def duplicate(self, parent): """ Duplicates this current view for another. Subclass this method to provide any additional duplication options. :param parent | <QWidget> :return <XView> | instance of this class """ # only return a single singleton instance if self.isViewSingleton(): return self output = type(self).createInstance(parent, self.viewWidget()) # save/restore the current settings xdata = ElementTree.Element('data') self.saveXml(xdata) new_name = output.objectName() output.setObjectName(self.objectName()) output.restoreXml(xdata) output.setObjectName(new_name) return output def hideEvent(self, event): """ Sets the visible state for this widget. If it is the first time this widget will be visible, the initialized signal will be emitted. :param state | <bool> """ super(XView, self).hideEvent(event) # record the visible state for this widget to be separate of Qt's # system to know if this view WILL be visible or not once the # system is done processing. This will affect how signals are # validated as part of the visible slot delegation self._visibleState = False if not self.signalsBlocked(): self.visibleStateChanged.emit(False) QTimer.singleShot(0, self.hidden) def initialize(self, force=False): """ Initializes the view if it is visible or being loaded. """ if force or (self.isVisible() and \ not self.isInitialized() and \ not self.signalsBlocked()): self._initialized = True self.initialized.emit() def isCurrent(self): """ Returns whether or not this view is current within its view widget. :return <bool> """ return self._current def isInitialized(self): """ Returns whether or not this view has been initialized. A view will be initialized the first time it becomes visible to the user. You can use this to delay loading of information until it is needed by listening for the initialized signal. :return <bool> """ return self._initialized def mousePressEvent(self, event): btn = event.button() mid = btn == Qt.MidButton lft = btn == Qt.LeftButton shft = event.modifiers() == Qt.ShiftModifier if self.windowFlags() & Qt.Dialog and \ (mid or (lft and shft)): pixmap = QPixmap.grabWidget(self) drag = QDrag(self) data = QMimeData() data.setData('x-application/xview/floating_view',\ QByteArray(self.objectName())) drag.setMimeData(data) drag.setPixmap(pixmap) self.hide() drag.exec_() self.show() else: super(XView, self).mousePressEvent(event) def popout(self): self.setParent(self.window()) self.setWindowFlags(Qt.Dialog) self.show() self.raise_() self.activateWindow() pos = QCursor.pos() w = self.width() self.move(pos.x() - w / 2.0, pos.y() - 10) # set the popup instance for this class to this widget key = '_{0}__popupInstance'.format(type(self).__name__) if not hasattr(type(self), key): setattr(type(self), key, weakref.ref(self)) self.poppedOut.emit() def restoreXml(self, xml): """ Restores the settings for this view from the inputed XML node. :param xml | <xml.etree.ElementTree.Element> """ pass def saveXml(self, xml): """ Saves the settings for this view to the inputed XML node. :param xml | <xml.etree.ElementTree.Element> """ pass def settingsName(self): """ Returns the default settings name for this view. :return <str> """ return 'Views/%s' % self.objectName() def setCurrent(self, state=True): """ Marks this view as the current source based on the inputed flag. \ This method will return True if the currency changes. :return <bool> | changed """ if self._current == state: return False widget = self.viewWidget() if widget: for other in widget.findChildren(type(self)): if other.isCurrent(): other._current = False if not other.signalsBlocked(): other.currentStateChanged.emit(state) other.deactivated.emit() self._current = state if not self.signalsBlocked(): self.currentStateChanged.emit(state) if state: self.activated.emit() else: self.deactivated.emit() return True def setFixedHeight(self, height): """ Sets the maximum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setFixedHeight(height) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setFixedWidth(self, width): """ Sets the maximum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setFixedWidth(width) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setMaximumHeight(self, height): """ Sets the maximum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setMaximumHeight(height) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setMaximumSize(self, *args): """ Sets the maximum size value to the inputed size and emits the \ sizeConstraintChanged signal. :param *args | <tuple> """ super(XView, self).setMaximumSize(*args) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setMaximumWidth(self, width): """ Sets the maximum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setMaximumWidth(width) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setMinimumHeight(self, height): """ Sets the minimum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setMinimumHeight(height) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setMinimumSize(self, *args): """ Sets the minimum size value to the inputed size and emits the \ sizeConstraintChanged signal. :param *args | <tuple> """ super(XView, self).setMinimumSize(*args) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setMinimumWidth(self, width): """ Sets the minimum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setMinimumWidth(width) if (not self.signalsBlocked()): self.sizeConstraintChanged.emit() def setSignalPolicy(self, policy): """ Sets the signal delegation policy for this instance to the given policy. By default, signals will be delegates for groups or by currency if they are not in a group. This will not directly affect signal propogation, only the result of the validateSignal method, so if you want to test against this, then you will need to check in your slot. :param policy | <XView.SignalPolicy> """ self._signalPolicy = policy def setViewingGroup(self, grp): """ Sets the viewing group that this view is associated with. :param grp | <int> """ self._viewingGroup = grp def setViewWidget(self, widget): """ Sets the view widget that is associated with this view item. :param widget | <projexui.widgets.xviewwidget.XViewWidget> """ self._viewWidget = widget def setWindowTitle(self, title): """ Sets the window title for this view, and emits the windowTitleChanged \ signal if the signals are not blocked. Setting this title will update \ the tab title for the view within the widget. :param title | <str> """ super(XView, self).setWindowTitle(title) if (not self.signalsBlocked()): self.windowTitleChanged.emit(title) def showActiveState(self, state): """ Shows this view in the active state based on the inputed state settings. :param state | <bool> """ return palette = self.window().palette() clr = palette.color(palette.Window) avg = (clr.red() + clr.green() + clr.blue()) / 3 if avg < 180 and state: clr = clr.lighter(105) elif not state: clr = clr.darker(105) palette.setColor(palette.Window, clr) self.setPalette(palette) def showEvent(self, event): """ Sets the visible state for this widget. If it is the first time this widget will be visible, the initialized signal will be emitted. :param state | <bool> """ super(XView, self).showEvent(event) # record the visible state for this widget to be separate of Qt's # system to know if this view WILL be visible or not once the # system is done processing. This will affect how signals are # validated as part of the visible slot delegation self._visibleState = True if not self.isInitialized(): self.initialize() # after the initial time the view is loaded, the visibleStateChanged # signal will be emitted elif not self.signalsBlocked(): self.visibleStateChanged.emit(True) QTimer.singleShot(0, self.shown) def signalPolicy(self): """ Returns the signal policy for this instance. :return <XView.SignalPolicy> """ return self._signalPolicy def rootWidget(self): widget = self while widget.parent(): widget = widget.parent() return widget def viewWidget(self): """ Returns the view widget that is associated with this instance. :return <projexui.widgets.xviewwidget.XViewWidget> """ return self._viewWidget def validateSignal(self, policy=None): """ Validates that this view is part of the group that was emitting the signal. Views that are not in any viewing group will accept all signals. :param policy | <XView.SignalPolicy> || None :return <bool> """ # validate whether or not to process a signal if policy is None: policy = self.signalPolicy() group_check = XView.getGlobal('emitGroup') == self.viewingGroup() current_check = self.isCurrent() # always delegate signals if they are not set to block, # or if the method is called directly (not from a signal) if not self.sender() or policy & XView.SignalPolicy.NeverBlock: return True # block delegation of the signal if the view is not initialized elif policy & XView.SignalPolicy.BlockIfNotInitialized and \ not self.isInitialized(): return False # block delegation if the view is not visible elif policy & XView.SignalPolicy.BlockIfNotVisible and \ not self._visibleState: return False # block delegation if the view is not part of a group elif self.viewingGroup() and \ policy & XView.SignalPolicy.BlockIfNotInGroup: return group_check # look for only currency releated connections elif policy & XView.SignalPolicy.BlockIfNotCurrent: return current_check else: return True def viewingGroup(self): """ Returns the viewing group that this view is assigned to. :return <int> """ return self._viewingGroup @classmethod def currentView(cls, parent=None): """ Returns the current view for the given class within a viewWidget. If no view widget is supplied, then a blank view is returned. :param viewWidget | <projexui.widgets.xviewwidget.XViewWidget> || None :return <XView> || None """ if parent is None: parent = projexui.topWindow() for inst in parent.findChildren(cls): if inst.isCurrent(): return inst return None @classmethod def createInstance(cls, parent, viewWidget=None): singleton_key = '_{0}__singleton'.format(cls.__name__) singleton = getattr(cls, singleton_key, None) singleton = singleton() if singleton is not None else None # assign the singleton instance if singleton is not None: singleton.setParent(parent) return singleton else: # determine if we need to store a singleton inst = cls(parent) inst.setObjectName(cls.uniqueName()) inst.setViewWidget(viewWidget) if cls.isViewSingleton(): setattr(cls, singleton_key, weakref.ref(inst)) return inst @classmethod def destroySingleton(cls): """ Destroys the singleton instance of this class, if one exists. """ singleton_key = '_{0}__singleton'.format(cls.__name__) singleton = getattr(cls, singleton_key, None) if singleton is not None: setattr(cls, singleton_key, None) singleton.close() singleton.deleteLater() @classmethod def instances(cls, parent=None): """ Returns all the instances that exist for a given parent. If no parent exists, then a blank list will be returned. :param parent | <QtGui.QWidget> :return [<XView>, ..] """ if parent is None: parent = projexui.topWindow() return parent.findChildren(cls) @classmethod def isViewAbstract(cls): """ Returns whether or not this view is a purely abstract view or not. :return <bool> """ return getattr(cls, '_{0}__viewAbstract'.format(cls.__name__), False) @classmethod def isViewSingleton(cls): return getattr(cls, '_{0}__viewSingleton'.format(cls.__name__), False) @classmethod def isPopupSingleton(cls): return getattr(cls, '_{0}__popupSingleton'.format(cls.__name__), True) @classmethod def popup(cls, parent=None, viewWidget=None): """ Pops up this view as a new dialog. If the forceDialog flag is set to \ False, then it will try to activate the first instance of the view \ within an existing viewwidget context before creating a new dialog. :param parent | <QWidget> || None viewWidget | <projexui.widgets.xviewwidget.XViewWidget> || None """ if cls.isViewSingleton(): inst = cls.createInstance(parent, viewWidget) inst.setWindowFlags(Qt.Dialog) else: inst = cls.popupInstance(parent, viewWidget) inst.showNormal() inst.setFocus() inst.raise_() inst.activateWindow() @classmethod def popupInstance(cls, parent, viewWidget=None): key = '_{0}__popupInstance'.format(cls.__name__) try: inst = getattr(cls, key, None)() except TypeError: inst = None if inst is not None: return inst # create a new instance for this popup inst = cls.createInstance(parent, viewWidget) inst.setWindowFlags(Qt.Dialog) if cls.isPopupSingleton(): setattr(cls, key, weakref.ref(inst)) return inst @classmethod def registerToWindow(cls, window): """ Registers this view to the window to update additional menu items, \ actions, and toolbars. :param window | <QWidget> """ pass @classmethod def restoreGlobalSettings(cls, settings): """ Restores the global settings for the inputed view class type. :param cls | <subclass of XView> settings | <QSettings> """ pass @classmethod def saveGlobalSettings(cls, settings): """ Saves the global settings for the inputed view class type. :param cls | <subclass of XView> settings | <QSettings> """ pass @classmethod def setViewAbstract(cls, state): """ Sets whether or not this view is used only as an abstract class. :param state | <bool> """ setattr(cls, '_{0}__viewAbstract'.format(cls.__name__), state) @classmethod def setViewGroup(cls, grp): setattr(cls, '_{0}__viewGroup'.format(cls.__name__), grp) @classmethod def setViewIcon(cls, icon): setattr(cls, '_{0}__viewIcon'.format(cls.__name__), icon) @classmethod def setViewName(cls, name): setattr(cls, '_{0}__viewName'.format(cls.__name__), name) @classmethod def setViewSingleton(cls, state): setattr(cls, '_{0}__viewSingleton'.format(cls.__name__), state) @classmethod def setPopupSingleton(cls, state): setattr(cls, '_{0}__popupSingleton'.format(cls.__name__), state) @classmethod def uniqueName(cls): key = '_{0}__serial'.format(cls.__name__) next = getattr(cls, key, 0) + 1 setattr(cls, key, next) return '{0}{1:02}'.format(cls.viewName(), next) @classmethod def unregisterToWindow(cls, window): """ Registers this view to the window to update additional menu items, \ actions, and toolbars. :param window | <QWidget> """ pass @classmethod def viewGroup(cls): return getattr(cls, '_{0}__viewGroup'.format(cls.__name__), 'Default') @classmethod def viewIcon(cls): default = resources.find('img/view/view.png') return getattr(cls, '_{0}__viewIcon'.format(cls.__name__), default) @classmethod def viewName(cls): return getattr(cls, '_{0}__viewName'.format(cls.__name__), cls.__name__) @classmethod def viewTypeName(cls): """ Returns the unique name for this view type by joining its group with \ its name. :return <str> """ return '%s.%s' % (cls.viewGroup(), cls.viewName()) #-------------------------------------------------------------------------- @staticmethod def dispatch(location='Central'): """ Returns the instance of the global view dispatching system. All views \ will route their signals through the central hub so no single view \ necessarily depends on another. :return <XViewDispatch> """ dispatch = XView._dispatch.get(nativestring(location)) if not dispatch: dispatch = XViewDispatch(QApplication.instance()) XView._dispatch[nativestring(location)] = dispatch return dispatch @staticmethod def getGlobal(key, default=None): """ Returns the global value for the inputed key. :param key | <str> default | <variant> :return <variant> """ return XView._globals.get(key, default) @staticmethod def registeredView(viewName, location='Central'): """ Returns the view that is registered to the inputed location for the \ given name. :param viewName | <str> location | <str> :return <subclass of XView> || None """ loc = nativestring(location) view = XView._registry.get(loc, {}).get(viewName, None) if not view: for view in XView._registry.get(nativestring(location), {}).values(): if view.__name__ == viewName: return view return view @staticmethod def registeredViews(location='Central'): """ Returns all the views types that have bene registered to a particular \ location. :param location | <str> :return [<subclass of XView>, ..] """ return [ view for view in XView._registry.get(nativestring( location), {}).values() if not view.isViewAbstract() ] @staticmethod def registerView(viewType, location='Central'): """ Registers the inputed view type to the given location. The location \ is just a way to group and organize potential view plugins for a \ particular widget, and is determined per application. This eases \ use when building a plugin based system. It has no relevance to the \ XView class itself where you register a view. :param viewType | <subclass of XView> """ # update the dispatch signals sigs = getattr(viewType, '__xview_signals__', []) XView.dispatch(location).registerSignals(sigs) location = nativestring(location) XView._registry.setdefault(location, {}) XView._registry[location][viewType.viewName()] = viewType XView.dispatch(location).emit('registeredView(QVariant)', viewType) @staticmethod def unregisterView(viewType, location='Central'): """ Unregisteres the given view type from the inputed location. :param viewType | <subclass of XView> """ XView._registry.get(location, {}).pop(viewType.viewName(), None) XView.dispatch(location).emit('unregisteredView(QVariant)', viewType) @staticmethod def setGlobal(key, value): """ Shares a global value across all views by setting the key in the \ globals dictionary to the inputed value. :param key | <str> value | <variant> """ XView._globals[key] = value
class Index(object): """ Defines an indexed way to lookup information from a database. Creating an Index generates an object that works like a method, however has a preset query built into it, along with caching options. """ Flags = enum('Unique', 'Private', 'Static') def __json__(self): output = { 'name': self.__name, 'dbname': self.__dbname, 'columns': self.__columns, 'flags': {k: True for k in self.Flags.toSet(self.__flags)}, 'order': self.__order } return output def __init__(self, columns=None, name='', dbname='', flags=0, order=None): self.__name = self.__name__ = name self.__dbname = dbname self.__columns = columns or [] self.__flags = self.Flags.fromSet(flags) if isinstance(flags, set) else flags self.__order = order self.__schema = None def __call__(self, model, *values, **context): # make sure we have the right number of arguments if len(values) != len(self.__columns): name = self.__name columnCount = len(self.__columns) valueCount = len(values) opts = (name, columnCount, valueCount) text = '%s() takes exactly %i arguments (%i given)' % opts raise TypeError(text) # create the lookup query schema = model.schema() query = orb.Query() for i, col in enumerate(self.__columns): value = values[i] column = schema.column(col) if isinstance(value, orb.Model) and not value.isRecord(): return None if self.testFlag( self.Flags.Unique) else orb.Collection() elif not column: raise errors.ColumnNotFound(schema.name(), col) query &= orb.Query(col) == value context['where'] = query & context.get('where') records = model.select(**context) return records.first() if self.testFlag(self.Flags.Unique) else records def copy(self): other = type(self)(columns=self.__columns[:], name=self.__name, dbname=self.__dbname, flags=self.__flags, order=self.__order) return other def columns(self): """ Returns the list of column names that this index will be expecting as \ inputs when it is called. :return [<str>, ..] """ schema = self.schema() return [schema.column(col) for col in self.__columns] def dbname(self): return self.__dbname or orb.system.syntax().indexdb( self.__schema, self.__name) def flags(self): return self.__flags def name(self): """ Returns the name of this index. :return <str> """ return self.__name def schema(self): return self.__schema def setColumns(self, columns): """ Sets the list of the column names that this index will use when \ looking of the records. :param columns | [<str>, ..] """ self.__columns = columns def setOrder(self, order): """ Sets the order information for this index for how to sort and \ organize the looked up data. :param order | [(<str> field, <str> direction), ..] """ self.__order = order def setDbName(self, dbname): self.__dbname = dbname def setFlags(self, flags): self.__flags = flags def setName(self, name): """ Sets the name for this index to this index. :param name | <str> """ self.__name = self.__name__ = name def setSchema(self, schema): self.__schema = schema def validate(self, record, values): """ Validates whether or not this index's requirements are satisfied by the inputted record and values. If this index fails validation, a ValidationError will be raised. :param record | subclass of <orb.Table> values | {<orb.Column>: <variant>, ..} :return <bool> """ schema = record.schema() columns = self.columns() try: column_values = [values[col] for col in columns] except KeyError as err: msg = 'Missing {0} from {1}.{2} index'.format( err[0].name(), record.schema().name(), self.name()) raise errors.IndexValidationError(self, msg=msg) # # ensure a unique record is preserved # if self.unique(): # lookup = getattr(record, self.name()) # other = lookup(*column_values) # if other and other != record: # msg = 'A record already exists with the same {0} combination.'.format(', '.join(self.columnNames())) # raise errors.IndexValidationError(self, msg=msg) return True def testFlag(self, flags): return (self.__flags & flags) > 0
class XView(QWidget): activated = qt.Signal() currentStateChanged = qt.Signal() windowTitleChanged = qt.Signal(str) sizeConstraintChanged = qt.Signal() initialized = qt.Signal() visibleStateChanged = qt.Signal(bool) _registry = {} _globals = {} _viewName = '' _viewGroup = 'Default' _viewIcon = resources.find('img/view/view.png') _viewSingleton = False _popupSingleton = True _popupInstance = None _currentViewRef = None _instances = None _instanceSingleton = None _dispatcher = None _dispatch = {} __designer_icon__ = resources.find('img/ui/view.png') SignalPolicy = enum('BlockIfNotCurrent', 'BlockIfNotInGroup', 'BlockIfNotVisible', 'BlockIfNotInitialized', 'NeverBlock') def __init__(self, parent, autoKillThreads=True): super(XView, self).__init__( parent ) if not self._viewSingleton: self.setAttribute(Qt.WA_DeleteOnClose) # define custom properties self._initialized = False self._destroyThreadsOnClose = True self._viewingGroup = 0 self._signalPolicy = XView.SignalPolicy.BlockIfNotInitialized | \ XView.SignalPolicy.BlockIfNotVisible | \ XView.SignalPolicy.BlockIfNotInGroup self._visibleState = False # storing this state for knowing if a # widget WILL be visible once Qt finishes # processing for purpose of signal # validation. # setup default properties self.setFocusPolicy( Qt.StrongFocus ) self.setWindowTitle( self.viewName() ) self.registerInstance(self) if autoKillThreads: QApplication.instance().aboutToQuit.connect(self.killChildThreads) def canClose( self ): return True def closeEvent( self, event ): if self.isViewSingleton(): self.setParent(None) # make sure to destroy any threads running on close if self.testAttribute(Qt.WA_DeleteOnClose) and \ self.destroyThreadsOnClose(): self.killChildThreads() # clear out any progress loaders XLoaderWidget.stopAll(self) # remove any registered instances if self.testAttribute(Qt.WA_DeleteOnClose): self.unregisterInstance(self) super(XView, self).closeEvent(event) def dispatchConnect( self, signal, slot ): """ Connect the slot for this view to the given signal that gets emitted by the XView.dispatch() instance. :param signal | <str> slot | <callable> """ XView.dispatch().connect(signal, slot) def dispatchEmit( self, signal, *args ): """ Emits the given signal via the XView dispatch instance with the given arguments. :param signal | <str> args | <tuple> """ XView.setGlobal('emitGroup', self.viewingGroup()) XView.dispatch().emit(signal, *args) def destroyThreadsOnClose( self ): """ Marks whether or not any child threads should be destroyed when \ this view is closed. :return <bool> """ return self._destroyThreadsOnClose def duplicate( self, parent ): """ Duplicates this current view for another. Subclass this method to provide any additional duplication options. :param parent | <QWidget> :return <XView> | instance of this class """ # only return a single singleton instance if ( self.isViewSingleton() ): return self output = type(self).createInstance(parent) # save/restore the current settings xdata = ElementTree.Element('data') self.saveXml(xdata) new_name = output.objectName() output.setObjectName(self.objectName()) output.restoreXml(xdata) output.setObjectName(new_name) return output def killChildThreads(self): """ Kills all child threads for this view. """ threads = self.findChildren(QThread) for thread in threads: thread.finished.connect(thread.deleteLater) thread.quit() thread.wait(100) def hideEvent(self, event): """ Sets the visible state for this widget. If it is the first time this widget will be visible, the initialized signal will be emitted. :param state | <bool> """ super(XView, self).hideEvent(event) # record the visible state for this widget to be separate of Qt's # system to know if this view WILL be visible or not once the # system is done processing. This will affect how signals are # validated as part of the visible slot delegation self._visibleState = False if not self.signalsBlocked(): self.visibleStateChanged.emit(False) def initialize(self, force=False): """ Initializes the view if it is visible or being loaded. """ if force or (self.isVisible() and \ not self.isInitialized() and \ not self.signalsBlocked()): self._initialized = True self.initialized.emit() def isCurrent( self ): return self == self.currentView() def isInitialized(self): """ Returns whether or not this view has been initialized. A view will be initialized the first time it becomes visible to the user. You can use this to delay loading of information until it is needed by listening for the initialized signal. :return <bool> """ return self._initialized @deprecatedmethod('XView', 'Use restoreXml instead.') def restoreSettings( self, settings ): """ Restores the settings for this view from the inputed QSettings. :param settings | <QSettings> """ pass def restoreXml( self, xml ): """ Restores the settings for this view from the inputed XML node. :param xml | <xml.etree.ElementTree.Element> """ pass @deprecatedmethod('XView', 'Use saveXml instead.') def saveSettings( self, settings ): """ Saves the current settings for this view to the inputed QSettings. :param settings | <QSettings> """ pass def saveXml( self, xml ): """ Saves the settings for this view to the inputed XML node. :param xml | <xml.etree.ElementTree.Element> """ pass def settingsName( self ): """ Returns the default settings name for this view. :return <str> """ return 'Views/%s' % self.objectName() def setCurrent( self, state = True ): """ Marks this view as the current source based on the inputed flag. \ This method will return True if the currency changes. :return <bool> """ if ( state ): changed = self.setCurrentView(self) elif ( self.currentView() == self ): changed = self.setCurrentView(None) if ( changed and not self.signalsBlocked() ): self.currentStateChanged.emit() if state: self.activated.emit() return changed def setDestroyThreadsOnClose( self, state ): """ Marks whether or not any child threads should be destroyed when \ this view is closed. :param state | <bool> """ self._destroyThreadsOnClose = state def setFixedHeight( self, height ): """ Sets the maximum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setFixedHeight(height) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setFixedWidth( self, width ): """ Sets the maximum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setFixedWidth(width) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMaximumHeight( self, height ): """ Sets the maximum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setMaximumHeight(height) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMaximumSize( self, *args ): """ Sets the maximum size value to the inputed size and emits the \ sizeConstraintChanged signal. :param *args | <tuple> """ super(XView, self).setMaximumSize(*args) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMaximumWidth( self, width ): """ Sets the maximum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setMaximumWidth(width) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMinimumHeight( self, height ): """ Sets the minimum height value to the inputed height and emits the \ sizeConstraintChanged signal. :param height | <int> """ super(XView, self).setMinimumHeight(height) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMinimumSize( self, *args ): """ Sets the minimum size value to the inputed size and emits the \ sizeConstraintChanged signal. :param *args | <tuple> """ super(XView, self).setMinimumSize(*args) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setMinimumWidth( self, width ): """ Sets the minimum width value to the inputed width and emits the \ sizeConstraintChanged signal. :param width | <int> """ super(XView, self).setMinimumWidth(width) if ( not self.signalsBlocked() ): self.sizeConstraintChanged.emit() def setSignalPolicy( self, policy ): """ Sets the signal delegation policy for this instance to the given policy. By default, signals will be delegates for groups or by currency if they are not in a group. This will not directly affect signal propogation, only the result of the validateSignal method, so if you want to test against this, then you will need to check in your slot. :param policy | <XView.SignalPolicy> """ self._signalPolicy = policy def setViewingGroup( self, grp ): """ Sets the viewing group that this view is associated with. :param grp | <int> """ self._viewingGroup = grp def setWindowTitle( self, title ): """ Sets the window title for this view, and emits the windowTitleChanged \ signal if the signals are not blocked. Setting this title will update \ the tab title for the view within the widget. :param title | <str> """ super(XView, self).setWindowTitle(title) if ( not self.signalsBlocked() ): self.windowTitleChanged.emit(title) def showActiveState( self, state ): """ Shows this view in the active state based on the inputed state settings. :param state | <bool> """ self.setAutoFillBackground(True) palette = self.window().palette() clr = palette.color(palette.Window) avg = (clr.red() + clr.green() + clr.blue()) / 3 if ( avg < 180 and state ): clr = clr.lighter(105) elif ( not state ): clr = clr.darker(105) palette.setColor(palette.Window, clr) self.setPalette(palette) def showEvent(self, event): """ Sets the visible state for this widget. If it is the first time this widget will be visible, the initialized signal will be emitted. :param state | <bool> """ super(XView, self).showEvent(event) # record the visible state for this widget to be separate of Qt's # system to know if this view WILL be visible or not once the # system is done processing. This will affect how signals are # validated as part of the visible slot delegation self._visibleState = True if not self.isInitialized(): self.initialize() # after the initial time the view is loaded, the visibleStateChanged # signal will be emitted elif not self.signalsBlocked(): self.visibleStateChanged.emit(True) def signalPolicy( self ): """ Returns the signal policy for this instance. :return <XView.SignalPolicy> """ return self._signalPolicy def rootWidget( self ): widget = self while ( widget.parent() ): widget = widget.parent() return widget def validateSignal( self, policy = None ): """ Validates that this view is part of the group that was emitting the signal. Views that are not in any viewing group will accept all signals. :param policy | <XView.SignalPolicy> || None :return <bool> """ # validate whether or not to process a signal if policy is None: policy = self.signalPolicy() group_check = XView.getGlobal('emitGroup') == self.viewingGroup() current_check = self.isCurrent() # always delegate signals if they are not set to block, # or if the method is called directly (not from a signal) if not self.sender() or policy & XView.SignalPolicy.NeverBlock: return True # block delegation of the signal if the view is not initialized elif policy & XView.SignalPolicy.BlockIfNotInitialized and \ not self.isInitialized(): return False # block delegation if the view is not visible elif policy & XView.SignalPolicy.BlockIfNotVisible and \ not self._visibleState: return False # block delegation if the view is not part of a group elif self.viewingGroup() and \ policy & XView.SignalPolicy.BlockIfNotInGroup: return group_check # look for only currency releated connections elif policy & XView.SignalPolicy.BlockIfNotCurrent: return current_check else: return True def viewingGroup( self ): """ Returns the viewing group that this view is assigned to. :return <int> """ return self._viewingGroup @classmethod def currentView( cls ): # look for last focused view if ( cls._currentViewRef ): inst = cls._currentViewRef() if ( inst ): return inst cls._currentViewRef = None return None @classmethod def createInstance( cls, parent ): # assign the singleton instance if ( cls._instanceSingleton ): instance = cls._instanceSingleton() # make sure the instance is still in use if ( not instance ): cls._instanceSingleton = None else: instance.setParent(parent) return instance # determine if we need to store a singleton inst = cls(parent) inst.setObjectName( cls.uniqueName() ) if ( cls.isViewSingleton() ): cls._instanceSingleton = weakref.ref(inst) return inst @classmethod def destroyInstance( cls, inst ): if ( cls.isViewSingleton() ): inst.close() return inst.close() inst.setParent(None) inst.deleteLater() @classmethod @deprecatedmethod('XView', 'Use the XView.dispatch() syntax now.') def dispatcher( cls ): if ( not cls._dispatcher ): cls._dispatcher = cls.MetaDispatcher(cls) return cls._dispatcher @classmethod def instances( cls ): """ Returns all generated instances of a particular view type. :return [<XView>, ..] """ if ( not cls._instances ): return [] # purge the instance list insts = [] refs = [] for ref in cls._instances: inst = ref() if ( not inst ): continue insts.append(inst) refs.append(ref) cls._instances = refs return insts @classmethod def isViewSingleton( cls ): return cls._viewSingleton @classmethod def isPopupSingleton( cls ): return cls._popupSingleton @classmethod def popup( cls, parent = None ): """ Pops up this view as a new dialog. If the forceDialog flag is set to \ False, then it will try to activate the first instance of the view \ within an existing viewwidget context before creating a new dialog. :param parent | <QWidget> || None """ # popup the singleton view for this class if ( cls.isViewSingleton() ): inst = cls.currentView() if ( not inst ): inst = cls.createInstance(parent) inst.setWindowFlags(Qt.Dialog) inst.show() # popup the popupSingleton for this class inst = cls.popupInstance(parent) inst.show() @classmethod def popupInstance( cls, parent ): if ( cls._popupInstance ): return cls._popupInstance inst = cls.createInstance(parent) inst.setWindowFlags(Qt.Dialog) if cls.isPopupSingleton(): inst.setAttribute(Qt.WA_DeleteOnClose, False) cls._popupInstance = inst return inst @classmethod def registerInstance( cls, instance ): if ( not cls._instances ): cls._instances = [] cls._instances.append(weakref.ref(instance)) @classmethod def registerToWindow( cls, window ): """ Registers this view to the window to update additional menu items, \ actions, and toolbars. :param window | <QWidget> """ pass @classmethod def restoreGlobalSettings( cls, settings ): """ Restores the global settings for the inputed view class type. :param cls | <subclass of XView> settings | <QSettings> """ pass @classmethod def saveGlobalSettings( cls, settings ): """ Saves the global settings for the inputed view class type. :param cls | <subclass of XView> settings | <QSettings> """ pass @classmethod def setViewGroup( cls, grp ): cls._viewGroup = grp @classmethod def setCurrentView( cls, view ): current = cls.currentView() if ( current == view ): return False elif ( current ): current.showActiveState(False) if ( view ): view.showActiveState(True) cls._currentViewRef = weakref.ref(view) return True @classmethod def setViewIcon( cls, icon ): cls._viewIcon = icon @classmethod def setViewName( cls, name ): cls._viewName = name @classmethod def setViewSingleton( cls, state ): cls._viewSingleton = state @classmethod def setPopupSingleton( cls, state ): cls._popupSingleton = state @classmethod def uniqueName( cls ): names = map(lambda x: str(x.objectName()), cls.instances()) index = 1 base = cls.viewName() name = '%s%02i' % (base, index) while ( name in names ): index += 1 name = '%s%02i' % (base, index) return name @classmethod def unregisterInstance( cls, instance ): if ( not cls._instances ): return if ( cls._currentViewRef and instance == cls._currentViewRef() ): cls._currentViewRef = None refs = [] for ref in cls._instances: inst = ref() if ( not inst or inst == instance ): continue refs.append(ref) cls._instances = refs @classmethod def unregisterToWindow( cls, window ): """ Registers this view to the window to update additional menu items, \ actions, and toolbars. :param window | <QWidget> """ pass @classmethod def viewGroup( cls ): return cls._viewGroup @classmethod def viewIcon( cls ): return cls._viewIcon @classmethod def viewName( cls ): return cls._viewName @classmethod def viewTypeName( cls ): """ Returns the unique name for this view type by joining its group with \ its name. :return <str> """ return '%s.%s' % (cls.viewGroup(), cls.viewName()) #-------------------------------------------------------------------------- @staticmethod def dispatch( location = 'Central' ): """ Returns the instance of the global view dispatching system. All views \ will route their signals through the central hub so no single view \ necessarily depends on another. :return <XViewDispatch> """ dispatch = XView._dispatch.get(str(location)) if ( not dispatch ): dispatch = XViewDispatch(QApplication.instance()) XView._dispatch[str(location)] = dispatch return dispatch @staticmethod def getGlobal( key, default = None ): """ Returns the global value for the inputed key. :param key | <str> default | <variant> :return <variant> """ return XView._globals.get(key, default) @staticmethod def registeredView( viewName, location = 'Central' ): """ Returns the view that is registered to the inputed location for the \ given name. :param viewName | <str> location | <str> :return <subclass of XView> || None """ for viewType in XView._registry.get(str(location), []): if ( viewType.viewName() == viewName ): return viewType return None @staticmethod def registeredViews( location = 'Central' ): """ Returns all the views types that have bene registered to a particular \ location. :param location | <str> :return [<subclass of XView>, ..] """ return XView._registry.get(str(location), []) @staticmethod def registerView( viewType, location = 'Central' ): """ Registers the inputed view type to the given location. The location \ is just a way to group and organize potential view plugins for a \ particular widget, and is determined per application. This eases \ use when building a plugin based system. It has no relevance to the \ XView class itself where you register a view. :param viewType | <subclass of XView> """ # update the dispatch signals if ( '__xview_signals__' in viewType.__dict__ ): XView.dispatch(location).registerSignals(viewType.__xview_signals__) location = str(location) XView._registry.setdefault(location, []) XView._registry[str(location)].append(viewType) @staticmethod def setGlobal( key, value ): """ Shares a global value across all views by setting the key in the \ globals dictionary to the inputed value. :param key | <str> value | <variant> """ XView._globals[key] = value @staticmethod def updateCurrentView( oldWidget, newWidget ): """ Updates the current view for each view class type based on the new \ focused widget. :param oldWidget | <QWidget> newWidget | <QWidget> """ widget = newWidget while widget: if isinstance(widget, XView): widget.setCurrent() break widget = widget.parent()
class XGanttWidget(QWidget): dateRangeChanged = qt.Signal() Timescale = enum('Week', 'Month', 'Year') def __init__(self, parent=None, _availabilityEnabled=0): super(XGanttWidget, self).__init__(parent) ''' menubar = QMenuBar(self) #menubar.sizeHint(QSize.setHeight(10)) fileMenu = menubar.addMenu('&File') fileMenu.addAction('Create Project') fileMenu.addSeparator() fileMenu.addAction('Save') fileMenu.addSeparator() fileMenu.addAction('Exit') fileMenu.triggered.connect( self.fileMenuActions ) ''' # load the user interface if getattr(sys, 'frozen', None): #print (sys._MEIPASS+"/ui/xganttwidget.ui"); projexui.loadUi(sys._MEIPASS, self, uifile=(sys._MEIPASS + "/ui/xganttwidget.ui")) else: projexui.loadUi(__file__, self) # define custom properties self._backend = None self._dateStart = QDate.currentDate() self._dateEnd = QDate.currentDate().addMonths(12) self._alternatingRowColors = False self._cellWidth = 15 self._cellHeight = 15 self._first = True self._dateFormat = 'M/d/yy' self._timescale = XGanttWidget.Timescale.Year self._scrolling = False # setup the palette colors palette = self.palette() color = palette.color(palette.Base) self._gridPen = QPen(color.darker(135)) self._brush = QBrush(color) self._alternateBrush = QBrush(color.darker(105)) self._currentDayBrush = QBrush(QColor(146, 252, 186)) self._holidayBrush = QBrush(QColor(166, 46, 46)) self._bookedBrush = QBrush(QColor(20, 250, 0)) self._unavailableBrush = QBrush(QColor(75, 75, 75)) self._underbookedBrush = QBrush(QColor(255, 255, 20)) self._overbookedBrush = QBrush(QColor(255, 25, 25)) self._overbookedAmount = {} self._unassignedBrush = QBrush(QColor(25, 25, 255)) weekendColor = color.darker(148) self._availabilityEnabled = _availabilityEnabled self._weekendBrush = QBrush(weekendColor) # setup the columns for the tree if _availabilityEnabled: self.setColumns(['Name']) else: self.setColumns( ['Name', 'Start', 'End', 'Calendar Days', 'Work Days']) header = self.uiGanttTREE.header() header.setFixedHeight(self._cellHeight * 2) header.setResizeMode(0, header.ResizeToContents) header.setDefaultSectionSize(60) headerItem = self.uiGanttTREE.headerItem() headerItem.setSizeHint(0, QSize(10, header.height())) self.uiGanttTREE.setContextMenuPolicy(Qt.CustomContextMenu) # connect signals self.uiGanttTREE.customContextMenuRequested.connect( self.showProjectMenu) # initialize the tree widget self.uiGanttTREE.setShowGrid(False) #enable drag and drop self.uiGanttTREE.setDragDropFilter( self.uiGanttTREE.setDragDropFilter(XGanttWidget.handleDragDrop)) if (sharedDB.currentUser._idPrivileges == 3): self.uiGanttTREE.setEditable(False) for act in sharedDB.mainWindow._fileMenu.actions(): if act.text() == "Save" or act.text() == "Create Project": act.setEnabled(False) else: self.uiGanttTREE.setEditable(True) self.uiGanttTREE.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.uiGanttTREE.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.uiGanttTREE.setVerticalScrollMode(self.uiGanttTREE.ScrollPerPixel) #left half size self.uiGanttTREE.resize(400, 20) # initialize the view widget #self.uiGanttVIEW.setDragMode( self.uiGanttVIEW.RubberBandDrag ) self.uiGanttVIEW.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.uiGanttVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiGanttVIEW.setScene(XGanttScene(self)) self.uiGanttVIEW.installEventFilter(self) #self.uiGanttVIEW.horizontalScrollBar().setValue(50) # create connections self.uiGanttTREE.itemExpanded.connect(self.syncView) self.uiGanttTREE.itemCollapsed.connect(self.syncView) # connect scrollbars tree_bar = self.uiGanttTREE.verticalScrollBar() view_bar = self.uiGanttVIEW.verticalScrollBar() tree_bar.rangeChanged.connect(self.__updateViewRect) tree_bar.valueChanged.connect(self.__scrollView) view_bar.valueChanged.connect(self.__scrollTree) # connect selection self.uiGanttTREE.itemSelectionChanged.connect(self.__selectView) self.uiGanttVIEW.scene().selectionChanged.connect(self.__selectTree) self.uiGanttTREE.itemChanged.connect(self.updateItemData) if self._availabilityEnabled: self._currentDayBrush = None self._holidayBrush = QBrush(QColor(75, 75, 75)) weekendColor = QBrush(QColor(75, 75, 75)) self.uiGanttTREE.setEditable(False) #self._cellHeight = 12 def __del__(self): self.uiGanttVIEW.scene().selectionChanged.disconnect(self.__selectTree) def __scrollTree(self, value): """ Updates the tree view scrolling to the inputed value. :param value | <int> """ if (self._scrolling): return tree_bar = self.uiGanttTREE.verticalScrollBar() self._scrolling = True tree_bar.setValue(value) self._scrolling = False def __scrollView(self, value): """ Updates the gantt view scrolling to the inputed value. :param value | <int> """ if (self._scrolling): return view_bar = self.uiGanttVIEW.verticalScrollBar() self._scrolling = True view_bar.setValue(value) self._scrolling = False def __selectTree(self): """ Matches the tree selection to the views selection. """ self.uiGanttTREE.blockSignals(True) self.uiGanttTREE.clearSelection() for item in self.uiGanttVIEW.scene().selectedItems(): item.treeItem().setSelected(True) sharedDB.sel.select([item.treeItem()._dbEntry]) self.uiGanttTREE.blockSignals(False) def __selectView(self): """ Matches the view selection to the trees selection. """ self.uiGanttVIEW.scene().blockSignals(True) self.uiGanttVIEW.scene().clearSelection() for item in self.uiGanttTREE.selectedItems(): item.viewItem().setSelected(True) sharedDB.sel.select([item._dbEntry]) self.uiGanttVIEW.scene().blockSignals(False) def __updateViewRect(self): """ Updates the view rect to match the current tree value. """ header_h = self._cellHeight * 2 rect = self.uiGanttVIEW.scene().sceneRect() sbar_max = self.uiGanttTREE.verticalScrollBar().maximum() sbar_max += self.uiGanttTREE.viewport().height() + header_h widget_max = self.uiGanttVIEW.height() widget_max -= (self.uiGanttVIEW.horizontalScrollBar().height() + 10) rect.setHeight(max(widget_max, sbar_max)) self.uiGanttVIEW.scene().setSceneRect(rect) def addTopLevelItem(self, item): """ Adds the inputed item to the gantt widget. :param item | <XGanttWidgetItem> """ vitem = item.viewItem() self.treeWidget().addTopLevelItem(item) if not self._availabilityEnabled: self.viewWidget().scene().addItem(vitem) item._viewItem = weakref.ref(vitem) #set scrollbar offset #item.treeWidget._scrollBar = self.uiGanttTREE.verticalScrollBar() item.sync(recursive=True) def alternateBrush(self): """ Returns the alternate brush to be used for the grid view. :return <QBrush> """ return self._alternateBrush def alternatingRowColors(self): """ Returns whether or not this widget should show alternating row colors. :return <bool> """ return self._alternatingRowColors def brush(self): """ Returns the background brush to be used for the grid view. :return <QBrush> """ return self._brush def cellHeight(self): """ Returns the height for the cells in this gantt's views. :return <int> """ return self._cellHeight def cellWidth(self): """ Returns the width for the cells in this gantt's views. :return <int> """ return self._cellWidth def clear(self): """ Clears all the gantt widget items for this widget. """ self.uiGanttTREE.clear() self.uiGanttVIEW.scene().clear() def closeEvent(self, event): if sharedDB.changesToBeSaved and sharedDB.users.currentUser._idPrivileges != 3: quit_msg = "Save before exit?" reply = QtGui.QMessageBox.question(self, 'Message', quit_msg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No, QtGui.QMessageBox.Cancel) if reply == QtGui.QMessageBox.Yes: self.SaveToDatabase() event.accept() elif reply == QtGui.QMessageBox.No: event.accept() else: event.ignore() def columns(self): """ Returns a list of the columns being used in the treewidget of this gantt chart. :return [<str>, ..] """ return self.treeWidget().columns() '''def CreateProject(self): #self._myCreateProjectWidget = CreateProjectWidget() #self._myCreateProjectWidget.show() sharedDB.app.CreateProjectWidget() ''' def dateEnd(self): """ Returns the date end for this date range of this gantt widget. :return <QDate> """ return self._dateEnd def dateFormat(self): """ Returns the date format that will be used when rendering items in the view. :return <str> """ return self._dateFormat def dateStart(self): """ Returns the date start for the date range of this gantt widget. :return <QDate> """ return self._dateStart def emitDateRangeChanged(self): """ Emits the date range changed signal provided signals aren't being blocked. """ if (not self.signalsBlocked()): self.dateRangeChanged.emit() def collapseAllTrees(self): self.uiGanttTREE.blockSignals(True) #for x in range(0,self.treeWidget().topLevelItemCount()): #self.treeWidget().topLevelItem(x).setExpanded(True) self.treeWidget().collapseAll() self.uiGanttTREE.blockSignals(False) self.syncView() def expandAllTrees(self): self.uiGanttTREE.blockSignals(True) #for x in range(0,self.treeWidget().topLevelItemCount()): #self.treeWidget().topLevelItem(x).setExpanded(True) self.treeWidget().expandAll() self.uiGanttTREE.blockSignals(False) self.syncView() def eventFilter(self, object, event): if (event.type() == event.Resize): self.__updateViewRect() return False def frameCurrentDate(self): # Subtract start date from current date prerollDays = self._dateStart.day() - QDate.currentDate().day() #set scroll to multiply difference against cel width view_bar = self.uiGanttVIEW.horizontalScrollBar() self._scrolling = True view_bar.setMaximum(1) view_bar.setMinimum(0) #print view_bar.maximum() view_bar.setValue(1) #view_bar.update() #self.update() #self.uiGanttVIEW.update() self._scrolling = False #self.__scrollView(self._cellWidth * prerollDays) def gridPen(self): """ Returns the pen that this widget uses to draw in the view. :return <QPen> """ return self._gridPen @staticmethod def handleDragDrop(object, event): if (event.type() == QEvent.DragEnter): event.acceptProposedActions() elif (event.type() == QEvent.Drop): print 'dropping' def indexOfTopLevelItem(self, item): """ Returns the index for the inputed item from the tree. :return <int> """ return self.treeWidget().indexOfTopLevelItem(item) def insertTopLevelItem(self, index, item): """ Inserts the inputed item at the given index in the tree. :param index | <int> item | <XGanttWidgetItem> """ self.treeWidget().insertTopLevelItem(index, item) item.sync(recursive=True) def setAlternateBrush(self, brush): """ Sets the alternating brush used for this widget to the inputed brush. :param brush | <QBrush> || <QColor> """ self._alternateBrush = QBrush(brush) def setAlternatingRowColors(self, state): """ Sets the alternating row colors state for this widget. :param state | <bool> """ self._alternatingRowColors = state self.treeWidget().setAlternatingRowColors(state) def setBrush(self, brush): """ Sets the main background brush used for this widget to the inputed brush. :param brush | <QBrush> || <QColor> """ self._brush = QBrush(brush) def setCellHeight(self, cellHeight): """ Sets the height for the cells in this gantt's views. :param cellHeight | <int> """ self._cellHeight = cellHeight def setCellWidth(self, cellWidth): """ Sets the width for the cells in this gantt's views. :param cellWidth | <int> """ self._cellWidth = cellWidth def setColumns(self, columns): """ Sets the columns for this gantt widget's tree to the inputed list of columns. :param columns | {<str>, ..] """ self.treeWidget().setColumns(columns) item = self.treeWidget().headerItem() for i in range(item.columnCount()): item.setTextAlignment(i, Qt.AlignBottom | Qt.AlignHCenter) def setDateEnd(self, dateEnd): """ Sets the end date for the range of this gantt widget. :param dateEnd | <QDate> """ self._dateEnd = dateEnd self.emitDateRangeChanged() def setDateFormat(self, format): """ Sets the date format that will be used when rendering in the views. :return <str> """ return self._dateFormat def setDateStart(self, dateStart): """ Sets the start date for the range of this gantt widget. :param dateStart | <QDate> """ self._dateStart = dateStart self.emitDateRangeChanged() def setGridPen(self, pen): """ Sets the pen used to draw the grid lines for the view. :param pen | <QPen> || <QColor> """ self._gridPen = QPen(pen) def setTimescale(self, timescale): """ Sets the timescale value for this widget to the inputed value. :param timescale | <XGanttWidget.Timescale> """ self._timescale = timescale #def setupUserView(self, privileges, department): #sif department def setWeekendBrush(self, brush): """ Sets the brush to be used when coloring weekend columns. :param brush | <QBrush> || <QColor> """ self._weekendBrush = QBrush(brush) def syncView(self): """ Syncs all the items to the view. """ if (not self.signalsBlocked()): for i in range(self.topLevelItemCount()): item = self.topLevelItem(i) item.syncView(recursive=True) def takeTopLevelItem(self, index): """ Removes the top level item at the inputed index from the widget. :param index | <int> :return <XGanttWidgetItem> || None """ item = self.topLevelItem(index) if (item): self.viewWidget().scene().removeItem(item.viewItem()) self.treeWidget().takeTopLevelItem(index) return item return None def timescale(self): """ Returns the timescale that is being used for this widget. :return <XGanttWidget.Timescale> """ return self._timescale def topLevelItem(self, index): """ Returns the top level item at the inputed index. :return <XGanttWidgetItem> """ return self.treeWidget().topLevelItem(index) def topLevelItemCount(self): """ Returns the number of top level items for this widget. :return <int> """ return self.treeWidget().topLevelItemCount() def treeWidget(self): """ Returns the tree widget for this gantt widget. :return <QTreeWidget> """ return self.uiGanttTREE def updateItemData(self, item, index): """ Updates the item information from the tree. :param item | <XGanttWidgetItem> index | <int> """ value = qt.unwrapVariant(item.data(index, Qt.EditRole)) if type(value) == QDateTime: value = value.date() item.setData(index, Qt.EditRole, qt.wrapVariant(value)) if type(value) == QDate: value = value.toPython() columnName = self.treeWidget().columnOf(index) item.setProperty(columnName, value) item.sync() def updatePhaseVisibility(self, visibility, phaseName=''): if (phaseName != ''): #print ("Changing "+ phaseName + " to : "+ str(visibility)) #iterate through all projects for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) keepVisible = False for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) if (child._dbEntry._phase._name == phaseName): #projectWidgetItem.setHidden(not visibility) child.setHidden(not visibility) if (visibility): keepVisible = True elif (visibility == False and not child.isHidden() and keepVisible == False): keepVisible = True #print child._name #print ("item: " + str(x)) #iterate through all phases if (keepVisible): if (projectWidgetItem.isHidden()): projectWidgetItem.setHidden(False) elif (not visibility): if (not projectWidgetItem.isHidden()): #self.syncView() projectWidgetItem.setHidden(True) #if phase matches, change visibility for phase in sharedDB.myPhases.values(): if (phase._name == phaseName): phase._visible = visibility else: #iterate through all projects #iterate through all phases #change visibility if visibility == False: self.collapseAllTrees() for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) child.setHidden(not visibility) #self.syncView() projectWidgetItem.setHidden(not visibility) for phase in sharedDB.myPhases.values(): phase._visible = visibility #print ("Changing all phases to: "+ str(visibility)) self.syncView() def updateUserVisibility(self, visibility, username=''): if (username != ''): #print ("Changing "+ phaseName + " to : "+ str(visibility)) #iterate through all projects for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) keepProjectVisible = False for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) if (child._dbEntry.type() == "phaseassignment"): print "keeping " + username + " visible." if self.CheckUserAssignmentForUser( child._dbEntry._userAssignments.values(), username): child.setHidden(not visibility) if (visibility): keepProjectVisible = True if (visibility == False and not child.isHidden() and keepProjectVisible == False): keepProjectVisible = True if (keepProjectVisible): if (projectWidgetItem.isHidden()): projectWidgetItem.setHidden(False) elif (not visibility): if (not projectWidgetItem.isHidden()): #self.syncView() for c in range(projectWidgetItem.childCount()): projectWidgetItem.child(c).setHidden(True) projectWidgetItem.setHidden(True) #if phase matches, change visibility for user in sharedDB.myUsers.values(): if (user.name() == username): user._calendarVisibility = visibility else: #iterate through all projects #iterate through all phases #change visibility if visibility == False: self.collapseAllTrees() for x in range(0, self.treeWidget().topLevelItemCount()): projectWidgetItem = self.treeWidget().topLevelItem(x) for c in range(projectWidgetItem.childCount()): child = projectWidgetItem.child(c) child.setHidden(not visibility) #self.syncView() projectWidgetItem.setHidden(not visibility) for phase in sharedDB.myPhases.values(): phase._visible = visibility for user in sharedDB.myUsers.values(): user._calendarVisibility = visibility #print ("Changing all phases to: "+ str(visibility)) self.syncView() def CheckUserAssignmentForUser(self, userAssignments, username): for ua in userAssignments: if ua._hours > 1 and ua.idUsers() is not None: if (sharedDB.myUsers[str(ua.idUsers())].name() == username): return True return False def viewWidget(self): """ Returns the view widget for this gantt widget. :return <QGraphicsView> """ return self.uiGanttVIEW def weekendBrush(self): """ Returns the weekend brush to be used for coloring in weekends. :return <QBrush> """ return self._weekendBrush def bookedBrush(self): """ Returns the booked brush to be used for coloring in booked days. :return <QBrush> """ return self._bookedBrush def unavailableBrush(self): """ Returns the unavailable brush to be used for coloring in unavailable days. :return <QBrush> """ return self._unavailableBrush def underbookedBrush(self): """ Returns the underbookedBrush brush to be used for coloring in underbooked days. :return <QBrush> """ return self._underbookedBrush def overbookedBrush(self): """ Returns the overbookedBrush brush to be used for coloring in overbooked days. :return <QBrush> """ return self._overbookedBrush def unassignedBrush(self): """ Returns the unassignedBrush brush to be used for coloring in unassigned days. :return <QBrush> """ return self._unassignedBrush def showProjectMenu(self, pos): """ Displays the header menu for this tree widget. :param pos | <QPoint> || None """ ''' header = self.header() index = header.logicalIndexAt(pos) self._headerIndex = index # show a pre-set menu if self._headerMenu: menu = self._headerMenu else: menu = self.createHeaderMenu(index) ''' # determine the point to show the menu from #if pos is not None: # point = header.mapToGlobal(pos) #else: index = self.uiGanttTREE.indexAt(pos) typ = "None" if index.isValid(): dbentry = self.uiGanttTREE.itemFromIndex(index)._dbEntry if hasattr(dbentry, '_type'): typ = dbentry._type point = QCursor.pos() #self.headerMenuAboutToShow.emit(menu, index) menu = QtGui.QMenu() if typ == "phaseassignment": statusMenu = menu.addMenu("TEST") elif typ == "project": statusAction = menu.addAction("Open in Project View") statusAction.setData(dbentry.id()) menu.addSeparator() if sharedDB.currentUser._idPrivileges < 2: addPhaseMenu = menu.addMenu("Add Phase") phases = sharedDB.myPhases.values() phases.sort(key=operator.attrgetter('_name')) middleChar = phases[len(phases) / 2]._name[0] AMMenu = addPhaseMenu.addMenu('A - ' + middleChar) NZMenu = addPhaseMenu.addMenu( chr(ord(middleChar) + 1) + ' - Z') for x in range(0, len(phases)): phase = phases[x] if phase._name == "DUE": continue #col = self.column(column) if x < len(phases) / 2 or phase._name[0] == middleChar: action = AMMenu.addAction(phase._name) action.setData("addphase_" + str(phase.id()) + "_" + str(dbentry.id())) else: action = NZMenu.addAction(phase._name) action.setData("addphase_" + str(phase.id()) + "_" + str(dbentry.id())) ''' for phase in sharedDB.myPhases.values(): if phase._name != "DUE": addPhaseAction = addPhaseMenu.addAction(phase._name) addPhaseAction.setData("addphase_"+str(phase.id())+"_"+str(dbentry.id())) ''' if sharedDB.currentUser._idPrivileges == 1: archiveAction = menu.addAction("Archive Project") archiveAction.setData(dbentry.id()) else: if sharedDB.currentUser._idPrivileges < 2: menu.addAction("Create Project") menu.triggered.connect(self.mActions) menu.exec_(point) def mActions(self, action): act = action.text() if act == "Open in Project View": self.loadinprojectview(sharedDB.myProjects[str( action.data().toPyObject())]) #print sharedDB.myProjects[str(projectId)]._name elif act == "Archive Project": sharedDB.myProjects[str(action.data().toPyObject())].setArchived(1) elif act == "Create Project": if not hasattr(sharedDB, 'myCreateProjectWidget'): sharedDB.myCreateProjectWidget = createprojectwidget.CreateProjectWidget( sharedDB.mainWindow) sharedDB.myCreateProjectWidget.setDefaults() sharedDB.myCreateProjectWidget.dockWidget.show() elif "addphase" in str(action.data().toPyObject()): phaseId = str(action.data().toPyObject()).split("_")[1] proj = sharedDB.myProjects[str( action.data().toPyObject()).split("_")[2]] phase = sharedDB.phaseAssignments.PhaseAssignments( _idphases=phaseId, _startdate=proj._startdate, _enddate=proj._startdate, _updated=0) proj.AddPhase(phase) #iterate through shots for for image in proj._images.values(): currentTask = sharedDB.tasks.Tasks( _idphaseassignments=phase._idphaseassignments, _idprojects=proj._idprojects, _idshots=image._idshots, _idphases=phase._idphases, _new=1) currentTask.Save() image._tasks[str(currentTask.id())] = (currentTask) currentTask.Save() for seq in proj._sequences.values(): for shot in seq._shots.values(): currentTask = sharedDB.tasks.Tasks( _idphaseassignments=phase._idphaseassignments, _idprojects=proj._idprojects, _idshots=shot._idshots, _idphases=phase._idphases, _new=1) currentTask.Save() shot._tasks[str(currentTask.id())] = (currentTask) def loadinprojectview(self, project): #print "Loading Project"+self.cellWidget(row,column)._phaseassignment._name sharedDB.mainWindow.centralTabbedWidget.setCurrentIndex(0) sharedDB.myProjectViewWidget._currentProject = project sharedDB.myProjectViewWidget.LoadProjectValues() sharedDB.myProjectViewWidget.projectPartWidget.setCurrentIndex(0)
class XOverlayWizard(XOverlayWidget): WizardButton = enum(BackButton=0, NextButton=1, CommitButton=2, FinishButton=3, CancelButton=4, HelpButton=5, RetryButton=6) WizardButtonColors = { WizardButton.BackButton: '#58A6D6', WizardButton.NextButton: '#58A6D6', WizardButton.CommitButton: '#49B84D', WizardButton.FinishButton: '#369939', WizardButton.CancelButton: '#B0B0B0', WizardButton.HelpButton: '#8CA4E6', WizardButton.RetryButton: '#D93D3D' } currentIdChanged = QtCore.Signal(int) helpRequested = QtCore.Signal() pageAdded = QtCore.Signal(int) pageRemoved = QtCore.Signal(int) def __init__(self, parent=None): super(XOverlayWizard, self).__init__(parent) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) # define custom properties self._fixedPageSize = QtCore.QSize() self._minimumPageSize = QtCore.QSize(0, 0) self._maximumPageSize = QtCore.QSize(10000, 10000) self._currentId = -1 self._startId = -1 self._pages = OrderedDict() self._buttons = {} self._fields = {} self._navigation = [] self._animationSpeed = 350 # create the buttons self._buttons[self.WizardButton.HelpButton] = QtGui.QPushButton( self.tr('Help'), self) self._buttons[self.WizardButton.BackButton] = QtGui.QPushButton( self.tr('Back'), self) self._buttons[self.WizardButton.NextButton] = QtGui.QPushButton( self.tr('Next'), self) self._buttons[self.WizardButton.CommitButton] = QtGui.QPushButton( self.tr('Commit'), self) self._buttons[self.WizardButton.FinishButton] = QtGui.QPushButton( self.tr('Finish'), self) self._buttons[self.WizardButton.CancelButton] = QtGui.QPushButton( self.tr('Cancel'), self) self._buttons[self.WizardButton.RetryButton] = QtGui.QPushButton( self.tr('Retry'), self) # don't show any buttons by default pal = self.palette() for which, btn in self._buttons.items(): pal.setColor(pal.Active, pal.Button, QtGui.QColor(self.WizardButtonColors[which])) pal.setColor(pal.Active, pal.ButtonText, QtGui.QColor('white')) pal.setColor(pal.Inactive, pal.Button, QtGui.QColor(self.WizardButtonColors[which])) pal.setColor(pal.Inactive, pal.ButtonText, QtGui.QColor('white')) btn.setPalette(pal) btn.setFixedSize(QtCore.QSize(120, 30)) btn.hide() # create connections self._buttons[self.WizardButton.HelpButton].clicked.connect( self.helpRequested) self._buttons[self.WizardButton.BackButton].clicked.connect(self.back) self._buttons[self.WizardButton.NextButton].clicked.connect(self.next) self._buttons[self.WizardButton.CommitButton].clicked.connect( self.commit) self._buttons[self.WizardButton.FinishButton].clicked.connect( self.accept) self._buttons[self.WizardButton.CancelButton].clicked.connect( self.reject) self._buttons[self.WizardButton.RetryButton].clicked.connect( self.retry) def accept(self): if not self.currentPage() or self.currentPage().validatePage(): return super(XOverlayWizard, self).accept() return False def addPage(self, page): """ Adds a new overlay wizard page to this wizard. :param page | <projexui.widgets.xoverlaywizard.XOverlayWizardPage> """ self.setPage(len(self._pages), page) def adjustSize(self): """ Adjusts the size of this wizard and its page contents to the inputed size. :param size | <QtCore.QSize> """ super(XOverlayWizard, self).adjustSize() # adjust the page size page_size = self.pageSize() # resize the current page to the inputed size x = (self.width() - page_size.width()) / 2 y = (self.height() - page_size.height()) / 2 btn_height = max([btn.height() for btn in self._buttons.values()]) curr_page = self.currentPage() for page in self._pages.values(): page.resize(page_size.width(), page_size.height() - btn_height - 6) if page == curr_page: page.move(x, y) else: page.move(self.width(), y) # move the left most buttons y += page_size.height() - btn_height left_btns = (self._buttons[self.WizardButton.HelpButton], self._buttons[self.WizardButton.BackButton]) for btn in left_btns: if btn.isVisible(): btn.move(x, y) btn.raise_() x += btn.width() + 6 # move the right buttons x = self.width() - (self.width() - page_size.width()) / 2 for key in reversed(sorted(self._buttons.keys())): btn = self._buttons[key] if not btn.isVisible() or btn in left_btns: continue btn.move(x - btn.width(), y) btn.raise_() x -= btn.width() + 6 def animationSpeed(self): """ Returns the speed that the pages should animate in/out in milliseconds. :return <int> """ return self._animationSpeed def back(self): """ Goes to the previous page for this wizard. """ try: pageId = self._navigation[-2] last_page = self.page(pageId) except IndexError: return curr_page = self.page(self._navigation.pop()) if not (last_page and curr_page): return self._currentId = pageId y = curr_page.y() last_page.move(-last_page.width(), y) last_page.show() # animate the last page in anim_in = QtCore.QPropertyAnimation(self) anim_in.setTargetObject(last_page) anim_in.setPropertyName('pos') anim_in.setStartValue(last_page.pos()) anim_in.setEndValue(curr_page.pos()) anim_in.setDuration(self.animationSpeed()) anim_in.setEasingCurve(QtCore.QEasingCurve.Linear) # animate the current page out anim_out = QtCore.QPropertyAnimation(self) anim_out.setTargetObject(curr_page) anim_out.setPropertyName('pos') anim_out.setStartValue(curr_page.pos()) anim_out.setEndValue(QtCore.QPoint(self.width() + curr_page.width(), y)) anim_out.setDuration(self.animationSpeed()) anim_out.setEasingCurve(QtCore.QEasingCurve.Linear) # create the anim group anim_grp = QtCore.QParallelAnimationGroup(self) anim_grp.addAnimation(anim_in) anim_grp.addAnimation(anim_out) anim_grp.finished.connect(curr_page.hide) anim_grp.finished.connect(anim_grp.deleteLater) # update the button states self._buttons[self.WizardButton.BackButton].setVisible( self.canGoBack()) self._buttons[self.WizardButton.NextButton].setVisible(True) self._buttons[self.WizardButton.RetryButton].setVisible( self.canRetry()) self._buttons[self.WizardButton.CommitButton].setVisible( last_page.isCommitPage()) self._buttons[self.WizardButton.FinishButton].setVisible( last_page.isFinalPage()) self.adjustSize() self.currentIdChanged.emit(pageId) anim_grp.start() def button(self, which): """ Returns the button associated with the inputed wizard button. :param which | <XOverlayWizard.WizardButton> :return <bool> || None """ return self._buttons.get(which) def buttonText(self, which): """ Returns the button text for a given wizard button. :param which | <XOverlayWizard.WizardButton> """ try: return self._buttons[which].text() except KeyError: return '' def canGoBack(self): """ Returns whether or not this wizard can move forward. :return <bool> """ try: backId = self._navigation.index(self.currentId()) - 1 if backId >= 0: self._navigation[backId] else: return False except StandardError: return False else: return True def canGoForward(self): """ Returns whether or not this wizard can move forward. :return <bool> """ try: return not self.currentPage().isFinalPage() except AttributeError: return False def canRetry(self): """ Returns whether or not this wizard can retry the current page. :return <bool> """ try: return self.currentPage().retryEnabled() except AttributeError: return False def commit(self): """ Commits the current page information. """ try: self.currentPage().commit() except AttributeError: pass def currentId(self): """ Returns the page ID of the current page that this wizard is on. :return <int> """ return self._currentId def currentPage(self): """ Returns the current page for this wizard. :return <projexui.widgets.xoverlaywizard.XOverlayWizardPage> || None """ try: return self._pages[self.currentId()] except KeyError: return None def field(self, name, default=None): """ Returns the value for the inputed field property for this wizard. :param name | <str> default | <variant> :return <variant> """ return self._fields.get(name, default) def fixedPageSize(self): """ Returns the fixed page size for this wizard's contents. :return <QtCore.QSize> """ return self._fixedPageSize def hasVisitedPage(self, pageId): """ Returns whether or not the user has seen the inputed page id. :return <bool> """ return False def minimumPageSize(self): """ Returns the minimum page size for this wizard's contents. :return <QtCore.QSize> """ return self._minimumPageSize def maximumPageSize(self): """ Returns the maximum page size for this wizard's contents. :return <QtCore.QSize> """ return self._maximumPageSize def next(self): """ Goes to the previous page for this wizard. """ curr_page = self.currentPage() if not curr_page: return elif not curr_page.validatePage(): return pageId = curr_page.nextId() try: next_page = self._pages[pageId] except KeyError: return self._currentId = pageId self._navigation.append(pageId) y = curr_page.y() next_page.move(self.width(), y) # animate the last page in anim_in = QtCore.QPropertyAnimation(self) anim_in.setTargetObject(curr_page) anim_in.setPropertyName('pos') anim_in.setStartValue(curr_page.pos()) anim_in.setEndValue(QtCore.QPoint(-curr_page.width(), y)) anim_in.setDuration(self.animationSpeed()) anim_in.setEasingCurve(QtCore.QEasingCurve.Linear) # animate the current page out anim_out = QtCore.QPropertyAnimation(self) anim_out.setTargetObject(next_page) anim_out.setPropertyName('pos') anim_out.setStartValue(next_page.pos()) anim_out.setEndValue(curr_page.pos()) anim_out.setDuration(self.animationSpeed()) anim_out.setEasingCurve(QtCore.QEasingCurve.Linear) # create the anim group anim_grp = QtCore.QParallelAnimationGroup(self) anim_grp.addAnimation(anim_in) anim_grp.addAnimation(anim_out) anim_grp.finished.connect(curr_page.hide) anim_grp.finished.connect(anim_grp.deleteLater) next_page.show() # update the button states self._buttons[self.WizardButton.BackButton].setVisible(True) self._buttons[self.WizardButton.NextButton].setVisible( self.canGoForward()) self._buttons[self.WizardButton.RetryButton].setVisible( self.canRetry()) self._buttons[self.WizardButton.CommitButton].setVisible( next_page.isCommitPage()) self._buttons[self.WizardButton.FinishButton].setVisible( next_page.isFinalPage()) self.adjustSize() # initialize the new page self.currentIdChanged.emit(pageId) next_page.initializePage() anim_grp.start() def page(self, pageId): """ Returns the page at the inputed id. :return <projexui.widgets.xoverlaywizard.XOverlayWizardPage> || None """ return self._pages.get(pageId) def pageCount(self): """ Returns the number of pages associated with this wizard. :return <int> """ return len(self._pages) def pageIds(self): """ Returns a list of all the page ids for this wizard. :return [<int>, ..] """ return self._pages.keys() def pageSize(self): """ Returns the current page size for this wizard. :return <QtCore.QSize> """ # update the size based on the new size page_size = self.fixedPageSize() if page_size.isEmpty(): w = self.width() - 80 h = self.height() - 80 min_w = self.minimumPageSize().width() min_h = self.minimumPageSize().height() max_w = self.maximumPageSize().width() max_h = self.maximumPageSize().height() page_size = QtCore.QSize(min(max(min_w, w), max_w), min(max(min_h, h), max_h)) return page_size def exec_(self): """ Executes this wizard within the system. """ self.show() self.adjustSize() self.restart() def restart(self): """ Restarts the whole wizard from the beginning. """ # hide all of the pages for page in self._pages.values(): page.hide() pageId = self.startId() try: first_page = self._pages[pageId] except KeyError: return self._currentId = pageId self._navigation = [pageId] page_size = self.pageSize() x = (self.width() - page_size.width()) / 2 y = (self.height() - page_size.height()) / 2 first_page.move(self.width() + first_page.width(), y) first_page.show() # animate the current page out anim_out = QtCore.QPropertyAnimation(self) anim_out.setTargetObject(first_page) anim_out.setPropertyName('pos') anim_out.setStartValue(first_page.pos()) anim_out.setEndValue(QtCore.QPoint(x, y)) anim_out.setDuration(self.animationSpeed()) anim_out.setEasingCurve(QtCore.QEasingCurve.Linear) anim_out.finished.connect(anim_out.deleteLater) # update the button states self._buttons[self.WizardButton.BackButton].setVisible(False) self._buttons[self.WizardButton.NextButton].setVisible( self.canGoForward()) self._buttons[self.WizardButton.CommitButton].setVisible( first_page.isCommitPage()) self._buttons[self.WizardButton.FinishButton].setVisible( first_page.isFinalPage()) self.adjustSize() first_page.initializePage() self.currentIdChanged.emit(pageId) anim_out.start() def removePage(self, pageId): """ Removes the inputed page from this wizard. :param pageId | <int> """ try: self._pages[pageId].deleteLater() del self._pages[pageId] except KeyError: pass def retry(self): """ Reruns the current page operation. """ try: self.currentPage().initializePage() except AttributeError: pass def setAnimationSpeed(self, speed): """ Sets the speed that the pages should animate in/out in milliseconds. :param speed | <int> | milliseconds """ self._animationSpeed = speed def setButton(self, which, button): """ Sets the button for this wizard for the inputed type. :param which | <XOverlayWizard.WizardButton> button | <QtGui.QPushButton> """ try: self._buttons[which].deleteLater() except KeyError: pass button.setParent(self) self._buttons[which] = button def setButtonText(self, which, text): """ Sets the display text for the inputed button to the given text. :param which | <XOverlayWizard.WizardButton> text | <str> """ try: self._buttons[which].setText(text) except KeyError: pass def setField(self, name, value): """ Sets the field value for this wizard to the given value. :param name | <str> value | <variant> """ self._fields[name] = value def setFixedPageSize(self, size): """ Sets the page size for the wizard. This will define the width/height for the contents for this wizards page widgets as well as the bounding information for the buttons. If an empty size is given, then the size will automatically be recalculated as the widget sizes. :param size | <QtCore.QSize> """ self._fixedPageSize = size self.adjustSize() def setMinimumPageSize(self, size): """ Sets the page size for the wizard. This will define the width/height for the contents for this wizards page widgets as well as the bounding information for the buttons. If an empty size is given, then the size will automatically be recalculated as the widget sizes. :param size | <QtCore.QSize> """ self._minimumPageSize = size self.adjustSize() def setMaximumPageSize(self, size): """ Sets the page size for the wizard. This will define the width/height for the contents for this wizards page widgets as well as the bounding information for the buttons. If an empty size is given, then the size will automatically be recalculated as the widget sizes. :param size | <QtCore.QSize> """ self._maximumPageSize = size self.adjustSize() def setPage(self, pageId, page): """ Sets the page and id for the given page vs. auto-constructing it. This will allow the developer to create a custom order for IDs. :param pageId | <int> page | <projexui.widgets.xoverlaywizard.XOverlayWizardPage> """ page.setParent(self) if self.property("useShadow") is not False: # create the drop shadow effect effect = QtGui.QGraphicsDropShadowEffect(page) effect.setColor(QtGui.QColor('black')) effect.setBlurRadius(50) effect.setOffset(0, 0) page.setGraphicsEffect(effect) self._pages[pageId] = page if self._startId == -1: self._startId = pageId def setStartId(self, pageId): """ Sets the starting page id for this wizard to the inputed page id. :param pageId | <int> """ self._startId = pageId def startId(self): """ Returns the starting id for this wizard. :return <int> """ return self._startId
ColumnType = enum( # simple types 'Bool', 'Decimal', 'Double', 'Integer', # 32-bit integer 'BigInt', # 64-bit integer 'Enum', # equates to an integer in databases, but links to an enum # string types 'String', # used for limited char sets 'Text', # used for larger string data 'Url', # similar to String, but uses url regex validation 'Email', # similar to String, but uses email regex validation 'Password', # similar to String, but uses password regex validation 'Filepath', # similar to String, but uses filepath regex validation 'Directory', # similar to String, but uses directory regex validation 'Xml', # similar to Text, but auto-escape data stored 'Html', # similar to Text, but store rich text data & auto-escape 'Color', # similar to String, stores the HEX value of a color (#ff00) # date/time types 'Datetime', 'Date', 'Interval', 'Time', 'DatetimeWithTimezone', 'Timestamp', 'Timestamp_UTC', # data types 'Image', # stores images in the database as binary 'ByteArray', # stores additional binary information 'Dict', # stores python dictionary types 'Pickle', # stores python pickle data 'Yaml', # stores python data as yaml (requires PyYaml) 'JSON', # stores python data as JSON 'Query', # stores an orb.Query class as xml # relation types 'ForeignKey', # tells the system to use the relation's information )
__maintainer__ = 'Projex Software, LLC' __email__ = '*****@*****.**' import datetime import projex.text from projex.enum import enum # define global enumerations RepeatFlags = enum( # Weekly Flags 'EveryMonday', 'EveryTuesday', 'EveryWednesday', 'EveryThursday', 'EveryFriday', 'EverySaturday', 'EverySunday', # Repeating Flags 'DayOfTheMonth', 'DayOfTheWeek') RepeatMode = enum( Weekly = 2, # removed the 'Daily' key, was 1 Monthly = 4, Yearly = 8) Names = enum( # default 'Sometime', # Preset Names 'Today',
class Column(AddonManager): """ Used to define database schema columns when defining Table classes. """ TypeMap = {} MathMap = { 'Default': { 'Add': u'{field} + {value}', 'Subtract': u'{field} - {value}', 'Multiply': u'{field} * {value}', 'Divide': u'{field} / {value}', 'And': u'{field} & {value}', 'Or': u'{field} | {value}' } } Flags = enum( 'ReadOnly', 'Polymorphic', 'AutoAssign', 'Required', 'Unique', 'Encrypted', 'Searchable', 'I18n', 'I18n_NoDefault', 'CaseSensitive', 'Virtual', 'Static', 'Protected', 'Private', 'AutoExpand', 'RequiresExpand', 'Keyable' ) def __json__(self): output = { 'type': self.addonName(), 'name': self.name(), 'field': self.field(), 'display': self.display(), 'flags': {k: True for k in self.Flags.toSet(self.__flags)}, 'default': self.default() } return output def __init__(self, name=None, field=None, display=None, getterName=None, setterName=None, flags=0, default=None, defaultOrder='asc', shortcut='', readPermit=None, writePermit=None, permit=None, getter=None, setter=None, queryFilter=None, schema=None, order=99999): # constructor items self.__name = name self.__field = field self.__display = display self.__flags = self.Flags.fromSet(flags) if isinstance(flags, set) else flags self.__default = default self.__defaultOrder = defaultOrder self.__getterName = getterName self.__setterName = setterName self.__shortcut = shortcut self.__query_filter = queryFilter self.__settermethod = setter self.__gettermethod = getter self.__readPermit = readPermit or permit self.__writePermit = writePermit or permit self.__order = order # custom options self.__schema = None self.__timezone = None if shortcut: self.setFlag(self.Flags.Virtual) # auto-register to the schema if provided if schema: schema.register(self) def copy(self): """ Returns a new instance copy of this column. :return: <orb.Column> """ out = type(self)( name=self.__name, field=self.__field, display=self.__display, flags=self.__flags, default=self.__default, defaultOrder=self.__defaultOrder, getterName=self.__getterName, setterName=self.__setterName, getter=self.__gettermethod, setter=self.__settermethod, queryFilter=self.__query_filter, shortcut=self.__shortcut, readPermit=self.__readPermit, writePermit=self.__writePermit, order=self.__order ) return out def dbRestore(self, db_value, context=None): """ Converts a stored database value to Python. :param typ: <str> :param py_value: <variant> :param context: <orb.Context> :return: <variant> """ # restore translatable column if self.testFlag(self.Flags.I18n): if isinstance(db_value, (str, unicode)): if db_value.startswith('{'): try: value = projex.text.safe_eval(db_value) except StandardError: value = {context.locale: db_value} else: value = {context.locale: db_value} else: value = db_value return value else: return db_value def dbMath(self, typ, field, op, value): """ Performs some database math on the given field. This will be database specific implementations and should return the resulting database operation. :param field: <str> :param op: <orb.Query.Math> :param target: <variant> :param context: <orb.Context> || None :return: <str> """ ops = orb.Query.Math(op) format = self.MathMap.get(typ, {}).get(ops) or self.MathMap.get('Default').get(ops) or '{field}' return format.format(field=field, value=value) def dbStore(self, typ, py_value): """ Prepares to store this column for the a particular backend database. :param backend: <orb.Database> :param py_value: <variant> :param context: <orb.Context> :return: <variant> """ # convert base types to work in the database if isinstance(py_value, (list, tuple, set)): py_value = tuple((self.dbStore(x) for x in py_value)) elif isinstance(py_value, orb.Collection): py_value = py_value.ids() elif isinstance(py_value, orb.Model): py_value = py_value.id() return py_value def dbType(self, typ): """ Returns the database object type based on the given connection type. :param typ: <str> :return: <str> """ return self.TypeMap.get(typ, self.TypeMap.get('Default')) def default(self): """ Returns the default value for this column to return when generating new instances. :return <variant> """ if isinstance(self.__default, (str, unicode)): return self.valueFromString(self.__default) else: return self.__default def defaultOrder(self): """ Returns the default ordering for this column when sorting. :return <str> """ return self.__defaultOrder def display(self): """ Returns the display text for this column. :return <str> """ return self.__display or orb.system.syntax().display(self.__name) def field(self): """ Returns the field name that this column will have inside the database. :return <str> """ return self.__field or orb.system.syntax().field(self.__name, isinstance(self, orb.ReferenceColumn)) def firstMemberSchema(self, schemas): """ Returns the first schema within the list that this column is a member of. :param schemas | [<orb.TableSchema>, ..] :return <orb.TableSchema> || None """ for schema in schemas: if schema.hasColumn(self): return schema return self.schema() def flags(self): """ Returns the flags that have been set for this column. :return <Column.Flags> """ return self.__flags def getter(self): def wrapped(func): self.__gettermethod = func return func return wrapped def getterName(self): return self.__getterName or orb.system.syntax().getterName(self.__name) def gettermethod(self): return self.__gettermethod def isMemberOf(self, schemas): """ Returns whether or not this column is a member of any of the given schemas. :param schemas | [<orb.TableSchema>, ..] || <orb.TableSchema> :return <bool> """ if type(schemas) not in (tuple, list, set): schemas = (schemas,) for schema in schemas: if schema.hasColumn(self): return True return False def isNull(self, value): """ Returns whether or not the given value is considered null for this column. :param value: <variant> :return: <bool> """ return value is None def loadJSON(self, jdata): """ Initializes the information for this class from the given JSON data blob. :param jdata: <dict> """ # required params self.__name = jdata['name'] self.__field = jdata['field'] # optional fields self.__display = jdata.get('display') or self.__display self.__flags = jdata.get('flags') or self.__flags self.__defaultOrder = jdata.get('defaultOrder') or self.__defaultOrder self.__default = jdata.get('default') or self.__default def memberOf(self, schemas): """ Returns a list of schemas this column is a member of from the inputted list. :param schemas | [<orb.TableSchema>, ..] :return [<orb.TableSchema>, ..] """ for schema in schemas: if schema.hasColumn(self): yield schema def name(self): """ Returns the accessor name that will be used when referencing this column around the app. :return <str> """ return self.__name def order(self): """ Returns the priority order for this column. Lower number priorities will be processed first. :return: <int> """ return self.__order def queryFilter(self, function=None): """ Defines a decorator that can be used to filter queries. It will assume the function being associated with the decorator will take a query as an input and return a modified query to use. :usage class MyModel(orb.Model): objects = orb.ReverseLookup('Object') @classmethod @objects.queryFilter() def objectsFilter(cls, query, **context): return orb.Query() :param function: <callable> :return: <wrapper> """ if function is not None: self.__query_filter = function return function def wrapper(func): self.__query_filter = func return func return wrapper def queryFilterMethod(self): """ Returns the actual query filter method, if any, that is associated with this collector. :return: <callable> """ return self.__query_filter def random(self): """ Returns a random value that fits this column's parameters. :return: <variant> """ return self.default() def readPermit(self): """ Returns the read permit for this column. :return: <str> || None """ return self.__readPermit def restore(self, value, context=None, inflated=True): """ Restores the value from a table cache for usage. :param value | <variant> context | <orb.Context> || None """ context = context or orb.Context() # check to see if this column is translatable before restoring if self.testFlag(self.Flags.I18n): locales = context.locale.split(',') if not isinstance(value, dict): default_locale = locales[0] if default_locale == 'all': default_locale = orb.system.settings().default_locale value = {default_locale: value} if 'all' in locales: return value if len(locales) == 1: return value.get(locales[0]) else: return {locale: value.get(locale) for locale in locales} else: return value def setShortcut(self, shortcut): """ Sets the shortcut information for this column. :param shortcut: <str> """ self.__shortcut = shortcut def shortcut(self): """ Returns the shortcut path that will be taken through this column. :return: <str> || None """ return self.__shortcut def store(self, value, context=None): """ Converts the value to one that is safe to store on a record within the record values dictionary :param value | <variant> :return <variant> """ if isinstance(value, (str, unicode)): value = self.valueFromString(value) # store the internationalized property if self.testFlag(self.Flags.I18n): if not isinstance(value, dict): context = context or orb.Context() return {context.locale: value} else: return value else: return value def schema(self): """ Returns the table that this column is linked to in the database. :return <TableSchema> """ return self.__schema def setterName(self): return self.__setterName or orb.system.syntax().setterName(self.__name) def settermethod(self): return self.__settermethod def setter(self): def wrapped(func): self.__settermethod = func return func return wrapped def setDefault(self, default): """ Sets the default value for this column to the inputted value. :param default | <str> """ self.__default = default def setDisplay(self, display): """ Sets the display name for this column. :param displayName | <str> """ self.__display = display def setField(self, field): """ Sets the field name for this column. :param field | <str> """ self.__field = field def setFlag(self, flag, state=True): """ Sets whether or not this flag should be on. :param flag | <Column.Flags> state | <bool> """ if state: self.__flags |= flag else: self.__flags &= ~flag def setFlags(self, flags): """ Sets the global flags for this column to the inputted flags. :param flags | <Column.Flags> """ self.__flags = flags def setName(self, name): """ Sets the name of this column to the inputted name. :param name | <str> """ self.__name = name def setReadPermit(self, permit): """ Sets the read permission for this column. :param permit: <str> || None """ self.__readPermit = permit def setSchema(self, schema): self.__schema = schema def setGetterName(self, getterName): self.__getterName = getterName def setOrder(self, order): """ Sets the priority order for this column. Lower orders will be processed first during updates (in case one column should be set before another). :param order: <int> """ self.__order = order def setSetterName(self, setterName): self.__setterName = setterName def setWritePermit(self, permit): """ Sets the write permission for this column. :param permit: <str> || None """ self.__writePermit = permit def testFlag(self, flag): """ Tests to see if this column has the inputted flag set. :param flag | <Column.Flags> """ if isinstance(flag, (str, unicode)): flag = self.Flags(flag) return bool(self.flags() & flag) if flag >= 0 else not bool(self.flags() & ~flag) def validate(self, value): """ Validates the inputted value against this columns rules. If the inputted value does not pass, then a validation error will be raised. Override this method in column sub-classes for more specialized validation. :param value | <variant> :return <bool> success """ # check for the required flag if self.testFlag(self.Flags.Required) and not self.testFlag(self.Flags.AutoAssign): if self.isNull(value): msg = '{0} is a required column.'.format(self.name()) raise orb.errors.ColumnValidationError(self, msg) # otherwise, we're good return True def valueFromString(self, value, context=None): """ Converts the inputted string text to a value that matches the type from this column type. :param value | <str> """ return projex.text.nativestring(value) def valueToString(self, value, context=None): """ Converts the inputted string text to a value that matches the type from this column type. :sa engine :param value | <str> extra | <variant> """ return projex.text.nativestring(value) def writePermit(self): """ Returns the permit required to modify a particular column. :return: <str> || None """ return self.__writePermit @classmethod def fromJSON(cls, jdata): """ Generates a new column from the given json data. This should be already loaded into a Python dictionary, not a JSON string. :param jdata | <dict> :return <orb.Column> || None """ cls_type = jdata.get('type') col_cls = cls.byName(cls_type) if not col_cls: raise orb.errors.InvalidColumnType(cls_type) else: col = col_cls() col.loadJSON(jdata) return col
class XChartRuler(object): Type = enum('Number', 'Date', 'Datetime', 'Time', 'Monthly', 'Custom') def __init__(self, rulerType): # define custom properties self._rulerType = rulerType self._minimum = None self._maximum = None self._step = None self._notches = None self._format = None self._title = '' self._padStart = 0 self._padEnd = 0 self._notchPadding = 6 self.setRulerType(rulerType) def calcTotal(self, values): """ Calculates the total for the given values. For Datetime/Time objects, this will be seconds from minimum to maximum - defaulting to 1 second. For Date objects, this will be days from minimum to maximum, defaulting to 1 day. For Number objects, this will be the sum of the list. For Custom objects, a -1 value is returned. :param values | [<variant>, ..] :return <int> """ rtype = self.rulerType() if (not values): return 0 if (rtype == XChartRuler.Type.Number): return sum(values) elif (rtype == XChartRuler.Type.Date): values.sort() days = values[0].daysTo(values[-1]) return max(1, days) elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)): values.sort() secs = values[0].secsTo(values[-1]) return max(1, secs) return -1 def clear(self): """ Clears all the cached information about this ruler. """ self._minimum = None self._maximum = None self._step = None self._notches = None self._format = None self._formatter = None self._padEnd = 0 self._padStart = 0 def compareValues(self, a, b): """ Compares two values based on the notches and values for this ruler. :param a | <variant> b | <variant> :return <int> 1 || 0 || -1 """ if (self.rulerType() in (XChartRuler.Type.Custom, XChartRuler.Type.Monthly)): try: aidx = self._notches.index(a) except ValueError: return -1 try: bidx = self._notches.index(b) except ValueError: return 1 return cmp(aidx, bidx) return cmp(a, b) def format(self): """ Returns the format that will be used for this ruler. :return <str> """ if (self._format is not None): return self._format rtype = self.rulerType() if (rtype == XChartRuler.Type.Number): self._format = '%i' elif (rtype == XChartRuler.Type.Date): self._format = 'M.d.yy' elif (rtype == XChartRuler.Type.Datetime): self._format = 'M.d.yy @ h:mm ap' elif (rtype == XChartRuler.Type.Time): self._format = 'h:mm ap' return self._format def formatter(self): """ Returns the formatter callable that is linked with this ruler for formatting the value for a notch to a string. :return <callable> """ return self._formatter def formatValue(self, value): """ Formats the inputed value based on the formatting system for this ruler. :param value | <variant> """ formatter = self.formatter() if (formatter is not None): return formatter(value) elif (self.format()): rtype = self.rulerType() if (rtype in (XChartRuler.Type.Date, XChartRuler.Type.Datetime, XChartRuler.Type.Time)): return value.toString(self.format()) else: try: return self.format() % value except TypeError: return nativestring(value) return nativestring(value) def maxNotchSize(self, orientation): """ Returns the maximum size for this ruler based on its notches and the given orientation. :param orientation | <Qt.Orientation> :return <int> """ metrics = QFontMetrics(QApplication.font()) if orientation == Qt.Vertical: notch = '' for n in self.notches(): if len(nativestring(n)) > len(nativestring(notch)): notch = nativestring(n) return metrics.width(notch) else: return metrics.height() def minLength(self, orientation): """ Returns the minimum length for this ruler based on its notches and the given orientation. :param orientation | <Qt.Orientation> :return <int> """ padding = self.padStart() + self.padEnd() count = len(self.notches()) notch_padding = self.notchPadding() if (orientation == Qt.Horizontal): section = self.maxNotchSize(Qt.Vertical) else: section = self.maxNotchSize(Qt.Horizontal) return notch_padding * count + section * count + padding def maximum(self): """ Returns the maximum value for this ruler. If the cached value is None, then a default value will be specified based on the ruler type. :return <variant> """ if (self._maximum is not None): return self._maximum rtype = self.rulerType() if (rtype == XChartRuler.Type.Number): self._maximum = 100 elif (rtype == XChartRuler.Type.Date): self._maximum = QDate.currentDate().addDays(10) elif (rtype == XChartRuler.Type.Datetime): self._maximum = QDateTime.currentDateTime().addDays(10) elif (rtype == XChartRuler.Type.Time): self._maximum = QDateTime.currentDateTime().time().addSecs(60 * 60) else: notches = self.notches() if (notches): self._maximum = notches[-1] return self._maximum def minimum(self): """ Returns the minimum value for this ruler. If the cached value is None, then a default value will be specified based on the ruler type. :return <variant> """ if (self._minimum is not None): return self._minimum rtype = self.rulerType() if (rtype == XChartRuler.Type.Number): self._minimum = 0 elif (rtype == XChartRuler.Type.Date): self._minimum = QDate.currentDate() elif (rtype == XChartRuler.Type.Datetime): self._minimum = QDateTime.currentDateTime() elif (rtype == XChartRuler.Type.Time): self._minimum = QDateTime.currentDateTime().time() else: notches = self.notches() if (notches): self._minimum = notches[0] return self._minimum def notches(self): """ Reutrns a list of the notches that are going to be used for this ruler. If the notches have not been explicitly set (per a Custom type), then the notches will be generated based on the minimum, maximum and step values the current ruler type. :return [<str>, ..] """ if (self._notches is not None): return self._notches rtype = self.rulerType() formatter = self.formatter() format = self.format() self._notches = [] minimum = self.minimum() maximum = self.maximum() step = self.step() if (step <= 0): return [] curr = minimum while (curr < maximum): self._notches.append(self.formatValue(curr)) if (rtype == XChartRuler.Type.Number): curr += step elif (rtype == XChartRuler.Type.Date): curr = curr.addDays(step) elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)): curr = curr.addSecs(step) else: break self._notches.append(self.formatValue(maximum)) return self._notches def notchPadding(self): """ Returns the minimum padding amount used between notches. :return <int> """ return self._notchPadding def padEnd(self): """ Returns the number of units to use as padding for the end of this ruler. :return <int> """ return self._padEnd def padStart(self): """ Returns the number of units to use as padding for the beginning of this ruler. :return <int> """ return self._padStart def percentAt(self, value): """ Returns the percentage where the given value lies between this rulers minimum and maximum values. If the value equals the minimum, then the percent is 0, if it equals the maximum, then the percent is 1 - any value between will be a floating point. If the ruler is a custom type, then only if the value matches a notch will be successful. :param value | <variant> :return <float> """ if (value is None): return 0.0 minim = self.minimum() maxim = self.maximum() rtype = self.rulerType() # simple minimum if (value == minim and not self.padStart()): perc = 0.0 # simple maximum elif (value == maxim and not self.padEnd()): perc = 1.0 # calculate a numeric percentage value elif (rtype == XChartRuler.Type.Number): perc = float(value - minim) / float(maxim - minim) # calculate a time percentage value elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)): maxsecs = minim.secsTo(maxim) valsecs = minim.secsTo(value) perc = float(valsecs) / maxsecs # calculate a date percentage value elif (rtype == XChartRuler.Type.Date): maxdays = minim.daysTo(maxim) valdays = minim.daysTo(value) perc = float(valdays) / maxdays # otherwise, compare against the notches else: perc = 0.0 notches = self.notches() count = len(notches) count += self.padStart() + self.padEnd() count = max(1, count - 1) perc = float(self.padStart()) / count for i, notch in enumerate(notches): if (notch == value): perc += float(i) / count break # normalize the percentage perc = min(perc, 1.0) perc = max(0, perc) return perc def rulerType(self): """ Returns the ruler type for this ruler. :reutrn <XChartRuler.Type> """ return self._rulerType def setFormat(self, format): """ Sets the string format that will be used when processing the ruler notches. For a Number type, this will be used as a standard string format in Python (%i, $%0.02f, etc.). For Datetime, Date, and Time types, it will be used with the QDate/QDateTime/QTime toString method. For Custom types, notches should be set manually. """ self._format = format def setFormatter(self, formatter): """ Sets the formatter method or callable that will accept one of the values for this ruler to render out the text display for the value. :param formatter | <callable> """ self._formatter = formatter def setMaximum(self, maximum): """ Sets the maximum value for this ruler to the inputed value. :param maximum | <variant> """ self._maximum = maximum self._notches = None def setMinimum(self, minimum): """ Sets the minimum value for this ruler to the inputed value. :param minimum | <variant> """ self._minimum = minimum self._notches = None def setNotches(self, notches): """ Manually sets the notches list for this ruler to the inputed notches. :param notches | [<str>, ..] || None """ self._rulerType = XChartRuler.Type.Custom self._notches = notches def setNotchPadding(self, padding): """ Sets the minimum padding amount used between notches. :param padding | <int> """ self._notchPadding = padding def setPadEnd(self, amount): """ Sets the number of units to use as padding for the end of this ruler. :param amount | <int> """ self._padEnd = amount def setPadStart(self, amount): """ Sets the number of units to use as padding for the beginning of this ruler. :param amount | <int> """ self._padStart = amount def setRulerType(self, rulerType): """ Sets the ruler type for this ruler to the inputed type. :param rulerType | <XChartRuler.Type> """ self._rulerType = rulerType self.clear() # handle custom types if (rulerType == XChartRuler.Type.Monthly): self.setNotches([ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]) def setStep(self, step): """ Sets the step value for this ruler to the inputed value. If the ruler type is Number, then an int or float value is acceptable. If the type is Datetime or Date, then the step value is an integer representing days. If the type is Time, then the step is an integer representing seconds. :param step | <int> || <float> """ self._step = int(step) self._notches = None def setTitle(self, title): """ Sets the title for this ruler to the inputed title. :param title | <str> """ self._title = title def step(self): """ Returns the step value for this ruler. If the cached value is None, then a default value will be specified based on the ruler type. :return <variant> """ if (self._step is not None): return self._step elif (self.rulerType() == XChartRuler.Type.Number): self._step = int((self.maximum() - self.minimum()) / 10.0) elif (self.rulerType() == XChartRuler.Type.Date): self._step = int(self.minimum().daysTo(self.maximum()) / 10.0) - 1 elif ( self.rulerType() & (XChartRuler.Type.Time | \ XChartRuler.Type.Datetime) ): self._step = int(self.minimum().secsTo(self.maximum()) / 10.0) - 1 return self._step def title(self): """ Returns the title for this ruler. :return <str> """ return self._title def valueAt(self, percent): """ Returns the value at the inputed percent. :param percent | <float> :return <variant> """ minim = self.minimum() maxim = self.maximum() rtype = self.rulerType() # simple minimum if (percent <= 0): return minim # simple maximum elif (1 <= percent): return maxim # calculate a numeric percentage value elif (rtype == XChartRuler.Type.Number): return (maxim - minim) * percent # calculate a time percentage value elif (rtype in (XChartRuler.Type.Datetime, XChartRuler.Type.Time)): maxsecs = minim.secsTo(maxim) diff = maxssecs * percent return minim.addSecs(diff) # calculate a date percentage value elif (rtype == XChartRuler.Type.Date): maxdays = minim.daysTo(maxim) diff = maxdays * percent return minim.addDays(diff) # otherwise, compare against the notches else: perc = 0.0 notches = self.notches() count = len(notches) count += self.padStart() + self.padEnd() count = max(1, count - 1) perc = float(self.padStart()) / count last = None for i, notch in enumerate(notches): perc += float(i) / count if (perc <= percent): break last = notch return last
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 XGanttWidget(QWidget): dateRangeChanged = Signal() itemMenuRequested = Signal(QTreeWidgetItem, QPoint) viewMenuRequested = Signal(QGraphicsItem, QPoint) treeMenuRequested = Signal(QTreeWidgetItem, QPoint) Timescale = enum('Minute', 'Hour', 'Day', 'Week', 'Month', 'Year') def __init__(self, parent=None): super(XGanttWidget, self).__init__(parent) # load the user interface projexui.loadUi(__file__, self) # define custom properties self._backend = None self._dateStart = QDate.currentDate().addMonths(-2) self._dateEnd = QDate.currentDate().addMonths(2) self._timeStart = QTime(0, 0, 0) self._timeEnd = QTime(23, 59, 59) self._alternatingRowColors = False self._cellWidth = 20 self._cellHeight = 20 self._first = True self._dateFormat = 'M/d/yy' self._timescale = XGanttWidget.Timescale.Month self._scrolling = False self._dirty = False # setup the palette colors palette = self.palette() color = palette.color(palette.Base) self._gridPen = QPen(color.darker(115)) self._brush = QBrush(color) self._alternateBrush = QBrush(color.darker(105)) weekendColor = color.darker(108) self._weekendBrush = QBrush(weekendColor) # setup the columns for the tree self.setColumns(['Name', 'Start', 'End', 'Calendar Days', 'Work Days']) header = self.uiGanttTREE.header() header.setFixedHeight(self._cellHeight * 2) headerItem = self.uiGanttTREE.headerItem() headerItem.setSizeHint(0, QSize(80, header.height())) # initialize the tree widget self.uiGanttTREE.setShowGrid(False) self.uiGanttTREE.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.uiGanttTREE.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.uiGanttTREE.setVerticalScrollMode(self.uiGanttTREE.ScrollPerPixel) self.uiGanttTREE.setResizeToContentsInteractive(True) self.uiGanttTREE.setEditable(True) self.uiGanttTREE.resize(500, 20) self.uiGanttTREE.setContextMenuPolicy(Qt.CustomContextMenu) # initialize the view widget self.uiGanttVIEW.setDragMode(self.uiGanttVIEW.RubberBandDrag) self.uiGanttVIEW.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.uiGanttVIEW.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.uiGanttVIEW.setScene(XGanttScene(self)) self.uiGanttVIEW.installEventFilter(self) self.uiGanttVIEW.horizontalScrollBar().setValue(50) self.uiGanttVIEW.setContextMenuPolicy(Qt.CustomContextMenu) # create connections self.uiGanttTREE.itemExpanded.connect(self.syncView) self.uiGanttTREE.itemCollapsed.connect(self.syncView) # connect scrollbars tree_bar = self.uiGanttTREE.verticalScrollBar() view_bar = self.uiGanttVIEW.verticalScrollBar() tree_bar.rangeChanged.connect(self._updateViewRect) tree_bar.valueChanged.connect(self._scrollView) view_bar.valueChanged.connect(self._scrollTree) # connect selection self.uiGanttTREE.itemSelectionChanged.connect(self._selectView) self.uiGanttVIEW.scene().selectionChanged.connect(self._selectTree) self.uiGanttTREE.itemChanged.connect(self.updateItemData) self.uiGanttTREE.customContextMenuRequested.connect( self.requestTreeMenu) self.uiGanttVIEW.customContextMenuRequested.connect( self.requestViewMenu) def _scrollTree(self, value): """ Updates the tree view scrolling to the inputed value. :param value | <int> """ if self._scrolling: return tree_bar = self.uiGanttTREE.verticalScrollBar() self._scrolling = True tree_bar.setValue(value) self._scrolling = False def _scrollView(self, value): """ Updates the gantt view scrolling to the inputed value. :param value | <int> """ if self._scrolling: return view_bar = self.uiGanttVIEW.verticalScrollBar() self._scrolling = True view_bar.setValue(value) self._scrolling = False def _selectTree(self): """ Matches the tree selection to the views selection. """ self.uiGanttTREE.blockSignals(True) self.uiGanttTREE.clearSelection() for item in self.uiGanttVIEW.scene().selectedItems(): item.treeItem().setSelected(True) self.uiGanttTREE.blockSignals(False) def _selectView(self): """ Matches the view selection to the trees selection. """ scene = self.uiGanttVIEW.scene() scene.blockSignals(True) scene.clearSelection() for item in self.uiGanttTREE.selectedItems(): item.viewItem().setSelected(True) scene.blockSignals(False) curr_item = self.uiGanttTREE.currentItem() vitem = curr_item.viewItem() if vitem: self.uiGanttVIEW.centerOn(vitem) def _updateViewRect(self): """ Updates the view rect to match the current tree value. """ if not self.updatesEnabled(): return header_h = self._cellHeight * 2 rect = self.uiGanttVIEW.scene().sceneRect() sbar_max = self.uiGanttTREE.verticalScrollBar().maximum() sbar_max += self.uiGanttTREE.viewport().height() + header_h widget_max = self.uiGanttVIEW.height() widget_max -= (self.uiGanttVIEW.horizontalScrollBar().height() + 10) rect.setHeight(max(widget_max, sbar_max)) self.uiGanttVIEW.scene().setSceneRect(rect) def addTopLevelItem(self, item): """ Adds the inputed item to the gantt widget. :param item | <XGanttWidgetItem> """ vitem = item.viewItem() self.treeWidget().addTopLevelItem(item) self.viewWidget().scene().addItem(vitem) item._viewItem = weakref.ref(vitem) if self.updatesEnabled(): try: item.sync(recursive=True) except AttributeError: pass def alternateBrush(self): """ Returns the alternate brush to be used for the grid view. :return <QBrush> """ return self._alternateBrush def alternatingRowColors(self): """ Returns whether or not this widget should show alternating row colors. :return <bool> """ return self._alternatingRowColors def blockSignals(self, state): """ Sets whether or not updates will be enabled. :param state | <bool> """ super(XGanttWidget, self).blockSignals(state) self.treeWidget().blockSignals(state) self.viewWidget().blockSignals(state) def brush(self): """ Returns the background brush to be used for the grid view. :return <QBrush> """ return self._brush def centerOnDateTime(self, dtime): """ Centers the view on a given datetime for the gantt widget. :param dtime | <QDateTime> """ view = self.uiGanttVIEW scene = view.scene() point = view.mapToScene(0, 0) x = scene.datetimeXPos(dtime) y = point.y() view.centerOn(x, y) def cellHeight(self): """ Returns the height for the cells in this gantt's views. :return <int> """ return self._cellHeight def cellWidth(self): """ Returns the width for the cells in this gantt's views. :return <int> """ return self._cellWidth def clear(self): """ Clears all the gantt widget items for this widget. """ self.uiGanttTREE.clear() self.uiGanttVIEW.scene().clear() def columns(self): """ Returns a list of the columns being used in the treewidget of this gantt chart. :return [<str>, ..] """ return self.treeWidget().columns() def currentDateTime(self): """ Returns the current date time for this widget. :return <datetime.datetime> """ view = self.uiGanttVIEW scene = view.scene() point = view.mapToScene(0, 0) return scene.datetimeAt(point.x()) def dateEnd(self): """ Returns the date end for this date range of this gantt widget. :return <QDate> """ return self._dateEnd def dateFormat(self): """ Returns the date format that will be used when rendering items in the view. :return <str> """ return self._dateFormat def dateTimeEnd(self): """ Returns the end date time for this gantt chart. :return <QDateTime> """ return QDateTime(self.dateEnd(), self.timeEnd()) def dateTimeStart(self): """ Returns the start date time for this gantt chart. :return <QDateTime> """ return QDateTime(self.dateStart(), self.timeStart()) def dateStart(self): """ Returns the date start for the date range of this gantt widget. :return <QDate> """ return self._dateStart def emitDateRangeChanged(self): """ Emits the date range changed signal provided signals aren't being blocked. """ if not self.signalsBlocked(): self.dateRangeChanged.emit() def gridPen(self): """ Returns the pen that this widget uses to draw in the view. :return <QPen> """ return self._gridPen def indexOfTopLevelItem(self, item): """ Returns the index for the inputed item from the tree. :return <int> """ return self.treeWidget().indexOfTopLevelItem(item) def insertTopLevelItem(self, index, item): """ Inserts the inputed item at the given index in the tree. :param index | <int> item | <XGanttWidgetItem> """ self.treeWidget().insertTopLevelItem(index, item) if self.updatesEnabled(): try: item.sync(recursive=True) except AttributeError: pass def rebuild(self): self.uiGanttVIEW.scene().rebuild() def requestTreeMenu(self, point): """ Emits the itemMenuRequested and treeMenuRequested signals for the given item. :param point | <QPoint> """ item = self.uiGanttTREE.itemAt(point) if item: glbl_pos = self.uiGanttTREE.viewport().mapToGlobal(point) self.treeMenuRequested.emit(item, glbl_pos) self.itemMenuRequested.emit(item, glbl_pos) def requestViewMenu(self, point): """ Emits the itemMenuRequested and viewMenuRequested signals for the given item. :param point | <QPoint> """ vitem = self.uiGanttVIEW.itemAt(point) if vitem: glbl_pos = self.uiGanttVIEW.mapToGlobal(point) item = vitem.treeItem() self.viewMenuRequested.emit(vitem, glbl_pos) self.itemMenuRequested.emit(item, glbl_pos) def setAlternateBrush(self, brush): """ Sets the alternating brush used for this widget to the inputed brush. :param brush | <QBrush> || <QColor> """ self._alternateBrush = QBrush(brush) def setAlternatingRowColors(self, state): """ Sets the alternating row colors state for this widget. :param state | <bool> """ self._alternatingRowColors = state self.treeWidget().setAlternatingRowColors(state) def setBrush(self, brush): """ Sets the main background brush used for this widget to the inputed brush. :param brush | <QBrush> || <QColor> """ self._brush = QBrush(brush) def setCellHeight(self, cellHeight): """ Sets the height for the cells in this gantt's views. :param cellHeight | <int> """ self._cellHeight = cellHeight def setCellWidth(self, cellWidth): """ Sets the width for the cells in this gantt's views. :param cellWidth | <int> """ self._cellWidth = cellWidth def setColumns(self, columns): """ Sets the columns for this gantt widget's tree to the inputed list of columns. :param columns | {<str>, ..] """ self.treeWidget().setColumns(columns) item = self.treeWidget().headerItem() for i in range(item.columnCount()): item.setTextAlignment(i, Qt.AlignBottom | Qt.AlignHCenter) def setDateEnd(self, dateEnd): """ Sets the end date for the range of this gantt widget. :param dateEnd | <QDate> """ self._dateEnd = dateEnd self.emitDateRangeChanged() def setDateFormat(self, format): """ Sets the date format that will be used when rendering in the views. :return <str> """ return self._dateFormat def setDateStart(self, dateStart): """ Sets the start date for the range of this gantt widget. :param dateStart | <QDate> """ self._dateStart = dateStart self.emitDateRangeChanged() def setDateTimeEnd(self, dtime): """ Sets the endiing date time for this gantt chart. :param dtime | <QDateTime> """ self._dateEnd = dtime.date() if self.timescale() in (self.Timescale.Minute, self.Timescale.Hour): self._timeEnd = dtime.time() else: self._timeEnd = QTime(23, 59, 59) def setDateTimeStart(self, dtime): """ Sets the starting date time for this gantt chart. :param dtime | <QDateTime> """ self._dateStart = dtime.date() if self.timescale() in (self.Timescale.Minute, self.Timescale.Hour): self._timeStart = dtime.time() else: self._timeStart = QTime(0, 0, 0) def setCurrentDateTime(self, dtime): """ Sets the current date time for this widget. :param dtime | <datetime.datetime> """ view = self.uiGanttVIEW scene = view.scene() point = view.mapToScene(0, 0) x = scene.datetimeXPos(dtime) y = point.y() view.ensureVisible(x, y, 1, 1) def setGridPen(self, pen): """ Sets the pen used to draw the grid lines for the view. :param pen | <QPen> || <QColor> """ self._gridPen = QPen(pen) def setTimescale(self, timescale): """ Sets the timescale value for this widget to the inputed value. :param timescale | <XGanttWidget.Timescale> """ self._timescale = timescale # show hour/minute scale if timescale == XGanttWidget.Timescale.Minute: self._cellWidth = 60 # (60 seconds) self._dateStart = QDate.currentDate() self._timeStart = QTime(0, 0, 0) self._dateEnd = QDate.currentDate() self._timeEnd = QTime(23, 59, 59) elif timescale == XGanttWidget.Timescale.Hour: self._cellWidth = 30 # (60 seconds / 2.0) self._dateStart = QDate.currentDate() self._timeStart = QTime(0, 0, 0) self._dateEnd = QDate.currentDate() self._timeEnd = QTime(23, 59, 59) # show day/hour scale elif timescale == XGanttWidget.Timescale.Day: self._cellWidth = 30 # (60 minutes / 2.0) self._dateStart = QDate.currentDate().addDays(-7) self._timeStart = QTime(0, 0, 0) self._dateEnd = QDate.currentDate().addDays(7) self._timeEnd = QTime(23, 59, 59) def setTimeEnd(self, time): """ Sets the ending time for this gantt chart. :param time | <QTime> """ self._timeEnd = time def setTimeStart(self, time): """ Sets the starting time for this gantt chart. :param time | <QTime> """ self._timeStart = time def setUpdatesEnabled(self, state): """ Sets whether or not updates will be enabled. :param state | <bool> """ super(XGanttWidget, self).setUpdatesEnabled(state) self.treeWidget().setUpdatesEnabled(state) self.viewWidget().setUpdatesEnabled(state) if state: self._updateViewRect() for i in range(self.topLevelItemCount()): item = self.topLevelItem(i) try: item.sync(recursive=True) except AttributeError: continue def setWeekendBrush(self, brush): """ Sets the brush to be used when coloring weekend columns. :param brush | <QBrush> || <QColor> """ self._weekendBrush = QBrush(brush) def syncView(self): """ Syncs all the items to the view. """ if not self.updatesEnabled(): return for item in self.topLevelItems(): try: item.syncView(recursive=True) except AttributeError: continue def takeTopLevelItem(self, index): """ Removes the top level item at the inputed index from the widget. :param index | <int> :return <XGanttWidgetItem> || None """ item = self.topLevelItem(index) if item: self.viewWidget().scene().removeItem(item.viewItem()) self.treeWidget().takeTopLevelItem(index) return item return None def timescale(self): """ Returns the timescale that is being used for this widget. :return <XGanttWidget.Timescale> """ return self._timescale def timeEnd(self): """ Returns the ending time for this gantt chart. Default value will be QTime(0, 0, 0) :return <QTime> """ return self._timeEnd def timeStart(self): """ Returns the starting time for this gantt chart. Default value will be QTime(0, 0, 0) :return <QTime> """ return self._timeStart def topLevelItems(self): """ Return the top level item generator. :return <generator [<QTreeWidgetItem>, ..]> """ return self.treeWidget().topLevelItems() def topLevelItem(self, index): """ Returns the top level item at the inputed index. :return <QTreeWidgetItem> """ return self.treeWidget().topLevelItem(index) def topLevelItemCount(self): """ Returns the number of top level items for this widget. :return <int> """ return self.treeWidget().topLevelItemCount() def treeWidget(self): """ Returns the tree widget for this gantt widget. :return <QTreeWidget> """ return self.uiGanttTREE def updateItemData(self, item, index): """ Updates the item information from the tree. :param item | <XGanttWidgetItem> index | <int> """ from projexui.widgets.xganttwidget.xganttwidgetitem import XGanttWidgetItem if not isinstance(item, XGanttWidgetItem): return value = unwrapVariant(item.data(index, Qt.EditRole)) if type(value) == QDateTime: value = value.date() item.setData(index, Qt.EditRole, wrapVariant(value)) if type(value) == QDate: value = value.toPython() columnName = self.treeWidget().columnOf(index) item.setProperty(columnName, value) item.sync() def viewWidget(self): """ Returns the view widget for this gantt widget. :return <QGraphicsView> """ return self.uiGanttVIEW def weekendBrush(self): """ Returns the weekend brush to be used for coloring in weekends. :return <QBrush> """ return self._weekendBrush
class XDockToolbar(QWidget): Position = enum('North', 'South', 'East', 'West') actionTriggered = Signal(object) actionMiddleTriggered = Signal(object) actionMenuRequested = Signal(object, QPoint) currentActionChanged = Signal(object) actionHovered = Signal(object) def __init__(self, parent=None): super(XDockToolbar, self).__init__(parent) # defines the position for this widget self._currentAction = -1 self._selectedAction = None self._padding = 8 self._position = XDockToolbar.Position.South self._minimumPixmapSize = QSize(16, 16) self._maximumPixmapSize = QSize(48, 48) self._hoverTimer = QTimer(self) self._hoverTimer.setSingleShot(True) self._hoverTimer.setInterval(1000) self._actionHeld = False self._easingCurve = QEasingCurve(QEasingCurve.InOutQuad) self._duration = 200 self._animating = False # install an event filter to update the location for this toolbar layout = QBoxLayout(QBoxLayout.LeftToRight) layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(0) layout.addStretch(1) layout.addStretch(1) self.setLayout(layout) self.setContentsMargins(2, 2, 2, 2) self.setMouseTracking(True) parent.window().installEventFilter(self) parent.window().statusBar().installEventFilter(self) self._hoverTimer.timeout.connect(self.emitActionHovered) def __markAnimatingFinished(self): self._animating = False def actionAt(self, pos): """ Returns the action at the given position. :param pos | <QPoint> :return <QAction> || None """ child = self.childAt(pos) if child: return child.action() return None def actionHeld(self): """ Returns whether or not the action will be held instead of closed on leaving. :return <bool> """ return self._actionHeld def actionLabels(self): """ Returns the labels for this widget. :return <XDockActionLabel> """ l = self.layout() return [l.itemAt(i).widget() for i in range(1, l.count() - 1)] def addAction(self, action): """ Adds the inputed action to this toolbar. :param action | <QAction> """ super(XDockToolbar, self).addAction(action) label = XDockActionLabel(action, self.minimumPixmapSize(), self) label.setPosition(self.position()) layout = self.layout() layout.insertWidget(layout.count() - 1, label) def clear(self): """ Clears out all the actions and items from this toolbar. """ # clear the actions from this widget for act in self.actions(): act.setParent(None) act.deleteLater() # clear the labels from this widget for lbl in self.actionLabels(): lbl.close() lbl.deleteLater() def currentAction(self): """ Returns the currently hovered/active action. :return <QAction> || None """ return self._currentAction def duration(self): """ Returns the duration value for the animation of the icons. :return <int> """ return self._duration def easingCurve(self): """ Returns the easing curve that will be used for the animation of animated icons for this dock bar. :return <QEasingCurve> """ return self._easingCurve def emitActionHovered(self): """ Emits a signal when an action is hovered. """ if not self.signalsBlocked(): self.actionHovered.emit(self.currentAction()) def eventFilter(self, object, event): """ Filters the parent objects events to rebuild this toolbar when the widget resizes. :param object | <QObject> event | <QEvent> """ if event.type() in (event.Move, event.Resize): if self.isVisible(): self.rebuild() elif object.isVisible(): self.setVisible(True) return False def holdAction(self): """ Returns whether or not the action should be held instead of clearing on leave. :return <bool> """ self._actionHeld = True def labelForAction(self, action): """ Returns the label that contains the inputed action. :return <XDockActionLabel> || None """ for label in self.actionLabels(): if label.action() == action: return label return None def leaveEvent(self, event): """ Clears the current action for this widget. :param event | <QEvent> """ super(XDockToolbar, self).leaveEvent(event) if not self.actionHeld(): self.setCurrentAction(None) def maximumPixmapSize(self): """ Returns the maximum pixmap size for this toolbar. :return <int> """ return self._maximumPixmapSize def minimumPixmapSize(self): """ Returns the minimum pixmap size that will be displayed to the user for the dock widget. :return <int> """ return self._minimumPixmapSize def mouseMoveEvent(self, event): """ Updates the labels for this dock toolbar. :param event | <XDockToolbar> """ # update the current label self.setCurrentAction(self.actionAt(event.pos())) def padding(self): """ Returns the padding value for this toolbar. :return <int> """ return self._padding def paintEvent(self, event): """ Paints the background for the dock toolbar. :param event | <QPaintEvent> """ x = 1 y = 1 w = self.width() h = self.height() clr_a = QColor(220, 220, 220) clr_b = QColor(190, 190, 190) grad = QLinearGradient() grad.setColorAt(0.0, clr_a) grad.setColorAt(0.6, clr_a) grad.setColorAt(1.0, clr_b) # adjust the coloring for the horizontal toolbar if self.position() & (self.Position.North | self.Position.South): h = self.minimumPixmapSize().height() + 6 if self.position() == self.Position.South: y = self.height() - h grad.setStart(0, y) grad.setFinalStop(0, self.height()) else: grad.setStart(0, 0) grad.setFinalStart(0, h) # adjust the coloring for the vertical toolbar if self.position() & (self.Position.East | self.Position.West): w = self.minimumPixmapSize().width() + 6 if self.position() == self.Position.West: x = self.width() - w grad.setStart(x, 0) grad.setFinalStop(self.width(), 0) else: grad.setStart(0, 0) grad.setFinalStop(w, 0) with XPainter(self) as painter: painter.fillRect(x, y, w, h, grad) # show the active action action = self.selectedAction() if action is not None and \ not self.currentAction() and \ not self._animating: for lbl in self.actionLabels(): if lbl.action() != action: continue geom = lbl.geometry() size = lbl.pixmapSize() if self.position() == self.Position.North: x = geom.left() y = 0 w = geom.width() h = size.height() + geom.top() + 2 elif self.position() == self.Position.East: x = 0 y = geom.top() w = size.width() + geom.left() + 2 h = geom.height() painter.setPen(QColor(140, 140, 40)) painter.setBrush(QColor(160, 160, 160)) painter.drawRect(x, y, w, h) break def position(self): """ Returns the position for this docktoolbar. :return <XDockToolbar.Position> """ return self._position def rebuild(self): """ Rebuilds the widget based on the position and current size/location of its parent. """ if not self.isVisible(): return self.raise_() max_size = self.maximumPixmapSize() min_size = self.minimumPixmapSize() widget = self.window() rect = widget.rect() rect.setBottom(rect.bottom() - widget.statusBar().height()) rect.setTop(widget.menuBar().height()) offset = self.padding() # align this widget to the north if self.position() == XDockToolbar.Position.North: self.move(rect.left(), rect.top()) self.resize(rect.width(), min_size.height() + offset) # align this widget to the east elif self.position() == XDockToolbar.Position.East: self.move(rect.left(), rect.top()) self.resize(min_size.width() + offset, rect.height()) # align this widget to the south elif self.position() == XDockToolbar.Position.South: self.move(rect.left(), rect.top() - min_size.height() - offset) self.resize(rect.width(), min_size.height() + offset) # align this widget to the west else: self.move(rect.right() - min_size.width() - offset, rect.top()) self.resize(min_size.width() + offset, rect.height()) def resizeToMinimum(self): """ Resizes the dock toolbar to the minimum sizes. """ offset = self.padding() min_size = self.minimumPixmapSize() if self.position() in (XDockToolbar.Position.East, XDockToolbar.Position.West): self.resize(min_size.width() + offset, self.height()) elif self.position() in (XDockToolbar.Position.North, XDockToolbar.Position.South): self.resize(self.width(), min_size.height() + offset) def selectedAction(self): """ Returns the action that was last selected. :return <QAction> """ return self._selectedAction def setActionHeld(self, state): """ Sets whether or not this action should be held before clearing on leaving. :param state | <bool> """ self._actionHeld = state def setCurrentAction(self, action): """ Sets the current action for this widget that highlights the size for this toolbar. :param action | <QAction> """ if action == self._currentAction: return self._currentAction = action self.currentActionChanged.emit(action) labels = self.actionLabels() anim_grp = QParallelAnimationGroup(self) max_size = self.maximumPixmapSize() min_size = self.minimumPixmapSize() if action: label = self.labelForAction(action) index = labels.index(label) # create the highlight effect palette = self.palette() effect = QGraphicsDropShadowEffect(label) effect.setXOffset(0) effect.setYOffset(0) effect.setBlurRadius(20) effect.setColor(QColor(40, 40, 40)) label.setGraphicsEffect(effect) offset = self.padding() if self.position() in (XDockToolbar.Position.East, XDockToolbar.Position.West): self.resize(max_size.width() + offset, self.height()) elif self.position() in (XDockToolbar.Position.North, XDockToolbar.Position.South): self.resize(self.width(), max_size.height() + offset) w = max_size.width() h = max_size.height() dw = (max_size.width() - min_size.width()) / 3 dh = (max_size.height() - min_size.height()) / 3 for i in range(4): before = index - i after = index + i if 0 <= before and before < len(labels): anim = XObjectAnimation(labels[before], 'setPixmapSize', anim_grp) anim.setEasingCurve(self.easingCurve()) anim.setStartValue(labels[before].pixmapSize()) anim.setEndValue(QSize(w, h)) anim.setDuration(self.duration()) anim_grp.addAnimation(anim) if i: labels[before].setGraphicsEffect(None) if after != before and 0 <= after and after < len(labels): anim = XObjectAnimation(labels[after], 'setPixmapSize', anim_grp) anim.setEasingCurve(self.easingCurve()) anim.setStartValue(labels[after].pixmapSize()) anim.setEndValue(QSize(w, h)) anim.setDuration(self.duration()) anim_grp.addAnimation(anim) if i: labels[after].setGraphicsEffect(None) w -= dw h -= dh else: offset = self.padding() for label in self.actionLabels(): # clear the graphics effect label.setGraphicsEffect(None) # create the animation anim = XObjectAnimation(label, 'setPixmapSize', self) anim.setEasingCurve(self.easingCurve()) anim.setStartValue(label.pixmapSize()) anim.setEndValue(min_size) anim.setDuration(self.duration()) anim_grp.addAnimation(anim) anim_grp.finished.connect(self.resizeToMinimum) anim_grp.start() self._animating = True anim_grp.finished.connect(anim_grp.deleteLater) anim_grp.finished.connect(self.__markAnimatingFinished) if self._currentAction: self._hoverTimer.start() else: self._hoverTimer.stop() def setDuration(self, duration): """ Sets the duration value for the animation of the icon. :param duration | <int> """ self._duration = duration def setEasingCurve(self, curve): """ Sets the easing curve for this toolbar to the inputed curve. :param curve | <QEasingCurve> """ self._easingCurve = QEasingCurve(curve) def setMaximumPixmapSize(self, size): """ Sets the maximum pixmap size for this toolbar. :param size | <int> """ self._maximumPixmapSize = size position = self.position() self._position = None self.setPosition(position) def setMinimumPixmapSize(self, size): """ Sets the minimum pixmap size that will be displayed to the user for the dock widget. :param size | <int> """ self._minimumPixmapSize = size position = self.position() self._position = None self.setPosition(position) def setPadding(self, padding): """ Sets the padding amount for this toolbar. :param padding | <int> """ self._padding = padding def setPosition(self, position): """ Sets the position for this widget and its parent. :param position | <XDockToolbar.Position> """ if position == self._position: return self._position = position widget = self.window() layout = self.layout() offset = self.padding() min_size = self.minimumPixmapSize() # set the layout to north if position == XDockToolbar.Position.North: self.move(0, 0) widget.setContentsMargins(0, min_size.height() + offset, 0, 0) layout.setDirection(QBoxLayout.LeftToRight) # set the layout to east elif position == XDockToolbar.Position.East: self.move(0, 0) widget.setContentsMargins(min_size.width() + offset, 0, 0, 0) layout.setDirection(QBoxLayout.TopToBottom) # set the layout to the south elif position == XDockToolbar.Position.South: widget.setContentsMargins(0, 0, 0, min_size.height() + offset) layout.setDirection(QBoxLayout.LeftToRight) # set the layout to the west else: widget.setContentsMargins(0, 0, min_size.width() + offset, 0) layout.setDirection(QBoxLayout.TopToBottom) # update the label alignments for label in self.actionLabels(): label.setPosition(position) # rebuilds the widget self.rebuild() self.update() def setSelectedAction(self, action): """ Sets the selected action instance for this toolbar. :param action | <QAction> """ self._hoverTimer.stop() self._selectedAction = action def setVisible(self, state): """ Sets whether or not this toolbar is visible. If shown, it will rebuild. :param state | <bool> """ super(XDockToolbar, self).setVisible(state) if state: self.rebuild() self.setCurrentAction(None) def unholdAction(self): """ Unholds the action from being blocked on the leave event. """ self._actionHeld = False point = self.mapFromGlobal(QCursor.pos()) self.setCurrentAction(self.actionAt(point)) def visualRect(self, action): """ Returns the visual rect for the inputed action, or a blank QRect if no matching action was found. :param action | <QAction> :return <QRect> """ for widget in self.actionLabels(): if widget.action() == action: return widget.geometry() return QRect()
class Plugin(object): """ Defines the base class for generating a plugin based system in Python. This class is just an abstract framework to provide an interface to managing multiple objects of a particular plugin type through a simple registration system. To use, you will need to subclass this object and implement your own abstract and virtual methods to provide an interface to your application, however it will handle all the common methods such as registration, enabling, etc. from here. :usage |from projex.plugin import Plugin |from projex.decorators import abstractmethod | |class MyPlugin(Plugin): | @abstractmethod | def convert(self, value): | pass | |class StringPlugin(MyPlugin): | def __init__( self ): | super(StringPlugin, self).__init__('string') | | def convert(self, value): | return str(value) | |class BoolPlugin(MyPlugin): | def __init__( self ): | super(StringPlugin, self).__init__('bool') | | def convert(self, value): | return bool(value) | |MyPlugin.register(StringPlugin()) |MyPlugin.register(BoolPlugin()) | |for plugin in MyPlugin.plugins(): | print plugin.instance().convert(1) | |print MyPlugin.plugin('string').instance().convert(1) """ # define enums Type = enum('Module', 'Package', 'RegistryFile') def __init__(self, name, version=1.0): # define basic information self._name = str(name) self._version = version # define advanced information self._enabled = True self._filepath = '' self._icon = '' self._description = '' # define error information self._loaded = True self._instance = self self._error = None # define authorship information self._author = '' self._email = '' self._url = '' # define the filepath loading information mod = sys.modules.get(self.__class__.__module__) if (mod): self._filepath = os.path.abspath(mod.__file__) def author(self): """ Returns the author information for this plugin. :return <str> """ return self._author def description(self): """ Returns the description for this plugin. :return <str> """ return self._description def disable(self): """ Disables this plugin. :sa setEnabled """ self.setEnabled(False) def email(self): """ Returns the email information for this plugin. :return <str> """ return self._email def enable(self): """ Enables this plugin. :sa setEnabled """ self.setEnabled(True) def error(self): """ Returns the error from this plugin. :return <Exception> || None """ return self._error def filepath(self): """ Returns the filepath from which this plugin was loaded. :return <str> """ return self._filepath def icon(self): """ Returns the icon filepath for this plugin. :return <str> """ path = self._icon if (not path): return '' path = os.path.expandvars(os.path.expanduser(path)) if (path.startswith('.')): base_path = os.path.dirname(self.filepath()) path = os.path.abspath(os.path.join(base_path, path)) return path def instance(self): """ Returns the main instance of this plugin. When wanting to use custom methods from a plugin class, call this method first, as plugins may decide to generate their instances in different ways. By default, this will just return the self value. For instance, a PluginProxy class will load its plugin registration information during the initialization stage, but not load the actual class until the instance method is called, delaying the load of the actual module until later. """ return self._instance def isEnabled(self): """ Returns whether or not this plugin is enabled. :return <bool> """ return self._enabled and not self.isErrored() def isErrored(self): """ Returns whether or not the plugin ran into errors. :return <bool> """ return self._error is not None def isLoaded(self): """ Returns whether or not this plugin has been loaded or not. :return <bool> """ return self._loaded def name(self): """ Returns the name for this plugin. :return <str> """ return self._name def setAuthor(self, author): """ Sets the author information for this plugin. :param author | <str> """ self._author = str(author) def setDescription(self, description): """ Sets the description for this plugin to the inputed plugin. :param description | <str> """ self._description = description def setEmail(self, email): """ Sets the email information for this plugin. :param email | <str> """ self._email = str(email) def setEnabled(self, state): """ Sets whether or not this plugin is enabled. :param state | <bool> """ self._enabled = state def setError(self, error): """ Sets the error that this plugin ran into last. :param error | <Exception> || None """ self._error = error def setFilepath(self, filepath): """ Sets the filepath location for this plugin. :param filepath | <str> """ self._filepath = filepath def setIcon(self, filepath): """ Sets the icon filepath for this plugin to the inputed path. This can be either an absolute path, or a relative path from the location this plugin was loaded from. :param filepath | <str> """ self._icon = str(filepath) def setUrl(self, url): """ Sets the url information for this plugin. :param url | <str> """ self._url = str(url) def url(self): """ Returns the URL information for this plugin. :return <str> """ return self._url def version(self): """ Returns the version for this plugin. :return <float> """ return self._version @classmethod def addPluginPath(cls, pluginpath): """ Adds the plugin path for this class to the given path. The inputed pluginpath value can either be a list of strings, or a string containing paths separated by the OS specific path separator (':' on Mac & Linux, ';' on Windows) :param pluginpath | [<str>, ..] || <str> """ prop_key = '_%s__pluginpath' % cls.__name__ if not hasattr(cls, prop_key): setattr(cls, prop_key, []) curr_path = getattr(cls, prop_key) if isinstance(pluginpath, basestring): pluginpath = pluginpath.split(os.path.pathsep) for path in pluginpath: if not path: continue path = os.path.expanduser(os.path.expandvars(path)) paths = path.split(os.path.pathsep) if len(paths) > 1: cls.addPluginPath(paths) else: curr_path.append(path) @classmethod def pluginRegisterType(cls): """ Returns the register type for this plugin class. :return <Plugin.RegisterType> """ default = Plugin.Type.Module default |= Plugin.Type.Package default |= Plugin.Type.RegistryFile return getattr(cls, '_%s__pluginRegisterType', default) @classmethod def loadPlugins(cls): """ Initializes the plugins by loading modules from the inputed paths. """ plugs = getattr(cls, '_%s__plugins' % cls.__name__, None) if plugs is not None: return plugs = {} setattr(cls, '_%s__plugins' % cls.__name__, plugs) typ = cls.pluginRegisterType() for path in cls.pluginPath(): base_package = projex.packageFromPath(path) base_path = os.path.normpath(projex.packageRootPath(path)) # make sure it is at the front of the path if base_path in sys.path: sys.path.remove(base_path) sys.path.insert(0, base_path) processed = ['__init__'] # load support for registries if typ & Plugin.Type.RegistryFile: files = glob.glob(os.path.join(path, '*/register.xml')) for file in files: name = os.path.normpath(file).split(os.path.sep)[-2] processed.append(name) try: proxy = PluginProxy.fromFile(cls, file) cls.register(proxy) except Exception, e: name = projex.text.pretty(name) err = Plugin(name) err.setError(e) err.setFilepath(file) cls.register(err) # log the error msg = "%s.plugin('%s') failed to load from %s." logger.warning(msg % (cls.__name__, name, file)) logger.error(e) # load support for packages if typ & Plugin.Type.Package: files = glob.glob(os.path.join(path, '*/__init__.py')) for file in files: name = os.path.normpath(file).split(os.path.sep)[-2] if (name in processed): continue processed.append(name) package = '.'.join([base_package, name]).strip('.') if (not package): continue try: __import__(package) except Exception, e: name = projex.text.pretty(name) err = Plugin(name) err.setError(e) err.setFilepath(file) cls.register(err) # log the error msg = "%s.plugin('%s') failed to load from %s." logger.warning(msg % (cls.__name__, name, file)) logger.error(e) # load support for modules if typ & Plugin.Type.Module: files = glob.glob(os.path.join(path, '*.py')) for file in files: name = os.path.basename(file).split('.')[0] if (name in processed): continue processed.append(name) package = '.'.join([base_package, name]).strip('.') if (not package): continue try: __import__(package) except Exception, e: name = projex.text.pretty(name) err = Plugin(name) err.setError(e) err.setFilepath(file) cls.register(err) # log the error msg = "%s.plugin('%s') failed to load from %s." logger.warning(msg % (cls.__name__, name, file)) logger.error(e)
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 DoxSource(object): Type = enum('Wiki', 'Module', 'Group') def __init__(self, dox, typ, name, filepath): self._dox = dox self._type = typ self._name = name self._filepath = filepath self._entries = [] def addEntry(self, entry): """ Adds a new entry to this entry. Entries can be grouped together to create custom hierarchies. :param entry | <DoxEntry> """ entry._order = len(self._entries) self._entries.append(entry) def entries(self): """ Returns the list of entries associated with this dox file. :return [<DoxEntry>, ..] """ return self._entries def export(self, outpath, breadcrumbs=None): """ Exports this source to the given output path location. :param outpath | <str> """ if breadcrumbs is None: breadcrumbs = ['Home'] # save the toc xtoc = ElementTree.Element('toc') xtoc.set('name', self.name()) for entry in sorted(self.entries()): entry.toXml(xtoc) projex.text.xmlindent(xtoc) toc_file = open(os.path.join(outpath, 'toc.xml'), 'w') toc_file.write(ElementTree.tostring(xtoc)) toc_file.close() # generate an arbitrary group if self.type() == DoxSource.Type.Group: key = self.name().replace(' ', '_').lower() target = os.path.join(outpath, key) if not os.path.exists(target): os.mkdir(target) breadcrumbs.append(self.name()) for source in self.sources(): source.export(target, breadcrumbs) # generate user docs elif self.type() == DoxSource.Type.Wiki: for entry in sorted(self._entries): self.exportEntry(self.filepath(), outpath, entry, breadcrumbs) # generate module docs elif self.type() == DoxSource.Type.Module: pass def exportEntry(self, src, target, entry, breadcrumbs): """ Exports the entry to HTML. :param srcpath | <str> targetpath | <str> entry | <DoxEntry> """ src_path = os.path.join(src, entry.basename()) target_path = os.path.join(target, entry.basename()) # export a folder if os.path.isdir(src_path): os.makedirs(target_path) crumbs = breadcrumbs + [entry.title()] for child in entry.children(): self.exportEntry(src_path, target_path, child, crumbs) # export a wiki file elif entry.basename().endswith('.wiki'): count = len(breadcrumbs) - 1 base_url = '.' + '/..' * count static_url = base_url + '/_static' # extract wiki information wiki_file = open(src_path, 'r') wiki_contents = wiki_file.read() wiki_file.close() # generate the contents # generate the breadcrumbs for this file incl = entry.basename() != 'index.wiki' html = self._dox.render(entry.title(), wiki_contents, breadcrumbs, filepath=src_path, baseUrl=base_url, staticUrl=static_url, includeTitleInCrumbs=incl) # generate the new html file target_path = target_path.replace('.wiki', '.html') html_file = open(target_path, 'w') html_file.write(html) html_file.close() def filepath(self): """ Returns the filepath for the source of this source. :return <str> """ path = os.path.expandvars(self._filepath) if os.path.isabs(path): return path elif self._dox.filename(): basepath = os.path.dirname(self._dox.filename()) basepath = os.path.join(basepath, path) else: basepath = path return os.path.abspath(basepath) def name(self): """ Returns the name for this source. :return <str> """ return self._name def sources(self): """ Returns a list of the sub-sources this instance holds. Sources can be nested together in groups to create custom hierarchies. :return [<DoxSource>, ..] """ return self._sources def sync(self): """ Syncs the contents of this entry based on the given filepath. :param filepath | <str> """ if self.type() != DoxSource.Type.Wiki: return filepath = self.filepath() entry_names = [entry.basename() for entry in self.entries()] filenames = list(os.listdir(str(filepath))) rem_paths = set(entry_names).difference(filenames) new_paths = set(filenames).difference(entry_names) # remove the deleted filepaths if rem_paths: kids = [c for c in self.entries() if c.basename() not in rem_paths] for i, kid in enumerate(kids): kid.setOrder(i) else: kids = self.entries()[:] # add new paths for path in sorted(new_paths): entrypath = os.path.join(filepath, path) if os.path.isdir(entrypath): title = projex.text.pretty(path) else: title = projex.text.pretty(os.path.splitext(path)[0]) order = len(kids) entry = DoxEntry(title=title, basename=path, order=order) kids.append(entry) self._entries = kids # sync sub-folders for entry in self.entries(): entrypath = os.path.join(filepath, entry.basename()) if os.path.isdir(entrypath): entry.sync(entrypath) def toXml(self, xml): """ Converts this source information to xml data. :param xml | <ElementTree.Element> :return <ElementTree.SubElement> """ xsource = ElementTree.SubElement(xml, 'source') xsource.set('type', DoxSource.Type[self.type()]) xsource.set('name', self.name()) xsource.set('path', self._filepath) for source in sorted(self._entries): source.toXml(xsource) return xsource def type(self): """ Returns the type of source this source is. :return <DoxSource.Type> """ return self._type @staticmethod def fromXml(dox, xml): """ Generates a new source based on the inputed xml node. :param xml | <ElementTree.Element> :return <DoxSource> """ typ = DoxSource.Type[xml.get('type')] rsc = DoxSource(dox, typ, xml.get('name', ''), xml.get('path', '')) # load entries for xentry in xml: rsc.addEntry(DoxEntry.fromXml(xentry)) return rsc
__maintainer__ = 'Projex Software, LLC' __email__ = '*****@*****.**' import datetime import projex.text from projex.enum import enum # define global enumerations RepeatFlags = enum( # Weekly Flags 'EveryMonday', 'EveryTuesday', 'EveryWednesday', 'EveryThursday', 'EveryFriday', 'EverySaturday', 'EverySunday', # Repeating Flags 'DayOfTheMonth', 'DayOfTheWeek') RepeatMode = enum( Weekly=2, # removed the 'Daily' key, was 1 Monthly=4, Yearly=8) Names = enum( # default 'Sometime', # Preset Names
class Collector(object): Flags = enum('Unique', 'Private', 'ReadOnly', 'Virtual', 'Static', 'AutoExpand') def __json__(self): try: model = self.model() except orb.errors.ModelNotFound: model_name = None else: model_name = model.schema().name() if model else None output = { 'name': self.__name, 'model': model_name, 'flags': {k: True for k in self.Flags.toSet(self.__flags)} } return output def __init__(self, name='', flags=0, getter=None, setter=None, model=None, queryFilter=None): self.__name = self.__name__ = name self.__model = model self.__schema = None self.__preload = None self.__getter = getter self.__setter = setter self.__query_filter = queryFilter self.__flags = self.Flags.fromSet(flags) if isinstance(flags, set) else flags def __call__(self, record, useMethod=True, **context): if self.__getter and useMethod: return self.__getter(record, **context) else: if not record.isRecord(): return orb.Collection() else: collection = self.collect(record, **context) # preload the results if isinstance(collection, orb.Collection): cache = record.preload(projex.text.underscore(self.name())) collection.preload(cache, **context) if self.testFlag(self.Flags.Unique): return collection.first() else: return collection else: return collection def copy(self): out = type(self)(name=self.__name, flags=self.__flags) out.setSchema(self.schema()) return out def collect(self, record, **context): if self.__getter: return self.__getter(record, **context) raise NotImplementedError def collectExpand(self, query, parts, **context): raise NotImplementedError def queryFilter(self, function=None): """ Defines a decorator that can be used to filter queries. It will assume the function being associated with the decorator will take a query as an input and return a modified query to use. :usage class MyModel(orb.Model): objects = orb.ReverseLookup('Object') @classmethod @objects.queryFilter() def objectsFilter(cls, query, **context): return orb.Query() :param function: <callable> :return: <wrapper> """ if function is not None: self.__query_filter = function return function def wrapper(func): self.__query_filter = func return func return wrapper def queryFilterMethod(self): """ Returns the actual query filter method, if any, that is associated with this collector. :return: <callable> """ return self.__query_filter def getter(self, function=None): if function is not None: self.__getter = function return function def wrapper(func): self.__getter = func return func return wrapper def gettermethod(self): return self.__getter def flags(self): return self.__flags def model(self): if isinstance(self.__model, (str, unicode)): schema = orb.system.schema(self.__model) if schema is not None: return schema.model() else: raise orb.errors.ModelNotFound(self.__model) else: return self.__model def name(self): return self.__name def schema(self): return self.__schema def setter(self, function=None): if function is not None: self.__setter = function return function def wrapper(func): self.__setter = func return func return wrapper def settermethod(self): return self.__setter def setName(self, name): self.__name = self.__name__ = name def setSchema(self, schema): self.__schema = schema def setFlags(self, flags): self.__flags = flags def testFlag(self, flag): return (self.__flags & flag) > 0
class XSplitterHandle(QSplitterHandle): CollapseDirection = enum('After', 'Before') def __init__(self, orientation, parent): super(XSplitterHandle, self).__init__(orientation, parent) # create a layout for the different buttons self._collapsed = False self._storedSizes = None self._collapseBefore = QToolButton(self) self._resizeGrip = QLabel(self) self._collapseAfter = QToolButton(self) self._collapseBefore.setAutoRaise(True) self._collapseAfter.setAutoRaise(True) self._collapseBefore.setCursor(Qt.ArrowCursor) self._collapseAfter.setCursor(Qt.ArrowCursor) # define the layout layout = QBoxLayout(QBoxLayout.LeftToRight, self) layout.setContentsMargins(0, 0, 0, 0) layout.addStretch(1) layout.addWidget(self._collapseBefore) layout.addWidget(self._resizeGrip) layout.addWidget(self._collapseAfter) layout.addStretch(1) self.setLayout(layout) # set the orientation to start with self.setOrientation(orientation) # create connections self._collapseAfter.clicked.connect(self.toggleCollapseAfter) self._collapseBefore.clicked.connect(self.toggleCollapseBefore) def collapse(self, direction): """ Collapses this splitter handle before or after other widgets based on \ the inputed CollapseDirection. :param direction | <XSplitterHandle.CollapseDirection> :return <bool> | success """ if (self.isCollapsed()): return False splitter = self.parent() if (not splitter): return False sizes = splitter.sizes() handles = [splitter.handle(i) for i in range(len(sizes))] index = handles.index(self) self.markCollapsed(direction, sizes) # determine the sizes to use based on the direction if (direction == XSplitterHandle.CollapseDirection.Before): sizes = [0 for i in range(i)] + sizes[i + 1:] else: sizes = sizes[:i] + [0 for i in range(i, len(sizes))] splitter.setSizes(sizes) return True def collapseAfter(self, handle): """ Collapses the splitter after the inputed handle. :param handle | <XSplitterHandle> """ self.setUpdatesEnabled(False) # collapse all items after the current handle if (handle.isCollapsed()): self.setSizes(handle.restoreSizes()) found = False sizes = self.sizes() handle.storeSizes(sizes) for c in range(self.count()): if (self.handle(c) == handle): found = True if (found): sizes[c] = 0 self.setSizes(sizes) self.update() self.setUpdatesEnabled(True) def collapseBefore(self, handle): """ Collapses the splitter before the inputed handle. :param handle | <XSplitterHandle> """ self.setUpdatesEnabled(False) # collapse all items after the current handle if (handle.isCollapsed()): self.setSizes(handle.restoreSizes()) # collapse all items before the current handle found = False sizes = self.sizes() handle.storeSizes(sizes) for c in range(self.count()): if (self.handle(c) == handle): break sizes[c] = 0 self.setSizes(sizes) self.setUpdatesEnabled(True) def isCollapsed(self): """ Returns whether or not this widget is collapsed. :return <bool> """ return self._collapsed def markCollapsed(self, direction, sizes): """ Updates the interface to reflect that the splitter is collapsed. :param direction | <XSplitterHandle.CollapseDirection> sizes | [<int>, ..] """ self._collapsed = True self._storedSizes = sizes[:] if (direction == XSplitterHandle.CollapseDirection.Before): if (self.orientation() == Qt.Horizontal): self._collapseAfter.setArrowType(Qt.RightArrow) self._collapseBefore.setArrowType(Qt.RightArrow) else: self._collapseAfter.setArrowType(Qt.DownArrow) self._collapseBefore.setArrowType(Qt.DownArrow) else: if (self.orientation() == Qt.Horizontal): self._collapseAfter.setArrowType(Qt.LeftArrow) self._collapseBefore.setArrowType(Qt.LeftArrow) else: self._collapseAfter.setArrowType(Qt.UpArrow) self._collapseAfter.setArrowType(Qt.UpArrow) def paintEvent(self, event): """ Overloads the paint event to handle drawing the splitter lines. :param event | <QPaintEvent> """ lines = [] count = 20 # calculate the lines if (self.orientation() == Qt.Vertical): x = self._resizeGrip.pos().x() h = self.height() spacing = int(float(self._resizeGrip.width()) / count) for i in range(count): lines.append(QLine(x, 0, x, h)) x += spacing else: y = self._resizeGrip.pos().y() w = self.width() spacing = int(float(self._resizeGrip.height()) / count) for i in range(count): lines.append(QLine(0, y, w, y)) y += spacing # draw the lines with XPainter(self) as painter: pal = self.palette() painter.setPen(pal.color(pal.Window).darker(120)) painter.drawLines(lines) def setOrientation(self, orientation): """ Sets the orientation for this handle and updates the widgets linked \ with it. :param orientation | <Qt.Orientation> """ super(XSplitterHandle, self).setOrientation(orientation) if (orientation == Qt.Vertical): self.layout().setDirection(QBoxLayout.LeftToRight) # update the widgets self._collapseBefore.setFixedSize(30, 10) self._collapseAfter.setFixedSize(30, 10) self._resizeGrip.setFixedSize(60, 10) self._collapseBefore.setArrowType(Qt.UpArrow) self._collapseAfter.setArrowType(Qt.DownArrow) elif (orientation == Qt.Horizontal): self.layout().setDirection(QBoxLayout.TopToBottom) # update the widgets self._collapseBefore.setFixedSize(10, 30) self._collapseAfter.setFixedSize(10, 30) self._resizeGrip.setFixedSize(10, 60) self._collapseBefore.setArrowType(Qt.LeftArrow) self._collapseAfter.setArrowType(Qt.RightArrow) def uncollapse(self): """ Uncollapses the splitter this handle is associated with by restoring \ its sizes from before the collapse occurred. :return <bool> | changed """ if (not self.isCollapsed()): return False self.parent().setSizes(self._storedSizes) self.unmarkCollapsed() return True def unmarkCollapsed(self): """ Unmarks this splitter as being in a collapsed state, clearing any \ collapsed information. """ if (not self.isCollapsed()): return self._collapsed = False self._storedSizes = None if (self.orientation() == Qt.Vertical): self._collapseBefore.setArrowType(Qt.UpArrow) self._collapseAfter.setArrowType(Qt.DownArrow) else: self._collapseBefore.setArrowType(Qt.LeftArrow) self._collapseAfter.setArrowType(Qt.RightArrow) def toggleCollapseAfter(self): """ Collapses the splitter after this handle. """ if (self.isCollapsed()): self.uncollapse() else: self.collapse(XSplitterHandle.CollapseDirection.After) def toggleCollapseBefore(self): """ Collapses the splitter before this handle. """ if (self.isCollapsed()): self.uncollapse() else: self.collapse(XSplitterHandle.CollapseDirection.Before)
class XCodeHighlighter(QtGui.QSyntaxHighlighter, AddonManager): Style = enum('Keyword', 'Comment', 'String', 'Class', 'Function') Theme = enum('Default', 'Dark') def __init__(self, parent=None): super(XCodeHighlighter, self).__init__(parent) # define the format options self._formats = {} self._patterns = [] self._multiline = [] self._theme = None self.setTheme(XCodeHighlighter.Theme.Default) def definePattern(self, style, pattern): """ Defines a pattern for the given style. :param style | <XCodeHighlighter.Style> pattern | <str> """ self._patterns.append((style, pattern)) def defineMultiline(self, style, openPattern, closePattern): """ Defines a pattern that can span multiple styles. :param style | <XCodeHighlighter.Style> openPattern | <str> closePattern | <str> """ self._multiline.append((style, openPattern, closePattern)) def highlightBlock(self, text): """ Highlights the given text format based on this highlighters syntax rules. :param text | <str> """ text = nativestring(text) for pattern, format in self.patterns(): for result in re.finditer(pattern, text): grps = result.groups() if grps: for i in range(len(grps)): start, end = result.span(i+1) self.setFormat(start, end - start, format) else: self.setFormat(result.start(), result.end() - result.start(), format) self.setCurrentBlockState(0) if self.previousBlockState() == 1: return for form, open, close in self._multiline: open = QtCore.QRegExp(open) close = QtCore.QRegExp(close) start = open.indexIn(text) processed = False while start >= 0: processed = True end = close.indexIn(text, start) if end == -1: self.setCurrentBlockState(1) length = len(text) - start else: length = end - start + close.matchedLength() self.setFormat(start, length, form) start = open.indexIn(text, start + length) if processed: break def keywords(self): """ Returns the list of keywords associated with this highlighter. :return [<str>, ..] """ return self._keywords def patterns(self): """ Returns a list of highlighting rules for this highlighter. This will join the list of keywords as a regular expression with the keyword format as well as any custom patterns that have been defined. :return [(<str>, <QtGui.QTextCharFormat>), ..] """ patterns = [] # add keyword patterns form = self.styleFormat(XCodeHighlighter.Style.Keyword) for kwd in self.keywords(): patterns.append((r'\b{0}\b'.format(kwd), form)) # add additional patterns for style, pattern in self._patterns: form = self.styleFormat(style) if not form: continue patterns.append((pattern, form)) return patterns def setStyleFormat(self, style, format): """ Sets the character format for the given style for this highlighter. :param style | <XCodeHighlighter.Style> format | <QtGui.QTextCharFormat> """ self._formats[style] = format def setKeywords(self, keywords): """ Sets the keywords for this highlighter to the inputed list of keywords. :param keywords | [<str>, ..] """ self._keywords = keywords def setTheme(self, theme): if theme == XCodeHighlighter.Theme.Default: # create the default keyword format form = QtGui.QTextCharFormat() form.setForeground(QtGui.QColor('blue')) self._formats[XCodeHighlighter.Style.Keyword] = form # create the default comment format form = QtGui.QTextCharFormat() form.setForeground(QtGui.QColor('green')) self._formats[XCodeHighlighter.Style.Comment] = form # create the default string format form = QtGui.QTextCharFormat() form.setForeground(QtGui.QColor('brown')) self._formats[XCodeHighlighter.Style.String] = form # create the class format form = QtGui.QTextCharFormat() form.setForeground(QtGui.QColor('darkMagenta')) form.setFontWeight(QtGui.QFont.Bold) self._formats[XCodeHighlighter.Style.Class] = form # create the function format form = QtGui.QTextCharFormat() form.setForeground(QtGui.QColor('darkMagenta')) form.setFontWeight(QtGui.QFont.Bold) self._formats[XCodeHighlighter.Style.Function] = form elif theme == XCodeHighlighter.Theme.Dark: opts = [] opts.append((self.Style.Keyword, '#2da4ff', False)) opts.append((self.Style.String, 'orange', False)) opts.append((self.Style.Comment, '#10ff00', False)) opts.append((self.Style.Class, '#f7ffc1', True)) opts.append((self.Style.Function, '#f7ffc1', True)) for style, clr, bold in opts: form = QtGui.QTextCharFormat() form.setForeground(QtGui.QColor(clr)) if bold: form.setFontWeight(QtGui.QFont.Bold) self._formats[style] = form def styleFormat(self, style): """ Returns the character format for the given style for this highlighter. :param style | <XCodeHighlighter.Style> :return <QtGui.QTextCharFormat> || None """ return self._formats.get(style, None) @classmethod def fileTypes(cls): prop = '_{0}__filetypes'.format(cls.__name__) return getattr(cls, prop, '') @classmethod def hasFileType(cls, filetype): return filetype in cls.fileTypes().split(',') @classmethod def setFileTypes(cls, filetypes): prop = '_{0}__filetypes'.format(cls.__name__) setattr(cls, prop, filetypes)
class XPopupWidget(QWidget): """ """ Direction = enum('North', 'South', 'East', 'West') Mode = enum('Popup', 'Dialog', 'ToolTip') Anchor = enum('TopLeft', 'TopCenter', 'TopRight', 'LeftTop', 'LeftCenter', 'LeftBottom', 'RightTop', 'RightCenter', 'RightBottom', 'BottomLeft', 'BottomCenter', 'BottomRight') aboutToShow = Signal() accepted = Signal() closed = Signal() rejected = Signal() resetRequested = Signal() shown = Signal() buttonClicked = Signal(QAbstractButton) def __init__(self, parent=None, buttons=None): super(XPopupWidget, self).__init__(parent) # define custom properties self._anchor = XPopupWidget.Anchor.TopCenter self._autoCalculateAnchor = False self._autoCloseOnAccept = True self._autoCloseOnReject = True self._autoCloseOnFocusOut = False self._autoDefault = True self._first = True self._animated = False self._currentMode = None self._positionLinkedTo = [] self._possibleAnchors = XPopupWidget.Anchor.all() # define controls self._result = 0 self._resizable = True self._popupPadding = 10 self._titleBarVisible = True self._buttonBoxVisible = True self._dialogButton = QToolButton(self) self._closeButton = QToolButton(self) self._scrollArea = QScrollArea(self) self._sizeGrip = QSizeGrip(self) self._sizeGrip.setFixedWidth(12) self._sizeGrip.setFixedHeight(12) self._leftSizeGrip = QSizeGrip(self) self._leftSizeGrip.setFixedWidth(12) self._leftSizeGrip.setFixedHeight(12) if buttons is None: buttons = QDialogButtonBox.NoButton self._buttonBox = QDialogButtonBox(buttons, Qt.Horizontal, self) self._buttonBox.setContentsMargins(3, 0, 3, 9) self._scrollArea.setWidgetResizable(True) self._scrollArea.setFrameShape(QScrollArea.NoFrame) self._scrollArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) palette = self.palette() self._scrollArea.setPalette(palette) self._dialogButton.setToolTip('Popout to Dialog') self._closeButton.setToolTip('Close Popup') for btn in (self._dialogButton, self._closeButton): btn.setAutoRaise(True) btn.setIconSize(QSize(14, 14)) btn.setMaximumSize(16, 16) # setup the icons icon = QIcon(projexui.resources.find('img/dialog.png')) self._dialogButton.setIcon(icon) icon = QIcon(projexui.resources.find('img/close.png')) self._closeButton.setIcon(icon) # define the ui hlayout = QHBoxLayout() hlayout.setSpacing(0) hlayout.addStretch(1) hlayout.addWidget(self._dialogButton) hlayout.addWidget(self._closeButton) hlayout.setContentsMargins(0, 0, 0, 0) hlayout2 = QHBoxLayout() hlayout2.addWidget(self._buttonBox) hlayout2.setContentsMargins(0, 0, 3, 0) vlayout = QVBoxLayout() vlayout.addLayout(hlayout) vlayout.addWidget(self._scrollArea) vlayout.addLayout(hlayout2) vlayout.setContentsMargins(3, 2, 3, 2) vlayout.setSpacing(0) self.setLayout(vlayout) self.setPositionLinkedTo(parent) # set default properties self.setAutoFillBackground(True) self.setBackgroundRole(QPalette.Window) self.setWindowTitle('Popup') self.setFocusPolicy(Qt.StrongFocus) self.setCurrentMode(XPopupWidget.Mode.Popup) # create connections self._dialogButton.clicked.connect(self.setDialogMode) self._closeButton.clicked.connect(self.reject) self._buttonBox.accepted.connect(self.accept) self._buttonBox.rejected.connect(self.reject) self._buttonBox.clicked.connect(self.handleButtonClick) def addButton(self, button, role=QDialogButtonBox.ActionRole): """ Adds a custom button to the button box for this popup widget. :param button | <QAbstractButton> || <str> :return <button> || None (based on if a button or string was given) """ return self._buttonBox.addButton(button, role) def adjustContentsMargins(self): """ Adjusts the contents for this widget based on the anchor and \ mode. """ anchor = self.anchor() mode = self.currentMode() # margins for a dialog if (mode == XPopupWidget.Mode.Dialog): self.setContentsMargins(0, 0, 0, 0) # margins for a top anchor point elif (anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.TopRight)): self.setContentsMargins(0, self.popupPadding() + 5, 0, 0) # margins for a bottom anchor point elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight)): self.setContentsMargins(0, 0, 0, self.popupPadding()) # margins for a left anchor point elif (anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.LeftBottom)): self.setContentsMargins(self.popupPadding(), 0, 0, 0) # margins for a right anchor point else: self.setContentsMargins(0, 0, self.popupPadding(), 0) self.adjustMask() def adjustMask(self): """ Updates the alpha mask for this popup widget. """ if self.currentMode() == XPopupWidget.Mode.Dialog: self.clearMask() return path = self.borderPath() bitmap = QBitmap(self.width(), self.height()) bitmap.fill(QColor('white')) with XPainter(bitmap) as painter: painter.setRenderHint(XPainter.Antialiasing) pen = QPen(QColor('black')) pen.setWidthF(0.75) painter.setPen(pen) painter.setBrush(QColor('black')) painter.drawPath(path) self.setMask(bitmap) def adjustSize(self): """ Adjusts the size of this popup to best fit the new widget size. """ widget = self.centralWidget() if widget is None: super(XPopupWidget, self).adjustSize() return widget.adjustSize() hint = widget.minimumSizeHint() size = widget.minimumSize() width = max(size.width(), hint.width()) height = max(size.height(), hint.height()) width += 20 height += 20 if self._buttonBoxVisible: height += self.buttonBox().height() + 10 if self._titleBarVisible: height += max(self._dialogButton.height(), self._closeButton.height()) + 10 curr_w = self.width() curr_h = self.height() # determine if we need to move based on our anchor anchor = self.anchor() if anchor & (self.Anchor.LeftBottom | self.Anchor.RightBottom | \ self.Anchor.BottomLeft | self.Anchor.BottomCenter | \ self.Anchor.BottomRight): delta_y = height - curr_h elif anchor & (self.Anchor.LeftCenter | self.Anchor.RightCenter): delta_y = (height - curr_h) / 2 else: delta_y = 0 if anchor & (self.Anchor.RightTop | self.Anchor.RightCenter | \ self.Anchor.RightTop | self.Anchor.TopRight): delta_x = width - curr_w elif anchor & (self.Anchor.TopCenter | self.Anchor.BottomCenter): delta_x = (width - curr_w) / 2 else: delta_x = 0 self.setMinimumSize(width, height) self.resize(width, height) pos = self.pos() pos.setX(pos.x() - delta_x) pos.setY(pos.y() - delta_y) self.move(pos) @Slot() def accept(self): """ Emits the accepted signal and closes the popup. """ self._result = 1 if not self.signalsBlocked(): self.accepted.emit() if self.autoCloseOnAccept(): self.close() def anchor(self): """ Returns the anchor point for this popup widget. :return <XPopupWidget.Anchor> """ return self._anchor def autoCalculateAnchor(self): """ Returns whether or not this popup should calculate the anchor point on popup based on the parent widget and the popup point. :return <bool> """ return self._autoCalculateAnchor def autoCloseOnAccept(self): """ Returns whether or not this popup widget manages its own close on accept behavior. :return <bool> """ return self._autoCloseOnAccept def autoCloseOnReject(self): """ Returns whether or not this popup widget manages its own close on reject behavior. :return <bool> """ return self._autoCloseOnReject def autoCloseOnFocusOut(self): """ Returns whether or not this popup widget should auto-close when the user clicks off the view. :return <bool> """ return self._autoCloseOnFocusOut def autoDefault(self): """ Returns whether or not clicking enter should default to the accept key. :return <bool> """ return self._autoDefault def borderPath(self): """ Returns the border path that will be drawn for this widget. :return <QPainterPath> """ path = QPainterPath() x = 1 y = 1 w = self.width() - 2 h = self.height() - 2 pad = self.popupPadding() anchor = self.anchor() # create a path for a top-center based popup if anchor == XPopupWidget.Anchor.TopCenter: path.moveTo(x, y + pad) path.lineTo(x + ((w / 2) - pad), y + pad) path.lineTo(x + (w / 2), y) path.lineTo(x + ((w / 2) + pad), y + pad) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a top-left based popup elif anchor == XPopupWidget.Anchor.TopLeft: path.moveTo(x, y + pad) path.lineTo(x + pad, y) path.lineTo(x + 2 * pad, y + pad) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a top-right based popup elif anchor == XPopupWidget.Anchor.TopRight: path.moveTo(x, y + pad) path.lineTo(x + w - 2 * pad, y + pad) path.lineTo(x + w - pad, y) path.lineTo(x + w, y + pad) path.lineTo(x + w, y + h) path.lineTo(x, y + h) path.lineTo(x, y + pad) # create a path for a bottom-left based popup elif anchor == XPopupWidget.Anchor.BottomLeft: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + 2 * pad, y + h - pad) path.lineTo(x + pad, y + h) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a south based popup elif anchor == XPopupWidget.Anchor.BottomCenter: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + ((w / 2) + pad), y + h - pad) path.lineTo(x + (w / 2), y + h) path.lineTo(x + ((w / 2) - pad), y + h - pad) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a bottom-right based popup elif anchor == XPopupWidget.Anchor.BottomRight: path.moveTo(x, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h - pad) path.lineTo(x + w - pad, y + h) path.lineTo(x + w - 2 * pad, y + h - pad) path.lineTo(x, y + h - pad) path.lineTo(x, y) # create a path for a right-top based popup elif anchor == XPopupWidget.Anchor.RightTop: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w, y + pad) path.lineTo(x + w - pad, y + 2 * pad) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a right-center based popup elif anchor == XPopupWidget.Anchor.RightCenter: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w - pad, y + ((h / 2) - pad)) path.lineTo(x + w, y + (h / 2)) path.lineTo(x + w - pad, y + ((h / 2) + pad)) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a right-bottom based popup elif anchor == XPopupWidget.Anchor.RightBottom: path.moveTo(x, y) path.lineTo(x + w - pad, y) path.lineTo(x + w - pad, y + h - 2 * pad) path.lineTo(x + w, y + h - pad) path.lineTo(x + w - pad, y + h) path.lineTo(x, y + h) path.lineTo(x, y) # create a path for a left-top based popup elif anchor == XPopupWidget.Anchor.LeftTop: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x + pad, y + 2 * pad) path.lineTo(x, y + pad) path.lineTo(x + pad, y) # create a path for an left-center based popup elif anchor == XPopupWidget.Anchor.LeftCenter: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x + pad, y + ((h / 2) + pad)) path.lineTo(x, y + (h / 2)) path.lineTo(x + pad, y + ((h / 2) - pad)) path.lineTo(x + pad, y) # create a path for a left-bottom based popup elif anchor == XPopupWidget.Anchor.LeftBottom: path.moveTo(x + pad, y) path.lineTo(x + w, y) path.lineTo(x + w, y + h) path.lineTo(x + pad, y + h) path.lineTo(x, y + h - pad) path.lineTo(x + pad, y + h - 2 * pad) path.lineTo(x + pad, y) return path def buttonBox(self): """ Returns the button box that is used to control this popup widget. :return <QDialogButtonBox> """ return self._buttonBox def centralWidget(self): """ Returns the central widget that is being used by this popup. :return <QWidget> """ return self._scrollArea.widget() def close(self): """ Closes the popup widget and central widget. """ widget = self.centralWidget() if widget and not widget.close(): return super(XPopupWidget, self).close() def closeEvent(self, event): widget = self.centralWidget() if widget and not widget.close() and \ self.currentMode() != XPopupWidget.Mode.ToolTip: event.ignore() else: super(XPopupWidget, self).closeEvent(event) self.closed.emit() def currentMode(self): """ Returns the current mode for this widget. :return <XPopupWidget.Mode> """ return self._currentMode @deprecatedmethod('XPopupWidget', 'Direction is no longer used, use anchor instead') def direction(self): """ Returns the current direction parameter for this widget. :return <XPopupWidget.Direction> """ anchor = self.anchor() if (anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.TopRight)): return XPopupWidget.Direction.North elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight)): return XPopupWidget.Direction.South elif (anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.LeftBottom)): return XPopupWidget.Direction.East else: return XPopupWidget.Direction.West def exec_(self, pos=None): self._result = 0 self.setWindowModality(Qt.ApplicationModal) self.popup(pos) while self.isVisible(): QApplication.processEvents() return self.result() def eventFilter(self, object, event): """ Processes when the window is moving to update the position for the popup if in popup mode. :param object | <QObject> event | <QEvent> """ if not self.isVisible(): return False links = self.positionLinkedTo() is_dialog = self.currentMode() == self.Mode.Dialog if object not in links: return False if event.type() == event.Close: self.close() return False if event.type() == event.Hide and not is_dialog: self.hide() return False if event.type() == event.Move and not is_dialog: deltaPos = event.pos() - event.oldPos() self.move(self.pos() + deltaPos) return False if self.currentMode() != self.Mode.ToolTip: return False if event.type() == event.Leave: pos = object.mapFromGlobal(QCursor.pos()) if (not object.rect().contains(pos)): self.close() event.accept() return True if event.type() in (event.MouseButtonPress, event.MouseButtonDblClick): self.close() event.accept() return True return False @Slot(QAbstractButton) def handleButtonClick(self, button): """ Handles the button click for this widget. If the Reset button was clicked, then the resetRequested signal will be emitted. All buttons will emit the buttonClicked signal. :param button | <QAbstractButton> """ if (self.signalsBlocked()): return if (button == self._buttonBox.button(QDialogButtonBox.Reset)): self.resetRequested.emit() self.buttonClicked.emit(button) def isAnimated(self): """ Returns whether or not the popup widget should animate its opacity when it is shown. :return <bool> """ return self._animated def isPossibleAnchor(self, anchor): return bool(anchor & self._possibleAnchors) def isResizable(self): """ Returns if this popup is resizable or not. :return <bool> """ return self._resizable def keyPressEvent(self, event): """ Looks for the Esc key to close the popup. :param event | <QKeyEvent> """ if (event.key() == Qt.Key_Escape): self.reject() event.accept() return elif (event.key() in (Qt.Key_Return, Qt.Key_Enter)): if self._autoDefault: self.accept() event.accept() return super(XPopupWidget, self).keyPressEvent(event) def mapAnchorFrom(self, widget, point): """ Returns the anchor point that best fits within the given widget from the inputed global position. :param widget | <QWidget> point | <QPoint> :return <XPopupWidget.Anchor> """ screen_geom = QtGui.QDesktopWidget(self).screenGeometry() # calculate the end rect for each position Anchor = self.Anchor w = self.width() h = self.height() possible_rects = { # top anchors Anchor.TopLeft: QtCore.QRect(point.x(), point.y(), w, h), Anchor.TopCenter: QtCore.QRect(point.x() - w / 2, point.y(), w, h), Anchor.TopRight: QtCore.QRect(point.x() - w, point.y(), w, h), # left anchors Anchor.LeftTop: QtCore.QRect(point.x(), point.y(), w, h), Anchor.LeftCenter: QtCore.QRect(point.x(), point.y() - h / 2, w, h), Anchor.LeftBottom: QtCore.QRect(point.x(), point.y() - h, w, h), # bottom anchors Anchor.BottomLeft: QtCore.QRect(point.x(), point.y() - h, w, h), Anchor.BottomCenter: QtCore.QRect(point.x() - w / 2, point.y() - h, w, h), Anchor.BottomRight: QtCore.QRect(point.x() - w, point.y() - h, w, h), # right anchors Anchor.RightTop: QtCore.QRect(point.x() - self.width(), point.y(), w, h), Anchor.RightCenter: QtCore.QRect(point.x() - self.width(), point.y() - h / 2, w, h), Anchor.RightBottom: QtCore.QRect(point.x() - self.width(), point.y() - h, w, h) } for anchor in (Anchor.TopCenter, Anchor.BottomCenter, Anchor.LeftCenter, Anchor.RightCenter, Anchor.TopLeft, Anchor.LeftTop, Anchor.BottomLeft, Anchor.LeftBottom, Anchor.TopRight, Anchor.RightTop, Anchor.BottomRight, Anchor.RightBottom): if not self.isPossibleAnchor(anchor): continue rect = possible_rects[anchor] if screen_geom.contains(rect): return anchor return self.anchor() def popup(self, pos=None): """ Pops up this widget at the inputed position. The inputed point should \ be in global space. :param pos | <QPoint> :return <bool> success """ if self._first and self.centralWidget() is not None: self.adjustSize() self._first = False if not self.signalsBlocked(): self.aboutToShow.emit() if not pos: pos = QCursor.pos() if self.currentMode() == XPopupWidget.Mode.Dialog and \ self.isVisible(): return False elif self.currentMode() == XPopupWidget.Mode.Dialog: self.setPopupMode() # auto-calculate the point if self.autoCalculateAnchor(): self.setAnchor(self.mapAnchorFrom(self.parent(), pos)) pad = self.popupPadding() # determine where to move based on the anchor anchor = self.anchor() # MODIFY X POSITION # align x-left if (anchor & (XPopupWidget.Anchor.TopLeft | XPopupWidget.Anchor.BottomLeft)): pos.setX(pos.x() - pad) # align x-center elif (anchor & (XPopupWidget.Anchor.TopCenter | XPopupWidget.Anchor.BottomCenter)): pos.setX(pos.x() - self.width() / 2) # align x-right elif ( anchor & (XPopupWidget.Anchor.TopRight | XPopupWidget.Anchor.BottomRight)): pos.setX(pos.x() - self.width() + pad) # align x-padded elif (anchor & (XPopupWidget.Anchor.RightTop | XPopupWidget.Anchor.RightCenter | XPopupWidget.Anchor.RightBottom)): pos.setX(pos.x() - self.width()) # MODIFY Y POSITION # align y-top if (anchor & (XPopupWidget.Anchor.LeftTop | XPopupWidget.Anchor.RightTop)): pos.setY(pos.y() - pad) # align y-center elif (anchor & (XPopupWidget.Anchor.LeftCenter | XPopupWidget.Anchor.RightCenter)): pos.setY(pos.y() - self.height() / 2) # align y-bottom elif (anchor & (XPopupWidget.Anchor.LeftBottom | XPopupWidget.Anchor.RightBottom)): pos.setY(pos.y() - self.height() + pad) # align y-padded elif ( anchor & (XPopupWidget.Anchor.BottomLeft | XPopupWidget.Anchor.BottomCenter | XPopupWidget.Anchor.BottomRight)): pos.setY(pos.y() - self.height()) self.adjustMask() self.move(pos) self.update() self.setUpdatesEnabled(True) if self.isAnimated(): anim = QPropertyAnimation(self, 'windowOpacity') anim.setParent(self) anim.setStartValue(0.0) anim.setEndValue(self.windowOpacity()) anim.setDuration(500) anim.finished.connect(anim.deleteLater) self.setWindowOpacity(0.0) else: anim = None self.show() if self.currentMode() != XPopupWidget.Mode.ToolTip: self.activateWindow() widget = self.centralWidget() if widget: self.centralWidget().setFocus() if anim: anim.start() if not self.signalsBlocked(): self.shown.emit() return True def paintEvent(self, event): """ Overloads the paint event to handle painting pointers for the popup \ mode. :param event | <QPaintEvent> """ # use the base technique for the dialog mode if self.currentMode() == XPopupWidget.Mode.Dialog: super(XPopupWidget, self).paintEvent(event) return # setup the coloring options palette = self.palette() with XPainter(self) as painter: pen = QPen(palette.color(palette.Window).darker(130)) pen.setWidthF(1.75) painter.setPen(pen) painter.setRenderHint(painter.Antialiasing) painter.setBrush(palette.color(palette.Window)) painter.drawPath(self.borderPath()) def popupPadding(self): """ Returns the amount of pixels to pad the popup arrow for this widget. :return <int> """ return self._popupPadding def possibleAnchors(self): return self._possibleAnchors def positionLinkedTo(self): """ Returns the widget that this popup is linked to for positional changes. :return [<QWidget>, ..] """ return self._positionLinkedTo @Slot() def reject(self): """ Emits the accepted signal and closes the popup. """ self._result = 0 if not self.signalsBlocked(): self.rejected.emit() if self.autoCloseOnReject(): self.close() def result(self): return self._result def resizeEvent(self, event): """ Resizes this widget and updates the mask. :param event | <QResizeEvent> """ self.setUpdatesEnabled(False) super(XPopupWidget, self).resizeEvent(event) self.adjustMask() self.setUpdatesEnabled(True) x = self.width() - self._sizeGrip.width() y = self.height() - self._sizeGrip.height() self._leftSizeGrip.move(0, y) self._sizeGrip.move(x, y) def scrollArea(self): """ Returns the scroll area widget for this popup. :return <QScrollArea> """ return self._scrollArea def setAnimated(self, state): """ Sets whether or not the popup widget should animate its opacity when it is shown. :param state | <bool> """ self._animated = state self.setAttribute(Qt.WA_TranslucentBackground, state) def setAutoCloseOnAccept(self, state): """ Sets whether or not the popup handles closing for accepting states. :param state | <bool> """ self._autoCloseOnAccept = state def setAutoCloseOnReject(self, state): """ Sets whether or not the popup handles closing for rejecting states. :param state | <bool> """ self._autoCloseOnReject = state def setAutoDefault(self, state): """ Sets whether or not the buttons should respond to defaulting options when the user is interacting with it. :param state | <bool> """ self._autoDefault = state for button in self.buttonBox().buttons(): button.setAutoDefault(state) button.setDefault(state) def setAnchor(self, anchor): """ Sets the anchor position for this popup widget to the inputed point. :param anchor | <XPopupWidget.Anchor> """ self._anchor = anchor self.adjustContentsMargins() def setAutoCalculateAnchor(self, state=True): """ Sets whether or not this widget should auto-calculate the anchor point based on the parent position when the popup is triggered. :param state | <bool> """ self._autoCalculateAnchor = state def setAutoCloseOnFocusOut(self, state): """ Sets whether or not this popup widget should auto-close when the user clicks off the view. :param state | <bool> """ self._autoCloseOnFocusOut = state self.updateModeSettings() def setCentralWidget(self, widget): """ Sets the central widget that will be used by this popup. :param widget | <QWidget> || None """ self._scrollArea.takeWidget() self._scrollArea.setWidget(widget) self.adjustSize() def setCurrentMode(self, mode): """ Sets the current mode for this dialog to the inputed mode. :param mode | <XPopupWidget.Mode> """ if (self._currentMode == mode): return self._currentMode = mode self.updateModeSettings() @Slot() def setDialogMode(self): """ Sets the current mode value to Dialog. """ self.setCurrentMode(XPopupWidget.Mode.Dialog) @deprecatedmethod('XPopupWidget', 'Direction is no longer used, use setAnchor instead') def setDirection(self, direction): """ Sets the direction for this widget to the inputed direction. :param direction | <XPopupWidget.Direction> """ if (direction == XPopupWidget.Direction.North): self.setAnchor(XPopupWidget.Anchor.TopCenter) elif (direction == XPopupWidget.Direction.South): self.setAnchor(XPopupWidget.Anchor.BottomCenter) elif (direction == XPopupWidget.Direction.East): self.setAnchor(XPopupWidget.Anchor.LeftCenter) else: self.setAnchor(XPopupWidget.Anchor.RightCenter) def setPalette(self, palette): """ Sets the palette for this widget and the scroll area. :param palette | <QPalette> """ super(XPopupWidget, self).setPalette(palette) self._scrollArea.setPalette(palette) def setPopupMode(self): """ Sets the current mode value to Popup. """ self.setCurrentMode(XPopupWidget.Mode.Popup) def setPopupPadding(self, padding): """ Sets the amount to pad the popup area when displaying this widget. :param padding | <int> """ self._popupPadding = padding self.adjustContentsMargins() def setPossibleAnchors(self, anchors): self._possibleAnchors = anchors def setPositionLinkedTo(self, widgets): """ Sets the widget that this popup will be linked to for positional changes. :param widgets | <QWidget> || [<QWidget>, ..] """ if type(widgets) in (list, set, tuple): new_widgets = list(widgets) else: new_widgets = [] widget = widgets while widget: widget.installEventFilter(self) new_widgets.append(widget) widget = widget.parent() self._positionLinkedTo = new_widgets def setResizable(self, state): self._resizable = state self._sizeGrip.setVisible(state) self._leftSizeGrip.setVisible(state) def setShowButtonBox(self, state): self._buttonBoxVisible = state self.buttonBox().setVisible(state) def setShowTitleBar(self, state): self._titleBarVisible = state self._dialogButton.setVisible(state) self._closeButton.setVisible(state) def setToolTipMode(self): """ Sets the mode for this popup widget to ToolTip """ self.setCurrentMode(XPopupWidget.Mode.ToolTip) def setVisible(self, state): super(XPopupWidget, self).setVisible(state) widget = self.centralWidget() if widget: widget.setVisible(state) def timerEvent(self, event): """ When the timer finishes, hide the tooltip popup widget. :param event | <QEvent> """ if self.currentMode() == XPopupWidget.Mode.ToolTip: self.killTimer(event.timerId()) event.accept() self.close() else: super(XPopupWidget, self).timerEvent(event) def updateModeSettings(self): mode = self.currentMode() is_visible = self.isVisible() # display as a floating dialog if mode == XPopupWidget.Mode.Dialog: self.setWindowFlags(Qt.Dialog | Qt.Tool) self.setAttribute(Qt.WA_TransparentForMouseEvents, False) self._closeButton.setVisible(False) self._dialogButton.setVisible(False) # display as a user tooltip elif mode == XPopupWidget.Mode.ToolTip: flags = Qt.Popup | Qt.FramelessWindowHint self.setWindowFlags(flags) self.setBackgroundRole(QPalette.Window) self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setShowTitleBar(False) self.setShowButtonBox(False) self.setFocusPolicy(Qt.NoFocus) # hide the scrollbars policy = Qt.ScrollBarAlwaysOff self._scrollArea.setVerticalScrollBarPolicy(policy) self._scrollArea.setHorizontalScrollBarPolicy(policy) # display as a popup widget else: flags = Qt.Popup | Qt.FramelessWindowHint if not self.autoCloseOnFocusOut(): flags |= Qt.Tool self.setWindowFlags(flags) self._closeButton.setVisible(self._titleBarVisible) self._dialogButton.setVisible(self._titleBarVisible) self.setBackgroundRole(QPalette.Window) self.adjustContentsMargins() if (is_visible): self.show() @staticmethod @deprecatedmethod('XPopupWidget', 'This method no longer has an effect as we are not '\ 'storing references to the tooltip.') def hideToolTip(key=None): """ Hides any existing tooltip popup widgets. :warning This method is deprecated! """ pass @staticmethod def showToolTip(text, point=None, anchor=None, parent=None, background=None, foreground=None, key=None, seconds=5): """ Displays a popup widget as a tooltip bubble. :param text | <str> point | <QPoint> || None anchor | <XPopupWidget.Mode.Anchor> || None parent | <QWidget> || None background | <QColor> || None foreground | <QColor> || None key | <str> || None seconds | <int> """ if point is None: point = QCursor.pos() if parent is None: parent = QApplication.activeWindow() if anchor is None and parent is None: anchor = XPopupWidget.Anchor.TopCenter # create a new tooltip widget widget = XPopupWidget(parent) widget.setToolTipMode() widget.setResizable(False) # create the tooltip label label = QLabel(text, widget) label.setOpenExternalLinks(True) label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) label.setMargin(3) label.setIndent(3) label.adjustSize() widget.setCentralWidget(label) # update the tip label.adjustSize() widget.adjustSize() palette = widget.palette() if not background: background = palette.color(palette.ToolTipBase) if not foreground: foreground = palette.color(palette.ToolTipText) palette.setColor(palette.Window, QColor(background)) palette.setColor(palette.WindowText, QColor(foreground)) widget.setPalette(palette) widget.centralWidget().setPalette(palette) if anchor is None: widget.setAutoCalculateAnchor(True) else: widget.setAnchor(anchor) widget.setAutoCloseOnFocusOut(True) widget.setAttribute(Qt.WA_DeleteOnClose) widget.popup(point) widget.startTimer(1000 * seconds) return widget
class XGanttWidgetItem(XTreeWidgetItem): """ Defines the main widget item class that contains information for both the tree and view widget items. """ ItemStyle = enum('Normal', 'Group', 'Milestone') def __init__(self, ganttWidget): super(XGanttWidgetItem, self).__init__() # set default properties self.setFixedHeight(ganttWidget.cellHeight()) for i in range(1, 20): self.setTextAlignment(i, Qt.AlignCenter) # define custom properties self._blockedAdjustments = {} self._viewItem = self.createViewItem() self._dateStart = QDate.currentDate() self._dateEnd = QDate.currentDate() self._allDay = True self._timeStart = QTime(0, 0, 0) self._timeEnd = QTime(23, 59, 59) self._name = '' self._properties = {} self._itemStyle = XGanttWidgetItem.ItemStyle.Normal self._useGroupStyleWithChildren = True self._dependencies = {} self._reverseDependencies = {} def addChild(self, item): """ Adds a new child item to this item. :param item | <XGanttWidgetItem> """ super(XGanttWidgetItem, self).addChild(item) item.sync() def addDependency(self, item): """ Creates a dependency for this item to the next item. This item will be treated as the source, the other as the target. :param item | <QGanttWidgetItem> """ if item in self._dependencies: return viewItem = XGanttDepItem(self, item) self._dependencies[item] = viewItem item._reverseDependencies[self] = viewItem self.syncDependencies() def adjustmentsBlocked(self, key): """ Returns whether or not hierarchy adjustments are being blocked. :param key | <str> :return <bool> """ return self._blockedAdjustments.get(nativestring(key), False) def adjustChildren(self, delta, secs=False): """ Shifts the children for this item by the inputed number of days. :param delta | <int> """ if self.adjustmentsBlocked('children'): return if self.itemStyle() != self.ItemStyle.Group: return if not delta: return for c in range(self.childCount()): child = self.child(c) child.blockAdjustments('range', True) if secs: dstart = child.dateTimeStart() dstart = dstart.addSecs(delta) child.setDateStart(dstart.date()) child.setTimeStart(dstart.time()) else: child.setDateStart(child.dateStart().addDays(delta)) child.blockAdjustments('range', False) def adjustRange(self, recursive=True): """ Adjust the start and end ranges for this item based on the limits from its children. This method will only apply to group items. :param recursive | <bool> """ if (self.adjustmentsBlocked('range')): return if (self.itemStyle() == self.ItemStyle.Group): dateStart = self.dateStart() dateEnd = self.dateEnd() first = True for c in range(self.childCount()): child = self.child(c) if (first): dateStart = child.dateStart() dateEnd = child.dateEnd() first = False else: dateStart = min(child.dateStart(), dateStart) dateEnd = max(child.dateEnd(), dateEnd) self._dateStart = dateStart self._dateEnd = dateEnd self.sync() if (self.parent() and recursive): self.parent().adjustRange(True) def blockAdjustments(self, key, state): """ Blocks the inputed adjustments for the given key type. :param key | <str> state | <bool> """ self._blockedAdjustments[nativestring(key)] = state def clearDependencies(self): """ Clears out all the dependencies from the scene. """ gantt = self.ganttWidget() if (not gantt): return scene = gantt.viewWidget().scene() for target, viewItem in self._dependencies.items(): target._reverseDependencies.pop(self) scene.removeItem(viewItem) self._dependencies.clear() def createViewItem(self): """ Returns a new XGanttViewItem to use with this item. :return <XGanttViewItem> """ return XGanttViewItem(self) def dateEnd(self): """ Return the end date for this gantt item. :return <QDate> """ return self._dateEnd def dateStart(self): """ Return the start date for this gantt item. :return <QDate> """ return self._dateStart def dateTimeEnd(self): """ Returns a merging of data from the date end with the time end. :return <QDateTime> """ return QDateTime(self.dateEnd(), self.timeEnd()) def dateTimeStart(self): """ Returns a merging of data from the date end with the date start. :return <QDateTime> """ return QDateTime(self.dateStart(), self.timeStart()) def dependencies(self): """ Returns a list of all the dependencies linked with this item. :return [<XGanttWidgetItem>, ..] """ return self._dependencies.keys() def duration(self): """ Returns the number of days this gantt item represents. :return <int> """ return 1 + self.dateStart().daysTo(self.dateEnd()) def ganttWidget(self): """ Returns the gantt widget that this item is linked to. :return <XGanttWidget> || None """ tree = self.treeWidget() if (not tree): return None from projexui.widgets.xganttwidget import XGanttWidget return projexui.ancestor(tree, XGanttWidget) def insertChild(self, index, item): """ Inserts a new item in the given index. :param index | <int> item | <XGanttWidgetItem> """ super(XGanttWidgetItem, self).insertChild(index, item) item.sync() def isAllDay(self): """ Returns whehter or not this item reflects an all day event. :return <bool> """ return self._allDay def itemStyle(self): """ Returns the item style information for this item. :return <XGanttWidgetItem.ItemStyle> """ if (self.useGroupStyleWithChildren() and self.childCount()): return XGanttWidgetItem.ItemStyle.Group return self._itemStyle def name(self): """ Returns the name for this gantt widget item. :return <str> """ return self._name def property(self, key, default=None): """ Returns the custom data that is stored on this object. :param key | <str> default | <variant> :return <variant> """ if key == 'Name': return self.name() elif key == 'Start': return self.dateStart() elif key == 'End': return self.dateEnd() elif key == 'Calendar Days': return self.duration() elif key == 'Work Days': return self.weekdays() elif key == 'Time Start': return self.timeStart() elif key == 'Time End': return self.timeEnd() elif key == 'All Day': return self.isAllDay() else: return self._properties.get(nativestring(key), default) def removeFromScene(self): """ Removes this item from the view scene. """ gantt = self.ganttWidget() if not gantt: return scene = gantt.viewWidget().scene() scene.removeItem(self.viewItem()) for target, viewItem in self._dependencies.items(): target._reverseDependencies.pop(self) scene.removeItem(viewItem) def setAllDay(self, state): """ Sets whether or not this item is an all day event. :param state | <bool> """ self._allDay = state def setDateEnd(self, date): """ Sets the date start value for this item. :param dateStart | <QDate> """ self._dateEnd = date def setDateStart(self, date): """ Sets the date start value for this item. :param dateStart | <QDate> """ self._dateStart = date def setDateTimeEnd(self, dtime): """ Sets the endiing date time for this gantt chart. :param dtime | <QDateTime> """ self._dateEnd = dtime.date() self._timeEnd = dtime.time() self._allDay = False def setDateTimeStart(self, dtime): """ Sets the starting date time for this gantt chart. :param dtime | <QDateTime> """ self._dateStart = dtime.date() self._timeStart = dtime.time() self._allDay = False def setDuration(self, duration): """ Sets the duration for this item to the inputed duration. :param duration | <int> """ if duration < 1: return False self.setDateEnd(self.dateStart().addDays(duration - 1)) return True def setItemStyle(self, itemStyle): """ Sets the item style that will be used for this widget. If you are trying to set a style on an item that has children, make sure to turn off the useGroupStyleWithChildren option, or it will always display as a group. :param itemStyle | <XGanttWidgetItem.ItemStyle> """ self._itemStyle = itemStyle # initialize the group icon for group style if itemStyle == XGanttWidgetItem.ItemStyle.Group and \ self.icon(0).isNull(): ico = projexui.resources.find('img/folder_close.png') expand_ico = projexui.resources.find('img/folder_open.png') self.setIcon(0, QIcon(ico)) self.setExpandedIcon(0, QIcon(expand_ico)) def setName(self, name): """ Sets the name of this widget item to the inputed name. :param name | <str> """ self._name = name tree = self.treeWidget() if tree: col = tree.column('Name') if col != -1: self.setData(col, Qt.EditRole, wrapVariant(name)) def setProperty(self, key, value): """ Sets the custom property for this item's key to the inputed value. If the widget has a column that matches the inputed key, then the value will be added to the tree widget as well. :param key | <str> value | <variant> """ if key == 'Name': self.setName(value) elif key == 'Start': self.setDateStart(value) elif key == 'End': self.setDateEnd(value) elif key == 'Calendar Days': self.setDuration(value) elif key == 'Time Start': self.setTimeStart(value) elif key == 'Time End': self.setTimeEnd(value) elif key == 'All Day': self.setAllDay(value) elif key == 'Workadys': pass else: self._properties[nativestring(key)] = value tree = self.treeWidget() if tree: col = tree.column(key) if col != -1: self.setData(col, Qt.EditRole, wrapVariant(value)) def setTimeEnd(self, time): """ Sets the ending time that this item will use. To properly use a timed item, you need to also set this item's all day property to False. :sa setAllDay :param time | <QTime> """ self._timeEnd = time self._allDay = False def setTimeStart(self, time): """ Sets the starting time that this item will use. To properly use a timed item, you need to also set this item's all day property to False. :sa setAllDay :param time | <QTime> """ self._timeStart = time self._allDay = False def setUseGroupStyleWithChildren(self, state): """ Sets whether or not this item should display as group style when it has children. This will override whatever is set in the style property for the item. :return <bool> """ self._useGroupStyleWithChildren = state def sync(self, recursive=False): """ Syncs the information from this item to the tree and view. """ self.syncTree(recursive=recursive) self.syncView(recursive=recursive) def syncDependencies(self, recursive=False): """ Syncs the dependencies for this item to the view. :param recurisve | <bool> """ scene = self.viewItem().scene() if not scene: return visible = self.viewItem().isVisible() depViewItems = self._dependencies.values() depViewItems += self._reverseDependencies.values() for depViewItem in depViewItems: if not depViewItem.scene(): scene.addItem(depViewItem) depViewItem.rebuild() depViewItem.setVisible(visible) if recursive: for c in range(self.childCount()): self.child(c).syncDependencies(recursive=True) def syncTree(self, recursive=False, blockSignals=True): """ Syncs the information from this item to the tree. """ tree = self.treeWidget() # sync the tree information if not tree: return items = [self] if recursive: items += list(self.children(recursive=True)) if blockSignals and not tree.signalsBlocked(): blocked = True tree.blockSignals(True) else: blocked = False date_format = self.ganttWidget().dateFormat() for item in items: for c, col in enumerate(tree.columns()): value = item.property(col, '') item.setData(c, Qt.EditRole, wrapVariant(value)) if blocked: tree.blockSignals(False) def syncView(self, recursive=False): """ Syncs the information from this item to the view. """ # update the view widget gantt = self.ganttWidget() tree = self.treeWidget() if not gantt: return vwidget = gantt.viewWidget() scene = vwidget.scene() cell_w = gantt.cellWidth() tree_offset_y = tree.header().height() + 1 tree_offset_y += tree.verticalScrollBar().value() # collect the items to work on items = [self] if recursive: items += list(self.children(recursive=True)) for item in items: # grab the view item from the gantt item vitem = item.viewItem() if not vitem.scene(): scene.addItem(vitem) # make sure the item should be visible if item.isHidden() or not tree: vitem.hide() continue vitem.show() tree_rect = tree.visualItemRect(item) tree_y = tree_rect.y() + tree_offset_y tree_h = tree_rect.height() # check to see if this item is hidden if tree_rect.height() == 0: vitem.hide() continue if gantt.timescale() in (gantt.Timescale.Minute, gantt.Timescale.Hour, gantt.Timescale.Day): dstart = item.dateTimeStart() dend = item.dateTimeEnd() view_x = scene.datetimeXPos(dstart) view_r = scene.datetimeXPos(dend) view_w = view_r - view_x else: view_x = scene.dateXPos(item.dateStart()) view_w = item.duration() * cell_w # determine the % off from the length based on this items time if not item.isAllDay(): full_day = 24 * 60 * 60 # full days worth of seconds # determine the start offset start = item.timeStart() start_day = (start.hour() * 60 * 60) start_day += (start.minute() * 60) start_day += (start.second()) offset_start = (start_day / float(full_day)) * cell_w # determine the end offset end = item.timeEnd() end_day = (end.hour() * 60 * 60) end_day += (start.minute() * 60) end_day += (start.second() + 1) # forces at least 1 sec offset_end = ((full_day - end_day) / float(full_day)) offset_end *= cell_w # update the xpos and widths view_x += offset_start view_w -= (offset_start + offset_end) view_w = max(view_w, 5) vitem.setSyncing(True) vitem.setPos(view_x, tree_y) vitem.setRect(0, 0, view_w, tree_h) vitem.setSyncing(False) # setup standard properties flags = vitem.ItemIsSelectable flags |= vitem.ItemIsFocusable if item.flags() & Qt.ItemIsEditable: flags |= vitem.ItemIsMovable vitem.setFlags(flags) item.syncDependencies() def takeChild(self, index): """ Removes the child at the given index from this item. :param index | <int> """ item = super(XGanttWidgetItem, self).takeChild(index) if item: item.removeFromScene() return item def takeDependency(self, item): """ Removes the dependency between the this item and the inputed target. :param item | <XGanttWidgetItem> """ if (not item in self._dependencies): return item._reverseDependencies.pop(self) viewItem = self._dependencies.pop(item) scene = viewItem.scene() if (scene): scene.removeItem(viewItem) def timeEnd(self): """ Returns the ending time that will be used for this item. If it is an all day event, then the time returned will be 23:59:59. :return <QTime> """ if (self.isAllDay()): return QTime(23, 59, 59) return self._timeEnd def timeStart(self): """ Returns the starting time that will be used for this item. If it is an all day event, then the time returned will be 0:0:0 :return <QTime> """ if (self.isAllDay()): return QTime(0, 0, 0) return self._timeStart def useGroupStyleWithChildren(self): """ Returns whether or not this item should display as group style when it has children. This will override whatever is set in the style property for the item. :return <bool> """ return self._useGroupStyleWithChildren def viewChanged(self, start, end): """ Called when the view item is changed by the user. :param start | <QDate> || <QDateTime> end | <QDate> || <QDateTime> """ if type(start) == QDate: delta = self._dateStart.daysTo(start) self._dateStart = start self._dateEnd = end self.adjustChildren(delta) else: delta = self._dateStart.secsTo(start) self._dateStart = start.date() self._timeStart = start.time() self._dateEnd = end.date() self._timeEnd = end.time() self.adjustChildren(delta, secs=True) self.adjustRange() self.syncDependencies() self.syncTree() def viewItem(self): """ Returns the view item that is linked with this item. :return <XGanttViewItem> """ if type(self._viewItem).__name__ == 'weakref': return self._viewItem() return self._viewItem def weekdays(self): """ Returns the number of weekdays this item has. :return <int> """ if self.itemStyle() == self.ItemStyle.Group: out = 0 for i in range(self.childCount()): out += self.child(i).weekdays() return out else: dstart = self.dateStart().toPyDate() dend = self.dateEnd().toPyDate() return projex.dates.weekdays(dstart, dend)
class SearchThesaurus(AddonManager): """ Defines a global thesaurus system for searching. This will allow additional keyword lookups based on synonyms. Thesaurus can be define on a per API and per Table system. """ Flags = enum('FindSingular', 'FindPlural', 'FindInherited') def __init__(self, wordsets=None, parent=None): self._wordsets = [] self._phrases = set() self._parent = parent if wordsets is not None: self.update(wordsets) def add(self, word, synonym): """ Adds a new synonym for a given word. :param word | <str> synonym | <str> """ word = nstr(word) synonym = nstr(synonym) # lookup wordsets for wordset in self._wordsets: if word in wordset: wordset.add(synonym) # define new wordset self._wordsets.append({word, synonym}) def addset(self, wordset): """ Defines a wordset that will be used as a grouping of synonyms within the thesaurus. A wordset is a comma separated list of synonyms. :param wordset | <str> || <list> || <set> """ if type(wordset) in (list, tuple, set): wordset = set([nstr(word) for word in wordset]) else: wordset = set(nstr(wordset).split(',')) self._wordsets.append(wordset) def addPhrase(self, pattern): """ Phrases define groups of words that will create a singular search term within a search pattern. For instance, "is not" will be treated as single term, so instead of looking for "is" and "not", the phrase "is not" will be matched. The inputted pattern can be a regular expression, or hard set of terms. :param pattern | <str> """ self._phrases.add(pattern) def clear(self): """ Clears out the data for this thesaurus. """ self._phrases = set() self._wordsets = [] def expand(self, wordset, flags=Flags.all()): """ Expands a given wordset based on the synonyms per word in the set. :param wordset | <list> || <tuple> || <set> """ output = set() for word in wordset: output = output.union(self.synonyms(word, flags)) return output def parent(self): """ Returns the parent thesaurus for this instance if any is defined. :return <orb.SearchThesaurus> """ return self._parent def phrases(self): """ Returns a list of phrases for the searching pattern. :return [<str>, ..] """ out = set(self._phrases) if self.parent(): out = out.union(self.parent().phrases()) return out def load(self, xml): """ Loads the thesaurus information from the inputted XML file. :param filename | <str> """ try: xroot = ElementTree.parse(xml).getroot() except StandardError: try: xroot = ElementTree.fromstring(xml) except StandardError: return False # load wordsets xwordsets = xroot.find('wordsets') if xwordsets is not None: for xwordset in xwordsets: self.addset(xwordset.text) # load patterns xphrases = xroot.find('phrases') if xphrases is not None: for xphrase in xphrases: self.addPhrase(xphrase.text) return True def remove(self, word, synonym): """ Removes a given synonym from the inputted word in a wordset. :param word | <str> synonym | <str> """ word = nstr(word) synonym = nstr(synonym) for wordset in self._wordsets: if word in wordset: try: wordset.remove(synonym) except KeyError: pass return def removePhrase(self, pattern): """ Removes the given phrasing pattern for this search term. :param pattern | <str> """ try: self._phrases.remove(pattern) except KeyError: pass def setParent(self, parent): """ Sets the parent thesaurus for this instance if any is defined. :param parent | <orb.SearchThesaurus> || None """ self._parent = parent def splitterms(self, text): """ Splits the inputted search text into search terms. This will use the phrasing patterns within this thesaurus to determine groups of words. :param text | <str> :return [<str>, ..] """ text = nstr(text) repl = [] # pre-process all phrases into their own groups for phrase in self.phrases(): grp = re.search(phrase, text) while grp and grp.group(): result = grp.group() text = text.replace(result, '`REGEXGRP{0}`'.format(len(repl))) repl.append(result) grp = re.search(phrase, text) # split the terms into their own words, grouped together with phrases output = [] for term in text.split(): term = term.strip() grp = re.match('`REGEXGRP(\d+)`(.*)', term) if grp: index, remain = grp.groups() term = repl[int(index)] + remain output.append(term) return output def synonyms(self, word, flags=Flags.all()): """ Looks up the synonyms for the given word within this thesaurus system. :param word | <str> :return set(<str>, ..) """ word = nstr(word) output = {word} # find matching words for wordset in self._wordsets: if output.intersection(wordset): output = output.union(wordset) # lookup inherited synonyms if self.parent() and flags & SearchThesaurus.Flags.FindInherited: output = output.union(self.parent().synonyms(word, flags=flags)) return output def update(self, wordsets): """ Updates the records for this thesaurus' wordsets with the inputted list of sets. :param wordsets | [<str>, ..] """ for wordset in wordsets: self.addset(wordset) def updatePhrases(self, phrases): """ Updates the phrase sets for this thesaurus with the inputted list of phrases. :param phrases | [<str>, ..] """ for phrase in phrases: self.addPhrase(phrase)
QGraphicsPathItem, \ QPainterPath, \ QPen, \ QPolygonF, \ QTransform,\ QApplication,\ QGraphicsTextItem,\ QFontMetrics from projex.enum import enum from projexui.widgets.xnodewidget.xnodelayer import XNodeLayer # create the common enumerated types needed XConnectionStyle = enum( 'Linear', 'Block', 'Smooth') XConnectionLocation = enum( 'Top', 'Bottom', 'Left', 'Right' ) class XNodeConnection( QGraphicsPathItem ): """ Defines the base graphics item class that is used to draw a connection between two nodes. """ def __init__( self, scene ): self._visible = True
class QueryCompound(object): """ Defines combinations of queries via either the AND or OR mechanism. """ Op = enum('And', 'Or') def __contains__(self, value): """ Returns whether or not the query compound contains a query for the inputted column name. :param value | <variant> :return <bool> :usage |>>> from orb import Query as Q |>>> q = Q('testing') == True |>>> 'testing' in q |True |>>> 'name' in q |False """ for query in self._queries: if value in query: return True return False def __nonzero__(self): return not self.isNull() def __str__(self): """ Returns the string representation for this query instance :sa toString """ return self.toString() def __init__(self, *queries, **options): self._queries = queries self._op = options.get('op', QueryCompound.Op.And) self._name = nstr(options.get('name', '')) def __and__(self, other): """ Creates a new compound query using the QueryCompound.Op.And type. :param other <Query> || <QueryCompound> :return <QueryCompound> :sa and_ :usage |>>> from orb import Query as Q |>>> query = (Q('test') != 1) & (Q('name') == 'Eric') |>>> print query |(test does_not_equal 1 and name is Eric) """ return self.and_(other) def __hash__(self): return hash(self.toXmlString()) def __neg__(self): """ Negates the current state of the query. :sa negate :return self :usage |>>> from orb import Query as Q |>>> query = (Q('test') == 1) & (Q('name') == 'Eric') |>>> print -query |NOT (test is 1 and name is Eric) """ return self.negated() def __or__(self, other): """ Creates a new compound query using the QueryCompound.Op.Or type. :param other <Query> || <QueryCompound> :return <QueryCompound> :sa or_ :usage |>>> from orb import Query as Q |>>> query = (Q('test') != 1) | (Q('name') == 'Eric') |>>> print query |(test isNot 1 or name is Eric) """ return self.or_(other) def and_(self, other): """ Creates a new compound query using the QueryCompound.Op.And type. :param other <Query> || <QueryCompound> :return <QueryCompound> :sa __and__ :usage |>>> from orb import Query as Q |>>> query = (Q('test') != 1).and_((Q('name') == 'Eric') |>>> print query |(test isNot 1 and name is Eric) """ if other is None or other.isNull(): return self elif self.isNull(): return other # grow this objects list if the operator types are the same if self.operatorType() == QueryCompound.Op.And: queries = list(self._queries) queries.append(other) opts = {'op': QueryCompound.Op.And} return QueryCompound(*queries, **opts) # create a new compound return QueryCompound(self, other, op=QueryCompound.Op.And) def copy(self): """ Returns a copy of this query compound. :return <QueryCompound> """ out = QueryCompound() out._queries = [q.copy() for q in self._queries] out._op = self._op return out def columns(self, schema=None): """ Returns any columns used within this query. :return [<orb.Column>, ..] """ output = [] for query in self.queries(): output += query.columns(schema=schema) return list(set(output)) def expandShortcuts(self, basetable=None): """ Expands any shortcuts that were created for this query. Shortcuts provide the user access to joined methods using the '.' accessor to access individual columns for referenced tables. :param basetable | <orb.Table> || None :usage |>>> from orb import Query as Q |>>> # lookup the 'username' of foreign key 'user' |>>> Q('user.username') == 'bob.smith' :return <orb.Query> || <orb.QueryCompound> """ output = self.copy() queries = [] rset = None for query in output._queries: query = query.expandShortcuts(basetable) # chain together joins into sub-queries if isinstance(query, orb.Query) and \ isinstance(query.value(), orb.Query) and \ query.value().table(basetable) != query.table(basetable): columns = [query.value().columnName() ] if query.value().columnName() else ['id'] new_rset = query.value().table(basetable).select( columns=columns) query = query.copy() query.setOperatorType(query.Op.IsIn) query.setValue(new_rset) if rset is not None and rset.table() == query.table(basetable): rset.setQuery(query & rset.query()) else: queries.append(query) rset = new_rset # update the existing recordset in the chain elif rset is not None and \ ((isinstance(query, orb.Query) and rset.table() == query.table(basetable)) or (isinstance(query, orb.QueryCompound) and rset.table() in query.tables(basetable))): rset.setQuery(query & rset.query()) # clear out the chain and move on to the next query set else: rset = None queries.append(query) output._queries = queries return output def findValue(self, column, instance=1): """ Looks up the value for the inputted column name for the given instance. If the instance == 1, then this result will return the value and a 0 instance count, otherwise it will decrement the instance for a matching column to indicate it was found, but not at the desired instance. :param column | <str> instance | <int> :return (<bool> success, <variant> value, <int> instance) """ for query in self.queries(): success, value, instance = query.findValue(column, instance) if success: return success, value, 0 return False, None, instance def isNull(self): """ Returns whether or not this join is empty or not. :return <bool> """ am_null = True for query in self._queries: if not query.isNull(): am_null = False break return am_null def name(self): return self._name def negated(self): """ Negates this instance and returns it. :return self """ qcompound = QueryCompound(*self._queries) qcompound._op = QueryCompound.Op.And if self._op == QueryCompound.Op.Or else QueryCompound.Op.Or return qcompound def operatorType(self): """ Returns the operator type for this compound. :return <QueryCompound.Op> """ return self._op def or_(self, other): """ Creates a new compound query using the QueryCompound.Op.Or type. :param other <Query> || <QueryCompound> :return <QueryCompound> :sa or_ :usage |>>> from orb import Query as Q |>>> query = (Q('test') != 1).or_(Q('name') == 'Eric') |>>> print query |(test isNot 1 or name is Eric) """ if other is None or other.isNull(): return self elif self.isNull(): return other # grow this objects list if the operator types are the same if self.operatorType() == QueryCompound.Op.Or: queries = list(self._queries) queries.append(other) opts = {'op': QueryCompound.Op.Or} return QueryCompound(*queries, **opts) return QueryCompound(self, other, op=QueryCompound.Op.Or) def queries(self): """ Returns the list of queries that are associated with this compound. :return <list> [ <Query> || <QueryCompound>, .. ] """ return self._queries def removed(self, columnName): """ Removes the query containing the inputted column name from this query set. :param columnName | <str> :return <QueryCompound> """ out = self.copy() new_queries = [] for query in out._queries: new_queries.append(query.removed(columnName)) out._queries = new_queries return out def setName(self, name): self._name = nstr(name) def setOperatorType(self, op): """ Sets the operator type that this compound that will be used when joining together its queries. :param op <QueryCompound.Op> """ self._op = op def tables(self, base=None): """ Returns the tables that this query is referencing. :return [ <subclass of Table>, .. ] """ output = [] for query in self._queries: output += query.tables(base=base) return list(set(output)) def toString(self): """ Returns this query instance as a semi readable language query. :warning This method will NOT return a valid SQL statement. The backend classes will determine how to convert the Query instance to whatever lookup code they need it to be. :return <str> """ optypestr = QueryCompound.Op[self.operatorType()] op_type = ' %s ' % projex.text.underscore(optypestr) query = '(%s)' % op_type.join([q.toString() for q in self.queries()]) return query def toDict(self): """ Creates a dictionary representation of this query. :return <dict> """ output = {} if self.isNull(): return output output['type'] = 'compound' output['name'] = self.name() output['op'] = self.operatorType() queries = [] for query in self.queries(): queries.append(query.toDict()) output['queries'] = queries return output def toXml(self, xparent=None): """ Returns this query as an XML value. :param xparent | <xml.etree.ElementTree.Element> || None :return <xml.etree.ElementTree.Element> """ if self.isNull(): return None if xparent is None: xquery = ElementTree.Element('compound') else: xquery = ElementTree.SubElement(xparent, 'compound') xquery.set('name', nstr(self.name())) xquery.set('op', nstr(self.operatorType())) for query in self.queries(): query.toXml(xquery) return xquery def toXmlString(self, indented=False): """ Returns this query as an XML string. :param indented | <bool> :return <str> """ xml = self.toXml() if indented: projex.text.xmlindent(xml) return ElementTree.tostring(xml) def validate(self, record, table=None): """ Validates the inputted record against this query compound. :param record | <orb.Table> """ op = self._op queries = self.queries() if not queries: return False for query in queries: valid = query.validate(record, table) if op == QueryCompound.Op.And and not valid: return False elif op == QueryCompound.Op.Or and valid: return True return op == QueryCompound.Op.And @staticmethod def build(compound, queries): """ Builds a compound based on the inputted compound string. This should look like: ((QUERY_1 and QUERY_2) or (QUERY_3 and QUERY_4)). The inputted query dictionary should correspond with the keys in the string. This method will be called as part of the Query.fromString method and probably never really needs to be called otherwise. :param compound | <str> queries | [<Query>, ..] :return <Query> || <QueryCompound> """ indexStack = [] compounds = {} new_text = projex.text.decoded(compound) for index, char in enumerate(projex.text.decoded(compound)): # open a new compound if char == '(': indexStack.append(index) # close a compound elif char == ')' and indexStack: openIndex = indexStack.pop() match = compound[openIndex + 1:index] if not match: continue # create the new compound new_compound = QueryCompound.build(match, queries) key = 'QCOMPOUND_%i' % (len(compounds) + 1) compounds[key] = new_compound new_text = new_text.replace('(' + match + ')', key) new_text = new_text.strip('()') query = orb.Query() last_op = 'and' for section in new_text.split(): section = section.strip('()') # merge a compound if section in compounds: section_q = compounds[section] elif section in queries: section_q = queries[section] elif section in ('and', 'or'): last_op = section continue else: log.warning('Missing query section: %s', section) continue if query is None: query = section_q elif last_op == 'and': query &= section_q else: query |= section_q return query @staticmethod def fromDict(data): if data.get('type') != 'compound': return orb.Query.fromDict(data) compound = QueryCompound() compound.setName(data.get('name', '')) compound.setOperatorType(int(data.get('op', '1'))) queries = [] for subdata in data.get('queries', []): queries.append(orb.Query.fromDict(subdata)) compound._queries = queries return compound @staticmethod def fromString(querystr): """ Returns a new compound from the inputted query string. This simply calls the Query.fromString method, as the two work the same. :param querystr | <str> :return <Query> || <QueryCompound> || None """ return orb.Query.fromString(querystr) @staticmethod def fromXml(xquery): if xquery.tag == 'query': return orb.Query.fromXml(xquery) compound = QueryCompound() compound.setName(xquery.get('name', '')) compound.setOperatorType(int(xquery.get('op', '1'))) queries = [] for xsubquery in xquery: queries.append(orb.Query.fromXml(xsubquery)) compound._queries = queries return compound @staticmethod def fromXmlString(xquery_str): """ Returns a query from the XML string. :param xquery_str | <str> :return <orb.Query> || <orb.QueryCompound> """ try: xml = ElementTree.fromstring(xquery_str) except ExpatError: return orb.Query() return orb.Query.fromXml(xml) @staticmethod def typecheck(obj): """ Returns whether or not the inputted object is a QueryCompound object. :param obj <variant> :return ,bool> """ return isinstance(obj, QueryCompound)