class FeatureEditor(QtGui.QFrame): FUNCTIONS = dict(chain([(key, val) for key, val in math.__dict__.items() if not key.startswith("_")], [("str", str)])) featureChanged = Signal() featureEdited = Signal() modifiedChanged = Signal(bool) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) layout = QtGui.QFormLayout( fieldGrowthPolicy=QtGui.QFormLayout.ExpandingFieldsGrow ) layout.setContentsMargins(0, 0, 0, 0) self.nameedit = QtGui.QLineEdit( placeholderText="Name...", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) ) self.expressionedit = QtGui.QLineEdit( placeholderText="Expression..." ) self.attrs_model = itemmodels.VariableListModel( ["Select Feature"], parent=self) self.attributescb = QtGui.QComboBox( minimumContentsLength=16, sizeAdjustPolicy=QtGui.QComboBox.AdjustToMinimumContentsLengthWithIcon, sizePolicy=QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) ) self.attributescb.setModel(self.attrs_model) sorted_funcs = sorted(self.FUNCTIONS) self.funcs_model = itemmodels.PyListModelTooltip() self.funcs_model.setParent(self) self.funcs_model[:] = chain(["Select Function"], sorted_funcs) self.funcs_model.tooltips[:] = chain( [''], [self.FUNCTIONS[func].__doc__ for func in sorted_funcs]) self.functionscb = QtGui.QComboBox( minimumContentsLength=16, sizeAdjustPolicy=QtGui.QComboBox.AdjustToMinimumContentsLengthWithIcon, sizePolicy=QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)) self.functionscb.setModel(self.funcs_model) hbox = QtGui.QHBoxLayout() hbox.addWidget(self.attributescb) hbox.addWidget(self.functionscb) layout.addRow(self.nameedit, self.expressionedit) layout.addRow(self.tr(""), hbox) self.setLayout(layout) self.nameedit.editingFinished.connect(self._invalidate) self.expressionedit.textChanged.connect(self._invalidate) self.attributescb.currentIndexChanged.connect(self.on_attrs_changed) self.functionscb.currentIndexChanged.connect(self.on_funcs_changed) self._modified = False def setModified(self, modified): if not type(modified) is bool: raise TypeError if self._modified != modified: self._modified = modified self.modifiedChanged.emit(modified) def modified(self): return self._modified modified = Property(bool, modified, setModified, notify=modifiedChanged) def setEditorData(self, data, domain): self.nameedit.setText(data.name) self.expressionedit.setText(data.expression) self.setModified(False) self.featureChanged.emit() self.attrs_model[:] = ["Select Feature"] if domain: self.attrs_model[:] += chain(domain.attributes, domain.class_vars, domain.metas) def editorData(self): return FeatureDescriptor(name=self.nameedit.text(), expression=self.nameedit.text()) def _invalidate(self): self.setModified(True) self.featureEdited.emit() self.featureChanged.emit() def on_attrs_changed(self): index = self.attributescb.currentIndex() if index > 0: attr = sanitized_name(self.attrs_model[index].name) self.insert_into_expression(attr) self.attributescb.setCurrentIndex(0) def on_funcs_changed(self): index = self.functionscb.currentIndex() if index > 0: func = self.funcs_model[index] if func in ["atan2", "fmod", "ldexp", "log", "pow", "copysign", "hypot"]: self.insert_into_expression(func + "(,)") self.expressionedit.cursorBackward(False, 2) elif func in ["e", "pi"]: self.insert_into_expression(func) else: self.insert_into_expression(func + "()") self.expressionedit.cursorBackward(False) self.functionscb.setCurrentIndex(0) def insert_into_expression(self, what): cp = self.expressionedit.cursorPosition() ct = self.expressionedit.text() text = ct[:cp] + what + ct[cp:] self.expressionedit.setText(text) self.expressionedit.setFocus()
class PreviewDialog(QDialog): """A Dialog for selecting an item from a PreviewItem. """ currentIndexChanged = Signal(int) def __init__(self, parent=None, flags=Qt.WindowFlags(0), model=None, **kwargs): QDialog.__init__(self, parent, flags, **kwargs) self.__setupUi() if model is not None: self.setModel(model) def __setupUi(self): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0) heading = self.tr("Preview") heading = "<h3>{0}</h3>".format(heading) self.__heading = QLabel(heading, self, objectName="heading") self.__heading.setContentsMargins(12, 12, 12, 0) self.__browser = previewbrowser.PreviewBrowser(self) self.__buttons = QDialogButtonBox(QDialogButtonBox.Open | \ QDialogButtonBox.Cancel, Qt.Horizontal,) self.__buttons.button(QDialogButtonBox.Open).setAutoDefault(True) # Set the Open dialog as disabled until the current index changes self.__buttons.button(QDialogButtonBox.Open).setEnabled(False) # The QDialogButtonsWidget messes with the layout if it is # contained directly in the QDialog. So we create an extra # layer of indirection. buttons = QWidget(objectName="button-container") buttons_l = QVBoxLayout() buttons_l.setContentsMargins(12, 0, 12, 12) buttons.setLayout(buttons_l) buttons_l.addWidget(self.__buttons) layout.addWidget(self.__heading) layout.addWidget(self.__browser) layout.addWidget(buttons) self.__buttons.accepted.connect(self.accept) self.__buttons.rejected.connect(self.reject) self.__browser.currentIndexChanged.connect( self.__on_currentIndexChanged ) self.__browser.activated.connect(self.__on_activated) layout.setSizeConstraint(QVBoxLayout.SetFixedSize) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def setItems(self, items): """Set the items (a list of strings) for preview/selection. """ model = QStringListModel(items) self.setModel(model) def setModel(self, model): """Set the model for preview/selection. """ self.__browser.setModel(model) def model(self): """Return the model. """ return self.__browser.model() def currentIndex(self): return self.__browser.currentIndex() def setCurrentIndex(self, index): """Set the current selected (shown) index. """ self.__browser.setCurrentIndex(index) def setHeading(self, heading): """Set `heading` as the heading string ('<h3>Preview</h3>' by default). """ self.__heading.setText(heading) def heading(self): """Return the heading string. """ def __on_currentIndexChanged(self, index): button = self.__buttons.button(QDialogButtonBox.Open) button.setEnabled(index >= 0) self.currentIndexChanged.emit(index) def __on_activated(self, index): if self.currentIndex() != index: self.setCurrentIndex(index) self.accept()
class BonusTrait(StandardTrait): """ @brief Speichert zusätzlich noch den Bonuswert der Eigenschaft. """ bonusSpecialtiesChanged = Signal(object) bonusValueChanged = Signal(object) def __init__(self, character, name="", value=0, parent=None): super(BonusTrait, self).__init__(character, name, value, parent) self.__bonusValue = 0 self.__bonusSpecialties = [] self.valueChanged.connect(self.emitTotalvalueChanged) self.bonusValueChanged.connect(self.emitTotalvalueChanged) self.specialtiesChanged.connect(self.emitTotalspecialtiesChanged) self.bonusSpecialtiesChanged.connect(self.emitTotalspecialtiesChanged) @property def totalvalue(self): """ Der Eigenschaftswert mit Berücksichtigung des Bonuswertes. """ return self.value + self.bonusValue def emitTotalvalueChanged(self, value): self.totalvalueChanged.emit(self.totalvalue) def __getBonusValue(self): return self.__bonusValue def __setBonusValue(self, bonusValue): if self.__bonusValue != bonusValue: self.__bonusValue = bonusValue self.bonusValueChanged.emit(bonusValue) self.totalvalueChanged.emit(self.totalvalue) bonusValue = property(__getBonusValue, __setBonusValue) def __getBonusSpecialties(self): return self.__bonusSpecialties def __setBonusSpecialties(self, bonusSpecialties): if self.__bonusSpecialties != bonusSpecialties: self.__bonusSpecialties = bonusSpecialties self.bonusSpecialtiesChanged.emit(bonusSpecialties) self.traitChanged.emit(self) bonusSpecialties = property(__getBonusSpecialties, __setBonusSpecialties) @property def totalspecialties(self): ## Kopie erstellen result = self.specialties[:] result.extend(self.bonusSpecialties) return result def emitTotalspecialtiesChanged(self, value): self.totalspecialtiesChanged.emit(self.totalspecialties) def appendBonusSpecialty(self, name): """ Fügt der Liste von Bonusspezialisierungen eine hinzu. \note Diese Methode muß verwendet werden, wenn man das Signal \ref bonusSpecialtiesChanged nutzen möchte. """ self.__bonusSpecialties.append(name) self.bonusSpecialtiesChanged.emit(self.bonusSpecialties) self.traitChanged.emit(self) def removeBonusSpecialty(self, name): """ Entfernt eine Bonusspezialisierung. \note Diese Methode muß verwendet werden, wenn man das Signal \ref bonusSpecialtiesChanged nutzen möchte. """ self.__bonusSpecialties.remove(name) self.bonusSpecialtiesChanged.emit(self.bonusSpecialties) self.traitChanged.emit(self) def clearBonus(self): """ Entfernt alle Bonus-Werte dieser Eigenschaft. """ self.__bonusValue = 0 self.__bonusSpecialties = [] self.bonusValueChanged.emit(self.__bonusValue) self.bonusSpecialtiesChanged.emit(self.__bonusSpecialties) self.traitChanged.emit(self)
class OWWidget(QDialog, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget description name = None id = None category = None version = None description = None long_description = None icon = "icons/Unknown.png" priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None inputs = [] outputs = [] # Default widget layout settings want_basic_layout = True want_main_area = True want_control_area = True want_graph = False show_save_graph = True want_status_bar = False no_report = False save_position = False resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) asyncCallsStateChange = Signal() progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: SettingsHandler""" def __new__(cls, parent=None, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) setattr(self, gui.CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self)) self._guiElements = [] # used for automatic widget debugging self.__reportData = None # TODO: position used to be saved like this. Reimplement. #if save_position: # self.settingsList = getattr(self, "settingsList", []) + \ # ["widgetShown", "savedWidgetGeometry"] OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id if self.name: self.setCaption(self.name) self.setFocusPolicy(Qt.StrongFocus) self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__progressBarValue = -1 self.__progressState = 0 self.__statusMessage = "" if self.want_basic_layout: self.insertLayout() return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.topWidgetPart = gui.widgetBox(self, orientation="horizontal", margin=0) self.leftWidgetPart = gui.widgetBox(self.topWidgetPart, orientation="vertical", margin=0) if self.want_main_area: self.leftWidgetPart.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.leftWidgetPart.updateGeometry() self.mainArea = gui.widgetBox(self.topWidgetPart, orientation="vertical", sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding), margin=0) self.mainArea.layout().setMargin(4) self.mainArea.updateGeometry() if self.want_control_area: self.controlArea = gui.widgetBox(self.leftWidgetPart, orientation="vertical", margin=4) if self.want_graph and self.show_save_graph: graphButtonBackground = gui.widgetBox(self.leftWidgetPart, orientation="horizontal", margin=4) self.graphButton = gui.button(graphButtonBackground, self, "&Save Graph") self.graphButton.setAutoDefault(0) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(environ.widget_install_dir, "icons/triangle-orange.png")) self._errorWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(environ.widget_install_dir, "icons/triangle-red.png")) # status bar handler functions def setState(self, stateType, id, text): stateChanged = super().setState(stateType, id, text) if not stateChanged or not hasattr(self, "widgetStatusArea"): return iconsShown = 0 warnings = [("Warning", self._warningWidget, self._owWarning), ("Error", self._errorWidget, self._owError)] for state, widget, use in warnings: if not widget: continue if use and self.widgetState[state]: widget.setToolTip("\n".join(self.widgetState[state].values())) widget.show() iconsShown = 1 else: widget.setToolTip("") widget.hide() if iconsShown: self.statusBarIconArea.show() else: self.statusBarIconArea.hide() if (stateType == "Warning" and self._owWarning) or \ (stateType == "Error" and self._owError): if text: self.setStatusBarText(stateType + ": " + text) else: self.setStatusBarText("") self.updateStatusBarState() def updateWidgetStateInfo(self, stateType, id, text): html = self.widgetStateToHtml(self._owInfo, self._owWarning, self._owError) if html: self.widgetStateInfoBox.show() self.widgetStateInfo.setText(html) self.widgetStateInfo.setToolTip(html) else: if not self.widgetStateInfoBox.isVisible(): dHeight = -self.widgetStateInfoBox.height() else: dHeight = 0 self.widgetStateInfoBox.hide() self.widgetStateInfo.setText("") self.widgetStateInfo.setToolTip("") width, height = self.width(), self.height() + dHeight self.resize(width, height) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) # TODO add! def prepareDataReport(self, data): pass # ############################################## """ def isDataWithClass(self, data, wantedVarType=None, checkMissing=False): self.error([1234, 1235, 1236]) if not data: return 0 if not data.domain.classVar: self.error(1234, "A data set with a class attribute is required.") return 0 if wantedVarType and data.domain.classVar.varType != wantedVarType: self.error(1235, "Unable to handle %s class." % str(data.domain.class_var.var_type).lower()) return 0 if checkMissing and not orange.Preprocessor_dropMissingClasses(data): self.error(1236, "Unable to handle data set with no known classes") return 0 return 1 """ def restoreWidgetPosition(self): if self.save_position: geometry = getattr(self, "savedWidgetGeometry", None) restored = False if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored: space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) #Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) # when widget is resized, save new width and height into widgetWidth and # widgetHeight. some widgets can put this two variables into settings and # last widget shape is restored after restart def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives the resizeEvent before showEvent and we must not # overwrite the the savedGeometry before then) if self.save_position and self.isVisible(): self.savedWidgetGeometry = str(self.saveGeometry()) # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.savedWidgetGeometry = str(self.saveGeometry()) QDialog.hideEvent(self, ev) # set widget state to shown def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position: if not self.__was_restored: self.__was_restored = True self.restoreWidgetPosition() def closeEvent(self, ev): if self.save_position: self.savedWidgetGeometry = str(self.saveGeometry()) QDialog.closeEvent(self, ev) def wheelEvent(self, event): """ Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): self.settingsHandler.close_context(self) def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) # this function is only intended for derived classes to send appropriate # signals when all settings are loaded def activateLoadedSettings(self): pass # reimplemented in other widgets def onDeleteWidget(self): pass def handleNewSignals(self): # this is called after all new signals have been handled # implement this in your widget if you want to process something only # after you received multiple signals pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self): self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") if self.__progressState != 1: self.__progressState = 1 self.processingStateChanged.emit(1) self.progressBarValue = 0 def progressBarSet(self, value): old = self.__progressBarValue self.__progressBarValue = value if value > 0: if self.__progressState != 1: warnings.warn( "progressBarSet() called without a " "preceding progressBarInit()", stacklevel=2) self.__progressState = 1 self.processingStateChanged.emit(1) usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle( self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") self.progressBarValueChanged.emit(value) if old != value: self.progressBarValueChanged.emit(value) qApp.processEvents() def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) processingState = pyqtProperty(int, fget=lambda self: self.__progressState) def progressBarAdvance(self, value): self.progressBarSet(self.progressBarValue + value) def progressBarFinished(self): self.setWindowTitle(self.captionTitle) if self.__progressState != 0: self.__progressState = 0 self.processingStateChanged.emit(0) #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): return self.__statusMessage def keyPressEvent(self, e): if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): self.setState("Info", id, text) def warning(self, id=0, text=""): self.setState("Warning", id, text) def error(self, id=0, text=""): self.setState("Error", id, text) def setState(self, stateType, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[stateType]: self.widgetState[stateType].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[stateType]: self.widgetState[stateType].pop(id) changed = 1 else: self.widgetState[stateType][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(stateType, i, "") else: self.widgetStateChanged.emit(stateType, id, text or "") return changed def widgetStateToHtml(self, info=True, warning=True, error=True): pixmaps = self.getWidgetStateIcons() items = [] iconPath = { "Info": "canvasIcons:information.png", "Warning": "canvasIcons:warning.png", "Error": "canvasIcons:error.png" } for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append('<img src="%s" style="float: left;"> %s' % (iconPath[what], "\n".join( self.widgetState[what].values()))) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): iconsDir = os.path.join(environ.canvas_install_dir, "icons") QDir.addSearchPath( "canvasIcons", os.path.join(environ.canvas_install_dir, "icons/")) info = QPixmap("canvasIcons:information.png") warning = QPixmap("canvasIcons:warning.png") error = QPixmap("canvasIcons:error.png") cls._cached__widget_state_icons = \ {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible()) } def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the signal manager """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self)
class DendrogramWidget(QGraphicsWidget): """A Graphics Widget displaying a dendrogram.""" class ClusterGraphicsItem(QGraphicsPathItem): _rect = None def shape(self): if self._rect is not None: p = QPainterPath() p.addRect(self.boundingRect()) return p else: return super().shape() def setRect(self, rect): self.prepareGeometryChange() self._rect = QRectF(rect) def boundingRect(self): if self._rect is not None: return QRectF(self._rect) else: return super().boundingRect() #: Orientation Left, Top, Right, Bottom = 1, 2, 3, 4 selectionChanged = Signal() selectionEdited = Signal() def __init__(self, parent=None, root=None, orientation=Left): QGraphicsWidget.__init__(self, parent) self.orientation = orientation self._root = None self._highlighted_item = None #: a list of selected items self._selection = OrderedDict() #: a {node: item} mapping self._items = {} #: container for all cluster items. self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) self._cluster_parent = {} self.setContentsMargins(5, 5, 5, 5) self.set_root(root) def clear(self): for item in self._items.values(): item.setParentItem(None) if item.scene() is self.scene() and self.scene() is not None: self.scene().removeItem(item) for item in self._selection.values(): item.setParentItem(None) if item.scene(): item.scene().removeItem(item) self._root = None self._items = {} self._selection = OrderedDict() self._highlighted_item = None self._cluster_parent = {} def set_root(self, root): """Set the root cluster. :param Tree root: Root tree. """ self.clear() self._root = root if root: pen = make_pen(Qt.blue, width=1, cosmetic=True, join_style=Qt.MiterJoin) for node in postorder(root): item = DendrogramWidget.ClusterGraphicsItem(self._itemgroup) item.setAcceptHoverEvents(True) item.setPen(pen) item.node = node item.installSceneEventFilter(self) for branch in node.branches: assert branch in self._items self._cluster_parent[branch] = node self._items[node] = item self.updateGeometry() self._relayout() self._rescale() def item(self, node): """Return the DendrogramNode instance representing the cluster. :type cluster: :class:`Tree` """ return self._items.get(node) def height_at(self, point): """Return the cluster height at the point in widget local coordinates. """ if not self._root: return 0 tpoint = self.mapToItem(self._itemgroup, point) if self.orientation in [self.Left, self.Right]: height = tpoint.x() else: height = tpoint.y() if self.orientation in [self.Left, self.Bottom]: base = self._root.value.height height = base - height return height def pos_at_height(self, height): """Return a point in local coordinates for `height` (in cluster height scale). """ if not self._root: return QPointF() if self.orientation in [self.Left, self.Bottom]: base = self._root.value.height height = base - height if self.orientation in [self.Left, self.Right]: p = QPointF(height, 0) else: p = QPointF(0, height) return self.mapFromItem(self._itemgroup, p) def _set_hover_item(self, item): """Set the currently highlighted item.""" if self._highlighted_item is item: return def branches(item): return [self._items[ch] for ch in item.node.branches] if self._highlighted_item: pen = make_pen(Qt.blue, width=1, cosmetic=True) for it in postorder(self._highlighted_item, branches): it.setPen(pen) self._highlighted_item = item if item: hpen = make_pen(Qt.blue, width=2, cosmetic=True) for it in postorder(item, branches): it.setPen(hpen) def leaf_items(self): """Iterate over the dendrogram leaf items (:class:`QGraphicsItem`). """ if self._root: return (self._items[leaf] for leaf in leaves(self._root)) else: return iter(()) def leaf_anchors(self): """Iterate over the dendrogram leaf anchor points (:class:`QPointF`). The points are in the widget local coordinates. """ for item in self.leaf_items(): anchor = QPointF(item.element.anchor) yield self.mapFromItem(item, anchor) def selected_nodes(self): """Return the selected clusters.""" return [item.node for item in self._selection] def set_selected_items(self, items): """Set the item selection. :param items: List of `GraphicsItems`s to select. """ to_remove = set(self._selection) - set(items) to_add = set(items) - set(self._selection) for sel in to_remove: self._remove_selection(sel) for sel in to_add: self._add_selection(sel) if to_add or to_remove: self._re_enumerate_selections() self.selectionChanged.emit() def set_selected_clusters(self, clusters): """Set the selected clusters. :param Tree items: List of cluster nodes to select . """ self.set_selected_items(list(map(self.item, clusters))) def select_item(self, item, state): """Set the `item`s selection state to `select_state` :param item: QGraphicsItem. :param bool state: New selection state for item. """ if state is False and item not in self._selection or \ state == True and item in self._selection: return # State unchanged if item in self._selection: if state == False: self._remove_selection(item) self.selectionChanged.emit() else: # If item is already inside another selected item, # remove that selection super_selection = self._selected_super_item(item) if super_selection: self._remove_selection(super_selection) # Remove selections this selection will override. sub_selections = self._selected_sub_items(item) for sub in sub_selections: self._remove_selection(sub) if state: self._add_selection(item) self._re_enumerate_selections() elif item in self._selection: self._remove_selection(item) self.selectionChanged.emit() def _add_selection(self, item): """Add selection rooted at item """ outline = self._selection_poly(item) selection_item = QGraphicsPolygonItem(self) # selection_item = QGraphicsPathItem(self) selection_item.setPos(self.contentsRect().topLeft()) # selection_item.setPen(QPen(Qt.NoPen)) selection_item.setPen(make_pen(width=1, cosmetic=True)) transform = self._itemgroup.transform() path = transform.map(outline) margin = 4 if item.node.is_leaf: path = QPolygonF(path.boundingRect().adjusted( -margin, -margin, margin, margin)) else: pass # ppath = QPainterPath() # ppath.addPolygon(path) # path = path_outline(ppath, width=margin).toFillPolygon() selection_item.setPolygon(path) # selection_item.setPath(path_outline(path, width=4)) selection_item.unscaled_path = outline self._selection[item] = selection_item item.setSelected(True) def _remove_selection(self, item): """Remove selection rooted at item.""" selection_poly = self._selection[item] selection_poly.hide() selection_poly.setParentItem(None) if self.scene(): self.scene().removeItem(selection_poly) del self._selection[item] item.setSelected(False) self._re_enumerate_selections() def _selected_sub_items(self, item): """Return all selected subclusters under item.""" def branches(item): return [self._items[ch] for ch in item.node.branches] res = [] for item in list(preorder(item, branches))[1:]: if item in self._selection: res.append(item) return res def _selected_super_item(self, item): """Return the selected super item if it exists.""" def branches(item): return [self._items[ch] for ch in item.node.branches] for selected_item in self._selection: if item in set(preorder(selected_item, branches)): return selected_item return None def _re_enumerate_selections(self): """Re enumerate the selection items and update the colors.""" # Order the clusters items = sorted(self._selection.items(), key=lambda item: item[0].node.value.first) palette = colorpalette.ColorPaletteGenerator(len(items)) for i, (item, selection_item) in enumerate(items): # delete and then reinsert to update the ordering del self._selection[item] self._selection[item] = selection_item color = palette[i] color.setAlpha(150) selection_item.setBrush(QColor(color)) def _selection_poly(self, item): """Return an selection item covering the selection rooted at item. """ def branches(item): return [self._items[ch] for ch in item.node.branches] def left(item): return [self._items[ch] for ch in item.node.branches[:1]] def right(item): return [self._items[ch] for ch in item.node.branches[-1:]] allitems = list(preorder(item, left)) + list(preorder(item, right))[1:] if len(allitems) == 1: assert (allitems[0].node.is_leaf) else: allitems = [item for item in allitems if not item.node.is_leaf] brects = [QPolygonF(item.boundingRect()) for item in allitems] return reduce(QPolygonF.united, brects, QPolygonF()) def _update_selection_items(self): """Update the shapes of selection items after a scale change. """ transform = self._itemgroup.transform() for _, selection in self._selection.items(): path = transform.map(selection.unscaled_path) selection.setPolygon(path) # selection.setPath(path) # selection.setPath(path_outline(path, width=4)) def _relayout(self): if not self._root: return self._layout = dendrogram_path(self._root) for node_geom in postorder(self._layout): node, geom = node_geom.value item = self._items[node] item.element = geom item.setPath(Path_toQtPath(geom)) item.setZValue(-node.value.height) item.setPen(QPen(Qt.blue)) r = item.boundingRect() base = self._root.value.height if self.orientation == Left: r.setRight(base) elif self.orientation == Right: r.setLeft(0) elif self.orientation == Top: r.setBottom(base) else: r.setTop(0) item.setRect(r) def _rescale(self): if self._root is None: return crect = self.contentsRect() leaf_count = len(list(leaves(self._root))) if self.orientation in [Left, Right]: drect = QSizeF(self._root.value.height, leaf_count - 1) else: drect = QSizeF(self._root.value.last - 1, leaf_count - 1) transform = QTransform().scale(crect.width() / drect.width(), crect.height() / drect.height()) self._itemgroup.setPos(crect.topLeft()) self._itemgroup.setTransform(transform) self._selection_items = None self._update_selection_items() def sizeHint(self, which, constraint=QSizeF()): fm = QFontMetrics(self.font()) spacing = fm.lineSpacing() mleft, mtop, mright, mbottom = self.getContentsMargins() if self._root and which == Qt.PreferredSize: nleaves = len( [node for node in self._items.keys() if not node.branches]) if self.orientation in [self.Left, self.Right]: return QSizeF(250, spacing * nleaves + mleft + mright) else: return QSizeF(spacing * nleaves + mtop + mbottom, 250) elif which == Qt.MinimumSize: return QSizeF(mleft + mright + 10, mtop + mbottom + 10) else: return QSizeF() def sceneEventFilter(self, obj, event): if isinstance(obj, DendrogramWidget.ClusterGraphicsItem): if event.type() == QEvent.GraphicsSceneHoverEnter: self._set_hover_item(obj) event.accept() return True elif event.type() == QEvent.GraphicsSceneMousePress and \ event.button() == Qt.LeftButton: if event.modifiers() & Qt.ControlModifier: self.select_item(obj, not obj.isSelected()) else: self.set_selected_items([obj]) self.selectionEdited.emit() assert self._highlighted_item is obj event.accept() return True if event.type() == QEvent.GraphicsSceneHoverLeave: self._set_hover_item(None) return super().sceneEventFilter(obj, event) def changeEvent(self, event): super().changeEvent(event) if event.type() == QEvent.FontChange: self.updateGeometry() def resizeEvent(self, event): super().resizeEvent(event) self._rescale() def mousePressEvent(self, event): QGraphicsWidget.mousePressEvent(self, event) # A mouse press on an empty widget part if event.modifiers() == Qt.NoModifier and self._selection: self.set_selected_clusters([])
class QtWidgetRegistry(QObject, WidgetRegistry): """ A QObject wrapper for `WidgetRegistry` A QStandardItemModel instance containing the widgets in a tree (of depth 2). The items in a model can be quaries using standard roles (DisplayRole, BackgroundRole, DecorationRole ToolTipRole). They also have QtWidgetRegistry.CATEGORY_DESC_ROLE, QtWidgetRegistry.WIDGET_DESC_ROLE, which store Category/WidgetDescription respectfully. Furthermore QtWidgetRegistry.WIDGET_ACTION_ROLE stores an default QAction which can be used for widget creation action. """ CATEGORY_DESC_ROLE = Qt.UserRole + 1 """Category Description Role""" WIDGET_DESC_ROLE = Qt.UserRole + 2 """Widget Description Role""" WIDGET_ACTION_ROLE = Qt.UserRole + 3 """Widget Action Role""" BACKGROUND_ROLE = Qt.UserRole + 4 """Background color for widget/category in the canvas (different from Qt.BackgroundRole) """ category_added = Signal(str, CategoryDescription) """signal: category_added(name: str, desc: CategoryDescription) """ widget_added = Signal(str, str, WidgetDescription) """signal widget_added(category_name: str, widget_name: str, desc: WidgetDescription) """ reset = Signal() """signal: reset() """ def __init__(self, other_or_parent=None, parent=None): if isinstance(other_or_parent, QObject) and parent is None: parent, other_or_parent = other_or_parent, None QObject.__init__(self, parent) WidgetRegistry.__init__(self, other_or_parent) # Should the QStandardItemModel be subclassed? self.__item_model = QStandardItemModel(self) for i, desc in enumerate(self.categories()): cat_item = self._cat_desc_to_std_item(desc) self.__item_model.insertRow(i, cat_item) for j, wdesc in enumerate(self.widgets(desc.name)): widget_item = self._widget_desc_to_std_item(wdesc, desc) cat_item.insertRow(j, widget_item) def model(self): """ Return the widget descriptions in a Qt Item Model instance (QStandardItemModel). .. note:: The model should not be modified outside of the registry. """ return self.__item_model def item_for_widget(self, widget): """Return the QStandardItem for the widget. """ if isinstance(widget, basestring): widget = self.widget(widget) cat = self.category(widget.category) cat_ind = self.categories().index(cat) cat_item = self.model().item(cat_ind) widget_ind = self.widgets(cat).index(widget) return cat_item.child(widget_ind) def action_for_widget(self, widget): """ Return the QAction instance for the widget (can be a string or a WidgetDescription instance). """ item = self.item_for_widget(widget) return item.data(self.WIDGET_ACTION_ROLE).toPyObject() def create_action_for_item(self, item): """ Create a QAction instance for the widget description item. """ name = item.text() tooltip = item.toolTip() whatsThis = item.whatsThis() icon = item.icon() if icon: action = QAction(icon, name, self, toolTip=tooltip, whatsThis=whatsThis, statusTip=name) else: action = QAction(name, self, toolTip=tooltip, whatsThis=whatsThis, statusTip=name) widget_desc = item.data(self.WIDGET_DESC_ROLE) action.setData(widget_desc) action.setProperty("item", QVariant(item)) return action def _insert_category(self, desc): """ Override to update the item model and emit the signals. """ priority = desc.priority priorities = [c.priority for c, _ in self.registry] insertion_i = bisect.bisect_right(priorities, priority) WidgetRegistry._insert_category(self, desc) cat_item = self._cat_desc_to_std_item(desc) self.__item_model.insertRow(insertion_i, cat_item) self.category_added.emit(desc.name, desc) def _insert_widget(self, category, desc): """ Override to update the item model and emit the signals. """ assert (isinstance(category, CategoryDescription)) categories = self.categories() cat_i = categories.index(category) _, widgets = self._categories_dict[category.name] priorities = [w.priority for w in widgets] insertion_i = bisect.bisect_right(priorities, desc.priority) WidgetRegistry._insert_widget(self, category, desc) cat_item = self.__item_model.item(cat_i) widget_item = self._widget_desc_to_std_item(desc, category) cat_item.insertRow(insertion_i, widget_item) self.widget_added.emit(category.name, desc.name, desc) def _cat_desc_to_std_item(self, desc): """ Create a QStandardItem for the category description. """ item = QStandardItem() item.setText(desc.name) if desc.icon: icon = desc.icon else: icon = "icons/default-category.svg" icon = icon_loader.from_description(desc).get(icon) item.setIcon(icon) if desc.background: background = desc.background else: background = DEFAULT_COLOR background = NAMED_COLORS.get(background, background) brush = QBrush(QColor(background)) item.setData(brush, self.BACKGROUND_ROLE) tooltip = desc.description if desc.description else desc.name item.setToolTip(tooltip) item.setFlags(Qt.ItemIsEnabled) item.setData(QVariant(desc), self.CATEGORY_DESC_ROLE) return item def _widget_desc_to_std_item(self, desc, category): """ Create a QStandardItem for the widget description. """ item = QStandardItem(desc.name) item.setText(desc.name) if desc.icon: icon = desc.icon else: icon = "icons/default-widget.svg" icon = icon_loader.from_description(desc).get(icon) item.setIcon(icon) # This should be inherited from the category. background = None if desc.background: background = desc.background elif category.background: background = category.background else: background = DEFAULT_COLOR if background is not None: background = NAMED_COLORS.get(background, background) brush = QBrush(QColor(background)) item.setData(brush, self.BACKGROUND_ROLE) tooltip = tooltip_helper(desc) style = "ul { margin-top: 1px; margin-bottom: 1px; }" tooltip = TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip) item.setToolTip(tooltip) item.setWhatsThis(whats_this_helper(desc)) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) item.setData(QVariant(desc), self.WIDGET_DESC_ROLE) # Create the action for the widget_item action = self.create_action_for_item(item) item.setData(QVariant(action), self.WIDGET_ACTION_ROLE) return item
class CategoryPopupMenu(FramelessWindow): triggered = Signal(QAction) hovered = Signal(QAction) def __init__(self, parent=None, **kwargs): FramelessWindow.__init__(self, parent, **kwargs) self.setWindowFlags(self.windowFlags() | Qt.Popup) layout = QVBoxLayout() layout.setContentsMargins(6, 6, 6, 6) self.__menu = MenuPage() self.__menu.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE) if sys.platform == "darwin": self.__menu.view().setAttribute(Qt.WA_MacShowFocusRect, False) self.__menu.triggered.connect(self.__onTriggered) self.__menu.hovered.connect(self.hovered) self.__dragListener = ItemViewDragStartEventListener(self) self.__dragListener.dragStarted.connect(self.__onDragStarted) self.__menu.view().viewport().installEventFilter(self.__dragListener) layout.addWidget(self.__menu) self.setLayout(layout) self.__action = None self.__loop = None self.__item = None def setCategoryItem(self, item): """ Set the category root item (:class:`QStandardItem`). """ self.__item = item model = item.model() self.__menu.setModel(model) self.__menu.setRootIndex(item.index()) def popup(self, pos=None): if pos is None: pos = self.pos() geom = widget_popup_geometry(pos, self) self.setGeometry(geom) self.show() def exec_(self, pos=None): self.popup(pos) self.__loop = QEventLoop() self.__action = None self.__loop.exec_() self.__loop = None if self.__action is not None: action = self.__action else: action = None return action def hideEvent(self, event): if self.__loop is not None: self.__loop.exit(0) return FramelessWindow.hideEvent(self, event) def __onTriggered(self, action): self.__action = action self.triggered.emit(action) self.hide() if self.__loop: self.__loop.exit(0) def __onDragStarted(self, index): desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE) icon = index.data(Qt.DecorationRole) drag_data = QMimeData() drag_data.setData( "application/vnv.orange-canvas.registry.qualified-name", desc.qualified_name ) drag = QDrag(self) drag.setPixmap(icon.pixmap(38)) drag.setMimeData(drag_data) # TODO: Should animate (accept) hide. self.hide() # When a drag is started and the menu hidden the item's tool tip # can still show for a short time UNDER the cursor preventing a # drop. viewport = self.__menu.view().viewport() filter = ToolTipEventFilter() viewport.installEventFilter(filter) drag.exec_(Qt.CopyAction) viewport.removeEventFilter(filter)
class ToolGrid(QFrame): """ A widget containing a grid of actions/buttons. Actions can be added using standard :func:`QWidget.addAction(QAction)` and :func:`QWidget.insertAction(int, QAction)` methods. Parameters ---------- parent : :class:`QWidget` Parent widget. columns : int Number of columns in the grid layout. buttonSize : :class:`QSize`, optional Size of tool buttons in the grid. iconSize : :class:`QSize`, optional Size of icons in the buttons. toolButtonStyle : :class:`Qt.ToolButtonStyle` Tool button style. """ actionTriggered = Signal(QAction) actionHovered = Signal(QAction) def __init__(self, parent=None, columns=4, buttonSize=None, iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon): QFrame.__init__(self, parent) if buttonSize is not None: buttonSize = QSize(buttonSize) if iconSize is not None: iconSize = QSize(iconSize) self.__columns = columns self.__buttonSize = buttonSize or QSize(50, 50) self.__iconSize = iconSize or QSize(26, 26) self.__toolButtonStyle = toolButtonStyle self.__gridSlots = [] self.__buttonListener = ToolButtonEventListener(self) self.__buttonListener.buttonRightClicked.connect( self.__onButtonRightClick) self.__buttonListener.buttonEnter.connect( self.__onButtonEnter) self.__mapper = QSignalMapper() self.__mapper.mapped[QObject].connect(self.__onClicked) self.__setupUi() def __setupUi(self): layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.setSizeConstraint(QGridLayout.SetFixedSize) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) def setButtonSize(self, size): """ Set the button size. """ if self.__buttonSize != size: self.__buttonSize = size for slot in self.__gridSlots: slot.button.setFixedSize(size) def buttonSize(self): """ Return the button size. """ return QSize(self.__buttonSize) def setIconSize(self, size): """ Set the button icon size. """ if self.__iconSize != size: self.__iconSize = size for slot in self.__gridSlots: slot.button.setIconSize(size) def iconSize(self): """ Return the icon size """ return QSize(self.__iconSize) def setToolButtonStyle(self, style): """ Set the tool button style. """ if self.__toolButtonStyle != style: self.__toolButtonStyle = style for slot in self.__gridSlots: slot.button.setToolButtonStyle(style) def toolButtonStyle(self): """ Return the tool button style. """ return self.__toolButtonStyle def setColumnCount(self, columns): """ Set the number of button/action columns. """ if self.__columns != columns: self.__columns = columns self.__relayout() def columns(self): """ Return the number of columns in the grid. """ return self.__columns def clear(self): """ Clear all actions/buttons. """ for slot in reversed(list(self.__gridSlots)): self.removeAction(slot.action) self.__gridSlots = [] def insertAction(self, before, action): """ Insert a new action at the position currently occupied by `before` (can also be an index). Parameters ---------- before : :class:`QAction` or int Position where the `action` should be inserted. action : :class:`QAction` Action to insert """ if isinstance(before, int): actions = list(self.actions()) if len(actions) == 0 or before >= len(actions): # Insert as the first action or the last action. return self.addAction(action) before = actions[before] return QFrame.insertAction(self, before, action) def setActions(self, actions): """ Clear the grid and add `actions`. """ self.clear() for action in actions: self.addAction(action) def buttonForAction(self, action): """ Return the :class:`QToolButton` instance button for `action`. """ actions = [slot.action for slot in self.__gridSlots] index = actions.index(action) return self.__gridSlots[index].button def createButtonForAction(self, action): """ Create and return a :class:`QToolButton` for action. """ button = _ToolGridButton(self) button.setDefaultAction(action) if self.__buttonSize.isValid(): button.setFixedSize(self.__buttonSize) if self.__iconSize.isValid(): button.setIconSize(self.__iconSize) button.setToolButtonStyle(self.__toolButtonStyle) button.setProperty("tool-grid-button", True) return button def count(self): """ Return the number of buttons/actions in the grid. """ return len(self.__gridSlots) def actionEvent(self, event): QFrame.actionEvent(self, event) if event.type() == QEvent.ActionAdded: # Note: the action is already in the self.actions() list. actions = list(self.actions()) index = actions.index(event.action()) self.__insertActionButton(index, event.action()) elif event.type() == QEvent.ActionRemoved: self.__removeActionButton(event.action()) def __insertActionButton(self, index, action): """Create a button for the action and add it to the layout at index. """ self.__shiftGrid(index, 1) button = self.createButtonForAction(action) row = index / self.__columns column = index % self.__columns self.layout().addWidget( button, row, column, Qt.AlignLeft | Qt.AlignTop ) self.__gridSlots.insert( index, _ToolGridSlot(button, action, row, column) ) self.__mapper.setMapping(button, action) button.clicked.connect(self.__mapper.map) button.installEventFilter(self.__buttonListener) button.installEventFilter(self) def __removeActionButton(self, action): """Remove the button for the action from the layout and delete it. """ actions = [slot.action for slot in self.__gridSlots] index = actions.index(action) slot = self.__gridSlots.pop(index) slot.button.removeEventFilter(self.__buttonListener) slot.button.removeEventFilter(self) self.__mapper.removeMappings(slot.button) self.layout().removeWidget(slot.button) self.__shiftGrid(index + 1, -1) slot.button.deleteLater() def __shiftGrid(self, start, count=1): """Shift all buttons starting at index `start` by `count` cells. """ button_count = self.layout().count() direction = 1 if count >= 0 else -1 if direction == 1: start, end = button_count - 1, start - 1 else: start, end = start, button_count for index in range(start, end, -direction): item = self.layout().itemAtPosition(index / self.__columns, index % self.__columns) if item: button = item.widget() new_index = index + count self.layout().addWidget(button, new_index / self.__columns, new_index % self.__columns, Qt.AlignLeft | Qt.AlignTop) def __relayout(self): """Relayout the buttons. """ for i in reversed(range(self.layout().count())): self.layout().takeAt(i) self.__gridSlots = [_ToolGridSlot(slot.button, slot.action, i / self.__columns, i % self.__columns) for i, slot in enumerate(self.__gridSlots)] for slot in self.__gridSlots: self.layout().addWidget(slot.button, slot.row, slot.column, Qt.AlignLeft | Qt.AlignTop) def __indexOf(self, button): """Return the index of button widget. """ buttons = [slot.button for slot in self.__gridSlots] return buttons.index(button) def __onButtonRightClick(self, button): pass def __onButtonEnter(self, button): action = button.defaultAction() self.actionHovered.emit(action) def __onClicked(self, action): self.actionTriggered.emit(action) def paintEvent(self, event): return utils.StyledWidget_paintEvent(self, event) def eventFilter(self, obj, event): etype = event.type() if etype == QEvent.KeyPress and obj.hasFocus(): key = event.key() if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]: if self.__focusMove(obj, key): event.accept() return True return QFrame.eventFilter(self, obj, event) def __focusMove(self, focus, key): assert(focus is self.focusWidget()) try: index = self.__indexOf(focus) except IndexError: return False if key == Qt.Key_Down: index += self.__columns elif key == Qt.Key_Up: index -= self.__columns elif key == Qt.Key_Left: index -= 1 elif key == Qt.Key_Right: index += 1 if index >= 0 and index < self.count(): button = self.__gridSlots[index].button button.setFocus(Qt.TabFocusReason) return True else: return False
class MainDialog(qtGui.QMainWindow): ''' Main dialog for rsMap3D. This class also serves as the over action controller for the application ''' #define signals to be used here unblockTabsForLoad = Signal(name=UNBLOCK_TABS_FOR_LOAD_SIGNAL) def __init__(self, parent=None): ''' ''' logger.debug(METHOD_ENTER_STR) super(MainDialog, self).__init__(parent) #Create and layout the widgets self.appConfig = RSMap3DConfigParser() self.tabs = qtGui.QTabWidget() self.fileForm = FileInputController(appConfig=self.appConfig) self.scanForm = ScanForm() self.dataRange = DataRange() self.processScans = ProcessScansController(parent=self, \ appConfig=self.appConfig) self.dataExtentView = DataExtentView() self.fileTabIndex = self.tabs.addTab(self.fileForm, "File") self.dataTabIndex = self.tabs.addTab(self.dataRange, "Data Range") self.scanTabIndex = self.tabs.addTab(self.scanForm, "Scans") self.processTabIndex = self.tabs.addTab(self.processScans, "Process Data") self.tabs.setTabEnabled(self.dataTabIndex, False) self.tabs.setTabEnabled(self.scanTabIndex, False) self.tabs.setTabEnabled(self.processTabIndex, False) self.tabs.show() self.setCentralWidget(self.tabs) #Connect signals self.unblockTabsForLoad.connect(self._unblockTabsForLoad) self.tabs.currentChanged[int].connect(self._tabChanged) self.fileForm.fileError[str].connect(self._showFileError) self.fileForm.blockTabsForLoad.connect(self._blockTabsForLoad) self.fileForm.loadDataSourceToScanForm.\ connect(self._loadDataSourceToScanForm) self.fileForm.inputFormChanged.connect(self.updateOutputForms) self.dataRange.rangeChanged.connect(self._setScanRanges) self.scanForm.doneLoading.connect(self._setupRanges) self.scanForm.showRangeBounds[object].connect( \ self.dataExtentView.showRangeBounds) self.scanForm.clearRenderWindow.connect( self.dataExtentView.clearRenderWindow) self.scanForm.renderBoundsSignal[object].connect( self.dataExtentView.renderBounds) self.processScans.blockTabsForProcess.connect( self._blockTabsForProcess) self.processScans.unblockTabsForProcess.connect( \ self._unblockTabsForProcess) logger.debug(METHOD_EXIT_STR) def _blockTabsForLoad(self): ''' Disable tabs while loading ''' self.tabs.setTabEnabled(self.dataTabIndex, False) self.tabs.setTabEnabled(self.scanTabIndex, False) self.tabs.setTabEnabled(self.processTabIndex, False) @Slot() def _blockTabsForProcess(self): ''' disable tabs while processing ''' self.tabs.setTabEnabled(self.dataTabIndex, False) self.tabs.setTabEnabled(self.scanTabIndex, False) self.tabs.setTabEnabled(self.fileTabIndex, False) def closeEvent(self, event): ''' process event on window close ''' self.dataExtentView.vtkMain.close() def getDataSource(self): return self.fileForm.dataSource @qtCore.pyqtSlot() def getOutputForms(self): return self.fileForm.getOutputForms() def getTransform(self): return self.fileForm.transform @qtCore.pyqtSlot() def _loadDataSourceToScanForm(self): ''' When scan is done loading, load the data to the scan form. ''' self.scanForm.loadScanFile(self.fileForm.dataSource) @qtCore.pyqtSlot() def _setupRanges(self): ''' Get the overall data extent from the data source and set these values in the dataRange tab. ''' overallXmin, overallXmax, overallYmin, overallYmax, \ overallZmin, \ overallZmax = self.fileForm.dataSource.getOverallRanges() self.dataRange.setRanges(overallXmin, \ overallXmax, \ overallYmin, \ overallYmax, \ overallZmin, \ overallZmax) self._setScanRanges() self.unblockTabsForLoad.emit() def _setScanRanges(self): ''' Get the data range from the dataRange tab and set the bounds in this class. Tell scanForm tab to render the Qs for all scans. ''' ranges = self.dataRange.getRanges() self.fileForm.dataSource.setRangeBounds(ranges) self.scanForm.renderOverallQs() @qtCore.pyqtSlot(str) def _showFileError(self, error): ''' Show any errors from file loading in a message dialog. When done, toggle Load and Cancel buttons in file tab to Load Active/Cancel inactive ''' message = qtGui.QMessageBox() message.warning(self, \ "Load Scan File Warning", \ str(error)) self.fileForm.setScanLoadOK.emit() # @Slot(str) # def _showProcessError(self, error): # ''' # Show any errors from file processing in a message dialog. When done, # toggle Load and Cancel buttons in file tab to Load Active/Cancel # inactive # ''' # message = qtGui.QMessageBox() # message.warning(self, \ # "Processing Scan File Warning", \ # str(error)) # self.processScans.setProcessRunOK.emit() @Slot(int) def _tabChanged(self, index): ''' When changing to the data range tab, display all qs from all scans. ''' if str(self.tabs.tabText(index)) == "Data Range": self.scanForm.renderOverallQs() def _unblockTabsForLoad(self): ''' enable tabs when done loading ''' self.tabs.setTabEnabled(self.dataTabIndex, True) self.tabs.setTabEnabled(self.scanTabIndex, True) self.tabs.setTabEnabled(self.processTabIndex, True) @Slot() def _unblockTabsForProcess(self): ''' enable tabs when done processing ''' self.tabs.setTabEnabled(self.dataTabIndex, True) self.tabs.setTabEnabled(self.scanTabIndex, True) self.tabs.setTabEnabled(self.fileTabIndex, True) def updateOutputForms(self): newOutputForms = self.fileForm.getOutputForms() self.processScans.updateOutputForms(newOutputForms)
class CSVOptionsWidget(QWidget): _PresetDelimiters = [ ("Comma", ","), ("Tab", "\t"), ("Semicolon", ";"), ("Space", " "), ] format_changed = Signal() def __init__(self, parent=None, **kwargs): self._delimiter_idx = 0 self._delimiter_custom = "|" self._delimiter = "," self._quotechar = "'" self._escapechar = "\\" self._doublequote = True self._skipinitialspace = False super(QWidget, self).__init__(parent, **kwargs) # Dialect options form = QFormLayout() self.delimiter_cb = QComboBox() self.delimiter_cb.addItems( [name for name, _ in self._PresetDelimiters]) self.delimiter_cb.insertSeparator(self.delimiter_cb.count()) self.delimiter_cb.addItem("Other") self.delimiter_cb.setCurrentIndex(self._delimiter_idx) self.delimiter_cb.activated.connect(self._on_delimiter_idx_changed) validator = QRegExpValidator(QRegExp(".")) self.delimiteredit = LineEdit(self._delimiter_custom, enabled=False, minimumContentsLength=2) self.delimiteredit.setValidator(validator) self.delimiteredit.editingFinished.connect(self._on_delimiter_changed) delimlayout = QHBoxLayout() delimlayout.setContentsMargins(0, 0, 0, 0) delimlayout.addWidget(self.delimiter_cb) delimlayout.addWidget(self.delimiteredit) self.quoteedit = LineEdit(self._quotechar, minimumContentsLength=2) self.quoteedit.setValidator(validator) self.quoteedit.editingFinished.connect(self._on_quotechar_changed) self.escapeedit = LineEdit(self._escapechar, minimumContentsLength=2) self.escapeedit.setValidator(validator) self.escapeedit.editingFinished.connect(self._on_escapechar_changed) # self.skipinitialspace_cb = QCheckBox( # checked=self._skipinitialspace # ) form.addRow("Cell delimiter", delimlayout) form.addRow("Quote character", self.quoteedit) form.addRow("Escape character", self.escapeedit) form.addRow(QFrame(self, frameShape=QFrame.HLine)) # File format option self.missingedit = QLineEdit() self.missingedit.editingFinished.connect(self.format_changed) form.addRow("Missing values", self.missingedit) self.header_format_cb = QComboBox() self.header_format_cb.addItems([ "No header", "Plain header", "Orange header", "Orange simplified header" ]) self.header_format_cb.currentIndexChanged.connect(self.format_changed) form.addRow("Header", self.header_format_cb) self.setLayout(form) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) def dialect(self): """ Return the current state as a Dialect instance. """ if self._delimiter_idx >= len(self._PresetDelimiters): delimiter = self._delimiter_custom else: _, delimiter = self._PresetDelimiters[self._delimiter_idx] quotechar = str(self.quoteedit.text()) or "" escapechar = str(self.escapeedit.text()) or None skipinitialspace = True return Dialect(delimiter, quotechar, escapechar, doublequote=True, skipinitialspace=skipinitialspace) def set_dialect(self, dialect): """ Set the current state to match dialect instance. """ delimiter = dialect.delimiter try: index = [d for _, d in self._PresetDelimiters].index(delimiter) except ValueError: index = len(self._PresetDelimiters) + 1 self._delimiter_idx = index self._delimiter_custom = delimiter self._quotechar = dialect.quotechar self._escapechar = dialect.escapechar self._skipinitialspace = dialect.skipinitialspace self.delimiter_cb.setCurrentIndex(index) self.delimiteredit.setText(delimiter) self.quoteedit.setText(dialect.quotechar or '"') self.escapeedit.setText(dialect.escapechar or "") # self.skipinitialspace_cb.setChecked(dialect.skipinitialspace) def set_header_format(self, header_format): """Set the current selected header format.""" self._header_format = header_format self.header_format_cb.setCurrentIndex(header_format) def header_format(self): return self.header_format_cb.currentIndex() def set_missing_values(self, missing): self.missingedit.setText(missing) def missing_values(self): return str(self.missingedit.text()) def _on_delimiter_idx_changed(self, index): if index < len(self._PresetDelimiters): self.delimiteredit.setText(self._PresetDelimiters[index][1]) else: self.delimiteredit.setText(self._delimiter_custom) self.delimiteredit.setEnabled(index >= len(self._PresetDelimiters)) self._delimiter_idx = index self.format_changed.emit() def _on_delimiter_changed(self): self._delimiter_custom = str(self.delimiteredit.text()) self.format_changed.emit() def _on_quotechar_changed(self): self._quotechar = str(self.quoteedit.text()) self.format_changed.emit() def _on_escapechar_changed(self): self._escapechar = str(self.escapeedit.text()) self.format_changed.emit() def _on_skipspace_changed(self, skipinitialspace): self._skipinitialspace = skipinitialspace self.format_changed.emit()
class SchemeNode(QObject): """ A node in a :class:`.Scheme`. Parameters ---------- description : :class:`WidgetDescription` Node description instance. title : str, optional Node title string (if None `description.name` is used). position : tuple (x, y) tuple of floats for node position in a visual display. properties : dict Additional extra instance properties (settings, widget geometry, ...) parent : :class:`QObject` Parent object. """ def __init__(self, description, title=None, position=None, properties=None, parent=None): QObject.__init__(self, parent) self.description = description if title is None: title = description.name self.__title = title self.__position = position or (0, 0) self.__progress = -1 self.__processing_state = 0 self.__status_message = "" self.__state_messages = {} self.properties = properties or {} def input_channels(self): """ Return a list of input channels (:class:`InputSignal`) for the node. """ return list(self.description.inputs) def output_channels(self): """ Return a list of output channels (:class:`OutputSignal`) for the node. """ return list(self.description.outputs) def input_channel(self, name): """ Return the input channel matching `name`. Raise a `ValueError` if not found. """ for channel in self.input_channels(): if channel.name == name: return channel raise ValueError("%r is not a valid input channel name for %r." % \ (name, self.description.name)) def output_channel(self, name): """ Return the output channel matching `name`. Raise an `ValueError` if not found. """ for channel in self.output_channels(): if channel.name == name: return channel raise ValueError("%r is not a valid output channel name for %r." % \ (name, self.description.name)) #: The title of the node has changed title_changed = Signal(str) def set_title(self, title): """ Set the node title. """ if self.__title != title: self.__title = str(title) self.title_changed.emit(self.__title) def title(self): """ The node title. """ return self.__title title = Property(str, fset=set_title, fget=title) #: Position of the node in the scheme has changed position_changed = Signal(tuple) def set_position(self, pos): """ Set the position (``(x, y)`` tuple) of the node. """ if self.__position != pos: self.__position = pos self.position_changed.emit(pos) def position(self): """ ``(x, y)`` tuple containing the position of the node in the scheme. """ return self.__position position = Property(tuple, fset=set_position, fget=position) #: Node's progress value has changed. progress_changed = Signal(float) def set_progress(self, value): """ Set the progress value. """ if self.__progress != value: self.__progress = value self.progress_changed.emit(value) def progress(self): """ The current progress value. -1 if progress is not set. """ return self.__progress progress = Property(float, fset=set_progress, fget=progress) #: Node's processing state has changed. processing_state_changed = Signal(int) def set_processing_state(self, state): """ Set the node processing state. """ if self.__processing_state != state: self.__processing_state = state self.processing_state_changed.emit(state) def processing_state(self): """ The node processing state, 0 for not processing, 1 the node is busy. """ return self.__processing_state processing_state = Property(int, fset=set_processing_state, fget=processing_state) def set_tool_tip(self, tool_tip): if self.__tool_tip != tool_tip: self.__tool_tip = tool_tip def tool_tip(self): return self.__tool_tip tool_tip = Property(str, fset=set_tool_tip, fget=tool_tip) #: The node's status tip has changes status_message_changed = Signal(str) def set_status_message(self, text): if self.__status_message != text: self.__status_message = text self.status_message_changed.emit(text) def status_message(self): return self.__status_message #: The node's state message has changed state_message_changed = Signal(UserMessage) def set_state_message(self, message): """ Set a message to be displayed by a scheme view for this node. """ if message.message_id in self.__state_messages and \ not message.contents: del self.__state_messages[message.message_id] self.__state_messages[message.message_id] = message self.state_message_changed.emit(message) def state_messages(self): """ Return a list of all state messages. """ return self.__state_messages.values() def __str__(self): return "SchemeNode(description_id=%s, title=%r, ...)" % \ (str(self.description.id), self.title) def __repr__(self): return str(self)
class LogModel(QObject): log = Signal(dict) def __call__(self, data): self.log.emit(data)
class WidgetToolBox(ToolBox): """ `WidgetToolBox` widget shows a tool box containing button grids of actions for a :class:`QtWidgetRegistry` item model. """ triggered = Signal(QAction) hovered = Signal(QAction) def __init__(self, parent=None): ToolBox.__init__(self, parent) self.__model = None self.__iconSize = QSize(25, 25) self.__buttonSize = QSize(50, 50) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) def setIconSize(self, size): """ Set the widget icon size (icons in the button grid). """ self.__iconSize = size for widget in map(self.widget, range(self.count())): widget.setIconSize(size) def iconSize(self): """ Return the widget buttons icon size. """ return self.__iconSize iconSize_ = Property(QSize, fget=iconSize, fset=setIconSize, designable=True) def setButtonSize(self, size): """ Set fixed widget button size. """ self.__buttonSize = size for widget in map(self.widget, range(self.count())): widget.setButtonSize(size) def buttonSize(self): """Return the widget button size """ return self.__buttonSize buttonSize_ = Property(QSize, fget=buttonSize, fset=setButtonSize, designable=True) def saveState(self): """ Return the toolbox state (as a `QByteArray`). .. note:: Individual tabs are stored by their action's text. """ version = 2 actions = map(self.tabAction, range(self.count())) expanded = [action for action in actions if action.isChecked()] expanded = [action.text() for action in expanded] byte_array = QByteArray() stream = QDataStream(byte_array, QIODevice.WriteOnly) stream.writeInt(version) stream.writeQStringList(expanded) return byte_array def restoreState(self, state): """ Restore the toolbox from a :class:`QByteArray` `state`. .. note:: The toolbox should already be populated for the state changes to take effect. """ # In version 1 of saved state the state was saved in # a simple dict repr string. if isinstance(state, QByteArray): stream = QDataStream(state, QIODevice.ReadOnly) version = stream.readInt() if version == 2: expanded = stream.readQStringList() for action in map(self.tabAction, range(self.count())): if (action.text() in expanded) != action.isChecked(): action.trigger() return True return False def setModel(self, model): """ Set the widget registry model (:class:`QStandardItemModel`) for this toolbox. """ if self.__model is not None: self.__model.itemChanged.disconnect(self.__on_itemChanged) self.__model.rowsInserted.disconnect(self.__on_rowsInserted) self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved) self.__model = model if self.__model is not None: self.__model.itemChanged.connect(self.__on_itemChanged) self.__model.rowsInserted.connect(self.__on_rowsInserted) self.__model.rowsRemoved.connect(self.__on_rowsRemoved) self.__initFromModel(self.__model) def __initFromModel(self, model): for cat_item in iter_item(model.invisibleRootItem()): self.__insertItem(cat_item, self.count()) def __insertItem(self, item, index): """ Insert category item at index. """ grid = WidgetToolGrid() grid.setModel(item.model(), item.index()) grid.actionTriggered.connect(self.triggered) grid.actionHovered.connect(self.hovered) grid.setIconSize(self.__iconSize) grid.setButtonSize(self.__buttonSize) text = item.text() icon = item.icon() tooltip = item.toolTip() # Set the 'tab-title' property to text. grid.setProperty("tab-title", text) grid.setObjectName("widgets-toolbox-grid") self.insertItem(index, grid, text, icon, tooltip) button = self.tabButton(index) # Set the 'highlight' color if item.data(Qt.BackgroundRole) is not None: brush = item.background() elif item.data(QtWidgetRegistry.BACKGROUND_ROLE) is not None: brush = item.data(QtWidgetRegistry.BACKGROUND_ROLE) else: brush = self.palette().brush(QPalette.Button) if not brush.gradient(): gradient = create_gradient(brush.color()) brush = QBrush(gradient) palette = button.palette() palette.setBrush(QPalette.Highlight, brush) button.setPalette(palette) def __on_itemChanged(self, item): """ Item contents have changed. """ parent = item.parent() if parent is self.__model.invisibleRootItem(): button = self.tabButton(item.row()) button.setIcon(item.icon()) button.setText(item.text()) button.setToolTip(item.toolTip()) def __on_rowsInserted(self, parent, start, end): """ Items have been inserted in the model. """ # Only the top level items (categories) are handled here. if not parent is not None: root = self.__model.invisibleRootItem() for i in range(start, end + 1): item = root.child(i) self.__insertItem(item, i) def __on_rowsRemoved(self, parent, start, end): """ Rows have been removed from the model. """ # Only the top level items (categories) are handled here. if not parent is not None: for i in range(end, start - 1, -1): self.removeItem(i)
class AbstractBuildRunner(QObject): """ Base class to run a build. Create the required test runner and build manager, along with a thread that should be used for blocking tasks. """ running_state_changed = Signal(bool) worker_created = Signal(object) worker_class = None def __init__(self, mainwindow): QObject.__init__(self) self.mainwindow = mainwindow self.thread = None self.worker = None self.pending_threads = [] self.test_runner = None self.download_manager = None self.options = None self.stopped = False def init_worker(self, fetch_config, options): """ Create and initialize the worker. Should be subclassed to configure the worker, and should return the worker method that should start the work. """ self.options = options # global preferences global_prefs = get_prefs() self.global_prefs = global_prefs # apply the global prefs now apply_prefs(global_prefs) if fetch_config.is_nightly(): fetch_config.set_base_url(global_prefs['archive_base_url']) download_dir = global_prefs['persist'] if not download_dir: download_dir = self.mainwindow.persist persist_limit = int( abs(global_prefs['persist_size_limit']) * 1073741824) self.download_manager = GuiBuildDownloadManager( download_dir, persist_limit) self.test_runner = GuiTestRunner() self.thread = QThread() # options for the app launcher launcher_kwargs = {} for name in ('profile', 'preferences'): if name in options: value = options[name] if value: launcher_kwargs[name] = value # add add-ons paths to the app launcher launcher_kwargs['addons'] = options['addons'] self.test_runner.launcher_kwargs = launcher_kwargs if options['profile_persistence'] in ('clone-first', 'reuse') or options['profile']: launcher_kwargs['cmdargs'] = launcher_kwargs.get( 'cmdargs', []) + ['--allow-downgrade'] self.worker = self.worker_class(fetch_config, self.test_runner, self.download_manager) # Move self.bisector in the thread. This will # allow to the self.bisector slots (connected after the move) # to be automatically called in the thread. self.worker.moveToThread(self.thread) self.worker_created.emit(self.worker) def start(self, fetch_config, options): action = self.init_worker(fetch_config, options) assert callable(action), "%s should be callable" % action self.thread.start() # this will be called in the worker thread. QTimer.singleShot(0, action) self.stopped = False self.running_state_changed.emit(True) @Slot() def stop(self, wait=True): self.stopped = True if self.options: if self.options['profile'] and \ self.options['profile_persistence'] == 'clone-first': self.options['profile'].cleanup() if self.download_manager: self.download_manager.cancel() if self.thread: self.thread.quit() if wait: if self.download_manager: self.download_manager.wait(raise_if_error=False) if self.thread: # wait for thread(s) completion - this is the case when # user close the application self.thread.wait() for thread in self.pending_threads: thread.wait() self.thread = None elif self.thread: # do not block, just keep track of the thread - we got here # when user uses the stop button. self.pending_threads.append(self.thread) self.thread.finished.connect(self._remove_pending_thread) if self.test_runner: self.test_runner.finish(None) self.running_state_changed.emit(False) log('Stopped') @Slot() def _remove_pending_thread(self): for thread in self.pending_threads[:]: if thread.isFinished(): self.pending_threads.remove(thread)
class PlotToolBox(QtCore.QObject): actionTriggered = Signal(QtGui.QAction) toolActivated = Signal(linproj.PlotTool) class StandardActions(enum.IntEnum): NoAction = 0 #: Reset zoom (zoom to fit) action with CTRL + Key_0 shortcut ZoomReset = 1 #: Zoom in action with QKeySequence.ZoomIn shortcut ZoomIn = 2 #: Zoom out action with QKeySequence.ZoomOut shortcut ZoomOut = 4 # A Select tool action (exclusive with other *Tool) SelectTool = 8 # A Zoom tool action (exclusive with other *Tool) ZoomTool = 16 # A Pan tool (exclusive with other *Tool) PanTool = 32 NoAction, ZoomReset, ZoomIn, ZoomOut, SelectTool, ZoomTool, PanTool = \ list(StandardActions) DefaultActions = (ZoomReset | ZoomIn | ZoomOut | SelectTool | ZoomTool | PanTool) ExclusiveMask = SelectTool | ZoomTool | PanTool ActionData = { ZoomReset: ("Zoom to fit", "zoom_reset", Qt.ControlModifier + Qt.Key_0), ZoomIn: ("Zoom in", "", QtGui.QKeySequence.ZoomIn), ZoomOut: ("Zoom out", "", QtGui.QKeySequence.ZoomOut), SelectTool: ("Select", "arrow", Qt.ControlModifier + Qt.Key_1), ZoomTool: ("Zoom", "zoom", Qt.ControlModifier + Qt.Key_2), PanTool: ("Pan", "pan_hand", Qt.ControlModifier + Qt.Key_3), } def __init__(self, parent=None, standardActions=DefaultActions, **kwargs): super().__init__(parent, **kwargs) self.__standardActions = standardActions self.__actions = {} self.__tools = {} self.__viewBox = None self.__currentTool = None self.__toolgroup = QtGui.QActionGroup(self, exclusive=True) def on_toolaction(action): tool = action.property("tool") if self.__currentTool is not None: self.__currentTool.setViewBox(None) self.__currentTool = tool if tool is not None: tool.setViewBox(self.__viewBox) self.__viewBox.setCursor(tool.cursor) self.__toolgroup.triggered[QtGui.QAction].connect(on_toolaction) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) isfirsttool = True for flag in PlotToolBox.StandardActions: if standardActions & flag: _text, _iconname, _keyseq = self.ActionData[flag] action = QtGui.QAction(_text, self, icon=icon(_iconname), shortcut=QtGui.QKeySequence(_keyseq)) self.__actions[flag] = action if flag & PlotToolBox.ExclusiveMask: action.setCheckable(True) self.__toolgroup.addAction(action) if flag == self.SelectTool: tool = linproj.PlotSelectionTool(self) tool.cursor = Qt.ArrowCursor elif flag == self.ZoomTool: tool = linproj.PlotZoomTool(self) tool.cursor = Qt.ArrowCursor elif flag == self.PanTool: tool = linproj.PlotPanTool(self) tool.cursor = Qt.OpenHandCursor self.__tools[flag] = tool action.setProperty("tool", tool) if isfirsttool: action.setChecked(True) self.__currentTool = tool isfirsttool = False def setViewBox(self, box): if self.__viewBox is not box and self.__currentTool is not None: self.__currentTool.setViewBox(None) # TODO: Unset/restore default view box cursor self.__viewBox = None self.__viewBox = box if self.__currentTool is not None: self.__currentTool.setViewBox(box) if box is not None: box.setCursor(self.__currentTool.cursor) def viewBox(self): return self.__viewBox def standardAction(self, action): return self.__actions[action] def actions(self): return list(self.__actions.values()) def button(self, action, parent=None): action = self.standardAction(action) b = QtGui.QToolButton(parent) b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b def toolGroup(self): """Return the exclusive tool action button group""" return self.__toolgroup def plotTool(self, action): return self.__tools[action]
class WidgetManager(QObject): """ OWWidget instance manager class. This class handles the lifetime of OWWidget instances in a :class:`WidgetsScheme`. """ #: A new OWWidget was created and added by the manager. widget_for_node_added = Signal(SchemeNode, QWidget) #: An OWWidget was removed, hidden and will be deleted when appropriate. widget_for_node_removed = Signal(SchemeNode, QWidget) #: Widget processing state flags: #: * InputUpdate - signal manager is updating/setting the #: widget's inputs #: * BlockingUpdate - widget has entered a blocking state #: * ProcessingUpdate - widget has entered processing state InputUpdate, BlockingUpdate, ProcessingUpdate = 1, 2, 4 #: Widget initialization states Delayed = namedtuple("Delayed", ["node", "future"]) Materialized = namedtuple("Materialized", ["node", "widget"]) class WidgetInitEvent(QEvent): DelayedInit = QEvent.registerEventType() def __init__(self, initstate): super().__init__(WidgetManager.WidgetInitEvent.DelayedInit) self._initstate = initstate def initstate(self): return self._initstate def __init__(self, parent=None): QObject.__init__(self, parent) self.__scheme = None self.__signal_manager = None self.__widgets = [] self.__initstate_for_node = {} self.__widget_for_node = {} self.__node_for_widget = {} # If True then the initialization of the OWWidget instance # will be delayed (scheduled to run from the event loop) self.__delayed_init = True # Widgets that were 'removed' from the scheme but were at # the time in an input update loop and could not be deleted # immediately self.__delay_delete = set() # processing state flags for all nodes (including the ones # in __delay_delete). self.__widget_processing_state = {} # Tracks the widget in the update loop by the SignalManager self.__updating_widget = None def set_scheme(self, scheme): """ Set the :class:`WidgetsScheme` instance to manage. """ self.__scheme = scheme self.__signal_manager = scheme.findChild(SignalManager) self.__signal_manager.processingStarted[SchemeNode].connect( self.__on_processing_started) self.__signal_manager.processingFinished[SchemeNode].connect( self.__on_processing_finished) scheme.node_added.connect(self.add_widget_for_node) scheme.node_removed.connect(self.remove_widget_for_node) scheme.runtime_env_changed.connect(self.__on_env_changed) scheme.installEventFilter(self) def scheme(self): """ Return the scheme instance on which this manager is installed. """ return self.__scheme def signal_manager(self): """ Return the signal manager in use on the :func:`scheme`. """ return self.__signal_manager def widget_for_node(self, node): """ Return the OWWidget instance for the scheme node. """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Delayed): # Create the widget now if it is still in the event queue. state = self.__materialize(state) self.__initstate_for_node[node] = state return state.widget elif isinstance(state, WidgetManager.Materialized): return state.widget else: assert False def node_for_widget(self, widget): """ Return the SchemeNode instance for the OWWidget. Raise a KeyError if the widget does not map to a node in the scheme. """ return self.__node_for_widget[widget] def add_widget_for_node(self, node): """ Create a new OWWidget instance for the corresponding scheme node. """ future = concurrent.futures.Future() state = WidgetManager.Delayed(node, future) self.__initstate_for_node[node] = state event = WidgetManager.WidgetInitEvent(state) if self.__delayed_init: def schedule_later(): QCoreApplication.postEvent(self, event, Qt.LowEventPriority - 10) QTimer.singleShot(int(1000 / 30) + 10, schedule_later) else: QCoreApplication.sendEvent(self, event) def __materialize(self, state): # Initialize an OWWidget for a Delayed widget initialization. assert isinstance(state, WidgetManager.Delayed) node, future = state.node, state.future widget = self.create_widget_instance(node) self.__widgets.append(widget) self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__initialize_widget_state(node, widget) state = WidgetManager.Materialized(node, widget) self.__initstate_for_node[node] = state future.set_result(widget) self.widget_for_node_added.emit(node, widget) return state def remove_widget_for_node(self, node): """ Remove the OWWidget instance for node. """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Delayed): state.future.cancel() del self.__initstate_for_node[node] else: # Update the node's stored settings/properties dict before # removing the widget. # TODO: Update/sync whenever the widget settings change. node.properties = self._widget_settings(state.widget) self.__widgets.remove(state.widget) del self.__initstate_for_node[node] del self.__widget_for_node[node] del self.__node_for_widget[state.widget] node.title_changed.disconnect(state.widget.setCaption) state.widget.progressBarValueChanged.disconnect(node.set_progress) self.widget_for_node_removed.emit(node, state.widget) self._delete_widget(state.widget) def _widget_settings(self, widget): return widget.settingsHandler.pack_data(widget) def _delete_widget(self, widget): """ Delete the OWBaseWidget instance. """ widget.close() # Save settings to user global settings. widget.saveSettings() # Notify the widget it will be deleted. widget.onDeleteWidget() if self.__widget_processing_state[widget] != 0: # If the widget is in an update loop and/or blocking we # delay the scheduled deletion until the widget is done. self.__delay_delete.add(widget) else: widget.deleteLater() del self.__widget_processing_state[widget] def create_widget_instance(self, node): """ Create a OWWidget instance for the node. """ desc = node.description klass = widget = None initialized = False error = None # First try to actually retrieve the class. try: klass = name_lookup(desc.qualified_name) except (ImportError, AttributeError): sys.excepthook(*sys.exc_info()) error = "Could not import {0!r}\n\n{1}".format( node.description.qualified_name, traceback.format_exc()) except Exception: sys.excepthook(*sys.exc_info()) error = "An unexpected error during import of {0!r}\n\n{1}".format( node.description.qualified_name, traceback.format_exc()) if klass is None: widget = mock_error_owwidget(node, error) initialized = True if widget is None: log.info("Creating %r instance.", klass) widget = klass.__new__( klass, None, signal_manager=self.signal_manager(), stored_settings=node.properties, # NOTE: env is a view of the real env and reflects # changes to the environment. env=self.scheme().runtime_env()) initialized = False # Init the node/widget mapping and state before calling __init__ # Some OWWidgets might already send data in the constructor # (should this be forbidden? Raise a warning?) triggering the signal # manager which would request the widget => node mapping or state self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__widget_processing_state[widget] = 0 if not initialized: try: widget.__init__() except Exception: sys.excepthook(*sys.exc_info()) msg = traceback.format_exc() msg = "Could not create {0!r}\n\n{1}".format( node.description.name, msg) # remove state tracking for widget ... del self.__widget_for_node[node] del self.__node_for_widget[widget] del self.__widget_processing_state[widget] # ... and substitute it with a mock error widget. widget = mock_error_owwidget(node, msg) self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__widget_processing_state[widget] = 0 widget.setCaption(node.title) widget.widgetInfo = desc widget.setWindowIcon(icon_loader.from_description(desc).get(desc.icon)) widget.setVisible(node.properties.get("visible", False)) node.title_changed.connect(widget.setCaption) # Widget's info/warning/error messages. widget.widgetStateChanged.connect(self.__on_widget_state_changed) # Widget's statusTip node.set_status_message(widget.statusMessage()) widget.statusMessageChanged.connect(node.set_status_message) # Widget's progress bar value state. widget.progressBarValueChanged.connect(node.set_progress) # Widget processing state (progressBarInit/Finished) # and the blocking state. widget.processingStateChanged.connect( self.__on_processing_state_changed) widget.blockingStateChanged.connect(self.__on_blocking_state_changed) if widget.isBlocking(): # A widget can already enter blocking state in __init__ self.__widget_processing_state[widget] |= self.BlockingUpdate if widget.processingState != 0: # It can also start processing (initialization of resources, ...) self.__widget_processing_state[widget] |= self.ProcessingUpdate node.set_processing_state(1) node.set_progress(widget.progressBarValue) # Install a help shortcut on the widget help_shortcut = QShortcut(QKeySequence("F1"), widget) help_shortcut.activated.connect(self.__on_help_request) # Up shortcut (activate/open parent) up_shortcut = QShortcut(QKeySequence(Qt.ControlModifier + Qt.Key_Up), widget) up_shortcut.activated.connect(self.__on_activate_parent) return widget def node_processing_state(self, node): """ Return the processing state flags for the node. Same as `manager.widget_processing_state(manger.widget_for_node(node))` """ widget = self.widget_for_node(node) return self.__widget_processing_state[widget] def widget_processing_state(self, widget): """ Return the processing state flags for the widget. The state is an bitwise or of `InputUpdate` and `BlockingUpdate`. """ return self.__widget_processing_state[widget] def customEvent(self, event): if event.type() == WidgetManager.WidgetInitEvent.DelayedInit: state = event.initstate() node, future = state.node, state.future if not (future.cancelled() or future.done()): QCoreApplication.flush() self.__initstate_for_node[node] = self.__materialize(state) event.accept() else: super().customEvent(event) def eventFilter(self, receiver, event): if event.type() == QEvent.Close and receiver is self.__scheme: self.signal_manager().stop() # Notify the widget instances. for widget in list(self.__widget_for_node.values()): widget.close() widget.saveSettings() widget.onDeleteWidget() event.accept() return True return QObject.eventFilter(self, receiver, event) def __on_help_request(self): """ Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to the scheme and hope someone responds to it. """ # Sender is the QShortcut, and parent the OWBaseWidget widget = self.sender().parent() try: node = self.node_for_widget(widget) except KeyError: pass else: url = "help://search?id={0}".format(node.description.id) event = QWhatsThisClickedEvent(url) QCoreApplication.sendEvent(self.scheme(), event) def __on_activate_parent(self): """ Activate parent shortcut was pressed. """ event = ActivateParentEvent() QCoreApplication.sendEvent(self.scheme(), event) def __initialize_widget_state(self, node, widget): """ Initialize the tracked info/warning/error message state. """ for message_type, state in widget.widgetState.items(): for message_id, message_value in state.items(): message = user_message_from_state(widget, message_type, message_id, message_value) node.set_state_message(message) def __on_widget_state_changed(self, message_type, message_id, message_value): """ The OWBaseWidget info/warning/error state has changed. message_type is one of "Info", "Warning" or "Error" string depending of which method (information, warning, error) was called. message_id is the first int argument if supplied, and message_value the message text. """ widget = self.sender() try: node = self.node_for_widget(widget) except KeyError: pass else: message = user_message_from_state(widget, str(message_type), message_id, message_value) node.set_state_message(message) def __on_processing_state_changed(self, state): """ A widget processing state has changed (progressBarInit/Finished) """ widget = self.sender() try: node = self.node_for_widget(widget) except KeyError: return if state: self.__widget_processing_state[widget] |= self.ProcessingUpdate else: self.__widget_processing_state[widget] &= ~self.ProcessingUpdate self.__update_node_processing_state(node) def __on_processing_started(self, node): """ Signal manager entered the input update loop for the node. """ widget = self.widget_for_node(node) # Remember the widget instance. The node and the node->widget mapping # can be removed between this and __on_processing_finished. self.__updating_widget = widget self.__widget_processing_state[widget] |= self.InputUpdate self.__update_node_processing_state(node) def __on_processing_finished(self, node): """ Signal manager exited the input update loop for the node. """ widget = self.__updating_widget self.__widget_processing_state[widget] &= ~self.InputUpdate if widget in self.__node_for_widget: self.__update_node_processing_state(node) elif widget in self.__delay_delete: self.__try_delete(widget) else: raise ValueError("%r is not managed" % widget) self.__updating_widget = None def __on_blocking_state_changed(self, state): """ OWWidget blocking state has changed. """ if not state: # schedule an update pass. self.signal_manager()._update() widget = self.sender() if state: self.__widget_processing_state[widget] |= self.BlockingUpdate else: self.__widget_processing_state[widget] &= ~self.BlockingUpdate if widget in self.__node_for_widget: node = self.node_for_widget(widget) self.__update_node_processing_state(node) elif widget in self.__delay_delete: self.__try_delete(widget) def __update_node_processing_state(self, node): """ Update the `node.processing_state` to reflect the widget state. """ state = self.node_processing_state(node) node.set_processing_state(1 if state else 0) def __try_delete(self, widget): if self.__widget_processing_state[widget] == 0: self.__delay_delete.remove(widget) widget.deleteLater() del self.__widget_processing_state[widget] def __on_env_changed(self, key, newvalue, oldvalue): # Notify widgets of a runtime environment change for widget in self.__widget_for_node.values(): widget.workflowEnvChanged(key, newvalue, oldvalue)
class AsyncUpdateLoop(QObject): """ Run/drive an coroutine from the event loop. This is a utility class which can be used for implementing asynchronous update loops. I.e. coroutines which periodically yield control back to the Qt event loop. """ Next = QEvent.registerEventType() #: State flags Idle, Running, Cancelled, Finished = 0, 1, 2, 3 #: The coroutine has yielded control to the caller (with `object`) yielded = Signal(object) #: The coroutine has finished/exited (either with an exception #: or with a return statement) finished = Signal() #: The coroutine has returned (normal return statement / StopIteration) returned = Signal(object) #: The coroutine has exited with with an exception. raised = Signal(object) #: The coroutine was cancelled/closed. cancelled = Signal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.__coroutine = None self.__next_pending = False # Flag for compressing scheduled events self.__in_next = False self.__state = AsyncUpdateLoop.Idle @Slot(object) def setCoroutine(self, loop): """ Set the coroutine. The coroutine will be resumed (repeatedly) from the event queue. If there is an existing coroutine set it is first closed/cancelled. Raises an RuntimeError if the current coroutine is running. """ if self.__coroutine is not None: self.__coroutine.close() self.__coroutine = None self.__state = AsyncUpdateLoop.Cancelled self.cancelled.emit() self.finished.emit() if loop is not None: self.__coroutine = loop self.__state = AsyncUpdateLoop.Running self.__schedule_next() @Slot() def cancel(self): """ Cancel/close the current coroutine. Raises an RuntimeError if the current coroutine is running. """ self.setCoroutine(None) def state(self): """ Return the current state. """ return self.__state def isRunning(self): return self.__state == AsyncUpdateLoop.Running def __schedule_next(self): if not self.__next_pending: self.__next_pending = True QtCore.QTimer.singleShot(10, self.__on_timeout) def __next(self): if self.__coroutine is not None: try: rval = next(self.__coroutine) except StopIteration as stop: self.__state = AsyncUpdateLoop.Finished self.returned.emit(stop.value) self.finished.emit() self.__coroutine = None except BaseException as er: self.__state = AsyncUpdateLoop.Finished self.raised.emit(er) self.finished.emit() self.__coroutine = None else: self.yielded.emit(rval) self.__schedule_next() @Slot() def __on_timeout(self): assert self.__next_pending self.__next_pending = False if not self.__in_next: self.__in_next = True try: self.__next() finally: self.__in_next = False else: # warn self.__schedule_next() def customEvent(self, event): if event.type() == AsyncUpdateLoop.Next: self.__on_timeout() else: super().customEvent(event)
class GroupingTableView(QtGui.QWidget): # For use by parent widget dataChanged = Signal() addPairRequested = Signal(str, str) @staticmethod def warning_popup(message): message_box.warning(str(message)) def __init__(self, parent=None): super(GroupingTableView, self).__init__(parent) self.grouping_table = QtGui.QTableWidget(self) self.set_up_table() self.setup_interface_layout() self.grouping_table.cellChanged.connect(self.on_cell_changed) self._validate_group_name_entry = lambda text: True self._validate_detector_ID_entry = lambda text: True self._on_table_data_changed = lambda: 0 # whether the table is updating and therefore we shouldn't respond to signals self._updating = False # whether the interface should be disabled self._disabled = False def setup_interface_layout(self): self.setObjectName("GroupingTableView") self.resize(500, 500) self.add_group_button = QtGui.QToolButton() self.remove_group_button = QtGui.QToolButton() self.group_range_label = QtGui.QLabel() self.group_range_label.setText('Group Asymmetry Range from:') self.group_range_min = QtGui.QLineEdit() self.group_range_min.setEnabled(False) self.group_range_use_first_good_data = QtGui.QCheckBox() self.group_range_use_first_good_data.setText(u"\u03BCs (From data file)") self.group_range_use_first_good_data.setChecked(True) self.group_range_max = QtGui.QLineEdit() self.group_range_max.setEnabled(False) self.group_range_use_last_data = QtGui.QCheckBox() self.group_range_use_last_data.setText(u"\u03BCs (From data file)") self.group_range_use_last_data.setChecked(True) self.group_range_to_label = QtGui.QLabel() self.group_range_to_label.setText('to:') self.group_range_layout = QtGui.QGridLayout() self.group_range_layout_min = QtGui.QHBoxLayout() self.group_range_layout.addWidget(self.group_range_label, 0, 0) self.group_range_layout.addWidget(self.group_range_min, 0, 1) self.group_range_layout.addWidget(self.group_range_use_first_good_data, 0, 2) self.group_range_layout_max = QtGui.QHBoxLayout() self.group_range_layout.addWidget(self.group_range_to_label, 1, 0, QtCore.Qt.AlignRight) self.group_range_layout.addWidget(self.group_range_max, 1, 1) self.group_range_layout.addWidget(self.group_range_use_last_data, 1, 2) size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) size_policy.setHorizontalStretch(0) size_policy.setVerticalStretch(0) size_policy.setHeightForWidth(self.add_group_button.sizePolicy().hasHeightForWidth()) size_policy.setHeightForWidth(self.remove_group_button.sizePolicy().hasHeightForWidth()) self.add_group_button.setSizePolicy(size_policy) self.add_group_button.setObjectName("addGroupButton") self.add_group_button.setToolTip("Add a group to the end of the table") self.add_group_button.setText("+") self.remove_group_button.setSizePolicy(size_policy) self.remove_group_button.setObjectName("removeGroupButton") self.remove_group_button.setToolTip("Remove selected/last group(s) from the table") self.remove_group_button.setText("-") self.horizontal_layout = QtGui.QHBoxLayout() self.horizontal_layout.setObjectName("horizontalLayout") self.horizontal_layout.addWidget(self.add_group_button) self.horizontal_layout.addWidget(self.remove_group_button) self.spacer_item = QtGui.QSpacerItem(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) self.horizontal_layout.addItem(self.spacer_item) self.horizontal_layout.setAlignment(QtCore.Qt.AlignLeft) self.vertical_layout = QtGui.QVBoxLayout(self) self.vertical_layout.setObjectName("verticalLayout") self.vertical_layout.addWidget(self.grouping_table) self.vertical_layout.addLayout(self.horizontal_layout) self.vertical_layout.addLayout(self.group_range_layout) self.setLayout(self.vertical_layout) def set_up_table(self): self.grouping_table.setColumnCount(3) self.grouping_table.setHorizontalHeaderLabels(["Group Name", "Detector IDs", "N Detectors"]) header = self.grouping_table.horizontalHeader() header.setResizeMode(0, QtGui.QHeaderView.Stretch) header.setResizeMode(1, QtGui.QHeaderView.Stretch) header.setResizeMode(2, QtGui.QHeaderView.ResizeToContents) vertical_headers = self.grouping_table.verticalHeader() vertical_headers.setMovable(False) vertical_headers.setResizeMode(QtGui.QHeaderView.ResizeToContents) vertical_headers.setVisible(True) self.grouping_table.horizontalHeaderItem(0).setToolTip("The name of the group :" "\n - The name must be unique across all groups/pairs" "\n - The name can only use digits, characters and _") self.grouping_table.horizontalHeaderItem(1).setToolTip("The sorted list of detectors :" "\n - The list can only contain integers." "\n - , is used to separate detectors or ranges." "\n - \"-\" denotes a range, i,e \"1-5\" is the same as" " \"1,2,3,4,5\" ") self.grouping_table.horizontalHeaderItem(2).setToolTip("The number of detectors in the group.") def num_rows(self): return self.grouping_table.rowCount() def num_cols(self): return self.grouping_table.columnCount() def notify_data_changed(self): if not self._updating: self.dataChanged.emit() # ------------------------------------------------------------------------------------------------------------------ # Adding / removing table entries # ------------------------------------------------------------------------------------------------------------------ def add_entry_to_table(self, row_entries): assert len(row_entries) == self.grouping_table.columnCount() row_position = self.grouping_table.rowCount() self.grouping_table.insertRow(row_position) for i, entry in enumerate(row_entries): item = QtGui.QTableWidgetItem(entry) if group_table_columns[i] == group_table_columns[0]: # column 0 : group name group_name_widget = table_utils.ValidatedTableItem(self._validate_group_name_entry) group_name_widget.setText(entry) self.grouping_table.setItem(row_position, 0, group_name_widget) self.grouping_table.item(row_position, 0).setToolTip(entry) if group_table_columns[i] == group_table_columns[1]: # column 1 : detector IDs detector_widget = table_utils.ValidatedTableItem(self._validate_detector_ID_entry) detector_widget.setText(entry) self.grouping_table.setItem(row_position, 1, detector_widget) self.grouping_table.item(row_position, 1).setToolTip(entry) if group_table_columns[i] == group_table_columns[2]: # column 2 : number of detectors item.setFlags(QtCore.Qt.ItemIsEnabled) item.setFlags(QtCore.Qt.ItemIsSelectable) self.grouping_table.setItem(row_position, i, item) def _get_selected_row_indices(self): return list(set(index.row() for index in self.grouping_table.selectedIndexes())) def get_selected_group_names(self): indexes = self._get_selected_row_indices() return [str(self.grouping_table.item(i, 0).text()) for i in indexes] def remove_selected_groups(self): indices = self._get_selected_row_indices() for index in reversed(sorted(indices)): self.grouping_table.removeRow(index) def remove_last_row(self): last_row = self.grouping_table.rowCount() - 1 if last_row >= 0: self.grouping_table.removeRow(last_row) # ------------------------------------------------------------------------------------------------------------------ # Context menu on right-click in the table # ------------------------------------------------------------------------------------------------------------------ def _context_menu_add_group_action(self, slot): add_group_action = QtGui.QAction('Add Group', self) if len(self._get_selected_row_indices()) > 0: add_group_action.setEnabled(False) add_group_action.triggered.connect(slot) return add_group_action def _context_menu_remove_group_action(self, slot): if len(self._get_selected_row_indices()) > 1: # use plural if >1 item selected remove_group_action = QtGui.QAction('Remove Groups', self) else: remove_group_action = QtGui.QAction('Remove Group', self) if self.num_rows() == 0: remove_group_action.setEnabled(False) remove_group_action.triggered.connect(slot) return remove_group_action def _context_menu_add_pair_action(self, slot): add_pair_action = QtGui.QAction('Add Pair', self) if len(self._get_selected_row_indices()) != 2: add_pair_action.setEnabled(False) add_pair_action.triggered.connect(slot) return add_pair_action def contextMenuEvent(self, _event): """Overridden method""" self.menu = QtGui.QMenu(self) self.add_group_action = self._context_menu_add_group_action(self.add_group_button.clicked.emit) self.remove_group_action = self._context_menu_remove_group_action(self.remove_group_button.clicked.emit) self.add_pair_action = self._context_menu_add_pair_action(self.add_pair_requested) if self._disabled: self.add_group_action.setEnabled(False) self.remove_group_action.setEnabled(False) self.add_pair_action.setEnabled(False) self.menu.addAction(self.add_group_action) self.menu.addAction(self.remove_group_action) self.menu.addAction(self.add_pair_action) self.menu.popup(QtGui.QCursor.pos()) # ------------------------------------------------------------------------------------------------------------------ # Slot connections # ------------------------------------------------------------------------------------------------------------------ def on_user_changes_group_name(self, slot): self._validate_group_name_entry = slot def on_user_changes_detector_IDs(self, slot): self._validate_detector_ID_entry = slot def on_add_group_button_clicked(self, slot): self.add_group_button.clicked.connect(slot) def on_remove_group_button_clicked(self, slot): self.remove_group_button.clicked.connect(slot) def on_table_data_changed(self, slot): self._on_table_data_changed = slot def add_pair_requested(self): selected_names = self.get_selected_group_names() self.addPairRequested.emit(selected_names[0], selected_names[1]) def on_cell_changed(self, _row, _col): if not self._updating: self._on_table_data_changed(_row, _col) def on_user_changes_min_range_source(self, slot): self.group_range_use_first_good_data.stateChanged.connect(slot) def on_user_changes_max_range_source(self, slot): self.group_range_use_last_data.stateChanged.connect(slot) def on_user_changes_group_range_min_text_edit(self, slot): self.group_range_min.textChanged.connect(slot) def on_user_changes_group_range_max_text_edit(self, slot): self.group_range_max.textChanged.connect(slot) # ------------------------------------------------------------------------------------------------------------------ # # ------------------------------------------------------------------------------------------------------------------ def get_table_item_text(self, row, col): return self.grouping_table.item(row, col).text() def get_table_contents(self): if self._updating: return [] ret = [] for row in range(self.num_rows()): row_list = [] for col in range(self.num_cols()): row_list.append(str(self.grouping_table.item(row, col).text())) ret.append(row_list) return ret def clear(self): # Go backwards to preserve indices for row in reversed(range(self.num_rows())): self.grouping_table.removeRow(row) # ------------------------------------------------------------------------------------------------------------------ # Enabling and disabling editing and updating of the widget # ------------------------------------------------------------------------------------------------------------------ def disable_updates(self): """Usage : """ self._updating = True def enable_updates(self): """Usage : """ self._updating = False def disable_editing(self): self.disable_updates() self._disabled = True self._disable_buttons() self._disable_all_table_items() self.enable_updates() def enable_editing(self): self.disable_updates() self._disabled = False self._enable_buttons() self._enable_all_table_items() self.enable_updates() def _enable_buttons(self): self.add_group_button.setEnabled(True) self.remove_group_button.setEnabled(True) def _disable_buttons(self): self.add_group_button.setEnabled(False) self.remove_group_button.setEnabled(False) def _disable_all_table_items(self): for row in range(self.num_rows()): for col in range(self.num_cols()): item = self.grouping_table.item(row, col) item.setFlags(QtCore.Qt.ItemIsSelectable) def _enable_all_table_items(self): for row in range(self.num_rows()): for col in range(self.num_cols()): item = self.grouping_table.item(row, col) if group_table_columns[col] != 'number_of_detectors': item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled) else: # number of detectors should remain un-editable item.setFlags(QtCore.Qt.ItemIsSelectable) def get_group_range(self): return str(self.group_range_min.text()), str(self.group_range_max.text()) def set_group_range(self, range): self.group_range_min.setText(range[0]) self.group_range_max.setText(range[1])
class VariableEditor(QWidget): """An editor widget for a variable. Can edit the variable name, and its attributes dictionary. """ variable_changed = Signal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.setup_gui() def setup_gui(self): layout = QVBoxLayout() self.setLayout(layout) self.main_form = QFormLayout() self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) layout.addLayout(self.main_form) self._setup_gui_name() self._setup_gui_labels() def _setup_gui_name(self): self.name_edit = QLineEdit() self.main_form.addRow("Name", self.name_edit) self.name_edit.editingFinished.connect(self.on_name_changed) def _setup_gui_labels(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.setSpacing(1) self.labels_edit = QTreeView() self.labels_edit.setEditTriggers(QTreeView.CurrentChanged) self.labels_edit.setRootIsDecorated(False) self.labels_model = DictItemsModel() self.labels_edit.setModel(self.labels_model) self.labels_edit.selectionModel().selectionChanged.connect( self.on_label_selection_changed) # Necessary signals to know when the labels change self.labels_model.dataChanged.connect(self.on_labels_changed) self.labels_model.rowsInserted.connect(self.on_labels_changed) self.labels_model.rowsRemoved.connect(self.on_labels_changed) vlayout.addWidget(self.labels_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.setSpacing(1) self.add_label_action = QAction("+", self, toolTip="Add a new label.", triggered=self.on_add_label, enabled=False, shortcut=QKeySequence( QKeySequence.New)) self.remove_label_action = QAction(unicodedata.lookup("MINUS SIGN"), self, toolTip="Remove selected label.", triggered=self.on_remove_label, enabled=False, shortcut=QKeySequence( QKeySequence.Delete)) button_size = gui.toolButtonSizeHint() button_size = QSize(button_size, button_size) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.add_label_action) hlayout.addWidget(button) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.remove_label_action) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) self.main_form.addRow("Labels", vlayout) def set_data(self, var): """Set the variable to edit. """ self.clear() self.var = var if var is not None: self.name_edit.setText(var.name) self.labels_model.set_dict(dict(var.attributes)) self.add_label_action.setEnabled(True) else: self.add_label_action.setEnabled(False) self.remove_label_action.setEnabled(False) def get_data(self): """Retrieve the modified variable. """ name = str(self.name_edit.text()) labels = self.labels_model.get_dict() # Is the variable actually changed. if not self.is_same(): var = type(self.var)(name) var.attributes.update(labels) self.var = var else: var = self.var return var def is_same(self): """Is the current model state the same as the input. """ name = str(self.name_edit.text()) labels = self.labels_model.get_dict() return self.var and name == self.var.name and labels == self.var.attributes def clear(self): """Clear the editor state. """ self.var = None self.name_edit.setText("") self.labels_model.set_dict({}) def maybe_commit(self): if not self.is_same(): self.commit() def commit(self): """Emit a ``variable_changed()`` signal. """ self.variable_changed.emit() @Slot() def on_name_changed(self): self.maybe_commit() @Slot() def on_labels_changed(self, *args): self.maybe_commit() @Slot() def on_add_label(self): self.labels_model.appendRow([QStandardItem(""), QStandardItem("")]) row = self.labels_model.rowCount() - 1 index = self.labels_model.index(row, 0) self.labels_edit.edit(index) @Slot() def on_remove_label(self): rows = self.labels_edit.selectionModel().selectedRows() if rows: row = rows[0] self.labels_model.removeRow(row.row()) @Slot() def on_label_selection_changed(self): selected = self.labels_edit.selectionModel().selectedRows() self.remove_label_action.setEnabled(bool(len(selected)))
class DataTool(QObject): """ A base class for data tools that operate on PaintViewBox. """ #: Tool mouse cursor has changed cursorChanged = Signal(QtGui.QCursor) #: User started an editing operation. editingStarted = Signal() #: User ended an editing operation. editingFinished = Signal() #: Emits a data transformation command issueCommand = Signal(object) # Makes for a checkable push-button checkable = True # The tool only works if (at least) two dimensions only2d = True def __init__(self, parent, plot): super().__init__(parent) self._cursor = Qt.ArrowCursor self._plot = plot def cursor(self): return QtGui.QCursor(self._cursor) def setCursor(self, cursor): if self._cursor != cursor: self._cursor = QtGui.QCursor(cursor) self.cursorChanged.emit() def mousePressEvent(self, event): return False def mouseMoveEvent(self, event): return False def mouseReleaseEvent(self, event): return False def mouseClickEvent(self, event): return False def mouseDragEvent(self, event): return False def hoverEnterEvent(self, event): return False def hoverLeaveEvent(self, event): return False def mapToPlot(self, point): """Map a point in ViewBox local coordinates into plot coordinates. """ box = self._plot.getViewBox() return box.mapToView(point) def activate(self, ): """Activate the tool""" pass def deactivate(self, ): """Deactivate a tool""" pass
class OWWidget(QDialog, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget description name = None id = None category = None version = None description = None long_description = None icon = "icons/Unknown.png" priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None inputs = [] outputs = [] # Default widget layout settings want_basic_layout = True want_main_area = True want_control_area = True want_graph = False show_save_graph = True want_status_bar = False no_report = False save_position = True resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: settings.SettingsHandler""" savedWidgetGeometry = settings.Setting(None) def __new__(cls, parent=None, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) setattr(self, gui.CONTROLLED_ATTRIBUTES, gui.ControlledAttributesDict(self)) self.__reportData = None OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id if self.name: self.setCaption(self.name) self.setFocusPolicy(Qt.StrongFocus) self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__progressBarValue = -1 self.__progressState = 0 self.__statusMessage = "" if self.want_basic_layout: self.insertLayout() return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) # noinspection PyAttributeOutsideInit def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.warning_bar = gui.widgetBox(self, orientation="horizontal", margin=0, spacing=0) self.warning_icon = gui.widgetLabel(self.warning_bar, "") self.warning_label = gui.widgetLabel(self.warning_bar, "") self.warning_label.setStyleSheet("padding-top: 5px") self.warning_bar.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) gui.rubber(self.warning_bar) self.warning_bar.setVisible(False) self.topWidgetPart = gui.widgetBox(self, orientation="horizontal", margin=0) self.leftWidgetPart = gui.widgetBox(self.topWidgetPart, orientation="vertical", margin=0) if self.want_main_area: self.leftWidgetPart.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.leftWidgetPart.updateGeometry() self.mainArea = gui.widgetBox(self.topWidgetPart, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding), margin=0) self.mainArea.layout().setMargin(4) self.mainArea.updateGeometry() if self.want_control_area: self.controlArea = gui.widgetBox(self.leftWidgetPart, orientation="vertical", margin=4) if self.want_graph and self.show_save_graph: graphButtonBackground = gui.widgetBox(self.leftWidgetPart, orientation="horizontal", margin=4) self.graphButton = gui.button(graphButtonBackground, self, "&Save Graph") self.graphButton.setAutoDefault(0) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, gui.resource_filename("icons/triangle-orange.png")) self._errorWidget = createPixmapWidget( self.statusBarIconArea, gui.resource_filename("icons/triangle-red.png")) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) # TODO add! def prepareDataReport(self, data): pass def restoreWidgetPosition(self): restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored: space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) return restored def __updateSavedGeometry(self): if self.__was_restored: # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). self.savedWidgetGeometry = self.saveGeometry() # when widget is resized, save the new width and height def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before showEvent and we must not overwrite the the savedGeometry # with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, ev): QDialog.moveEvent(self, ev) if self.save_position and self.isVisible(): self.__updateSavedGeometry() # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.__updateSavedGeometry() self.__was_restored = False QDialog.hideEvent(self, ev) def closeEvent(self, ev): if self.save_position and self.isVisible(): self.__updateSavedGeometry() self.__was_restored = False QDialog.closeEvent(self, ev) def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position: # Restore saved geometry on show self.restoreWidgetPosition() self.__was_restored = True def wheelEvent(self, event): """ Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): if not any(s.name == signalName for s in self.outputs): raise ValueError('{} is not a valid output signal for widget {}'.format( signalName, self.name)) if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) gui.notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): self.settingsHandler.close_context(self) def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) def onDeleteWidget(self): """ Invoked by the canvas to notify the widget it has been deleted from the workflow. If possible, subclasses should gracefully cancel any currently executing tasks. """ pass def handleNewSignals(self): """ Invoked by the workflow signal propagation manager after all signals handlers have been called. Reimplement this method in order to coalesce updates from multiple updated inputs. """ pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self, processEvents=QEventLoop.AllEvents): """ Initialize the widget's progress (i.e show and set progress to 0%). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") if self.__progressState != 1: self.__progressState = 1 self.processingStateChanged.emit(1) self.progressBarSet(0, processEvents) def progressBarSet(self, value, processEvents=QEventLoop.AllEvents): """ Set the current progress bar to `value`. .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param float value: Progress value :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ old = self.__progressBarValue self.__progressBarValue = value if value > 0: if self.__progressState != 1: warnings.warn("progressBarSet() called without a " "preceding progressBarInit()", stacklevel=2) self.__progressState = 1 self.processingStateChanged.emit(1) usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle(self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") if old != value: self.progressBarValueChanged.emit(value) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) processingState = pyqtProperty(int, fget=lambda self: self.__progressState) def progressBarAdvance(self, value, processEvents=QEventLoop.AllEvents): self.progressBarSet(self.progressBarValue + value, processEvents) def progressBarFinished(self, processEvents=QEventLoop.AllEvents): """ Stop the widget's progress (i.e hide the progress bar). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.setWindowTitle(self.captionTitle) if self.__progressState != 0: self.__progressState = 0 self.processingStateChanged.emit(0) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): return self.__statusMessage def keyPressEvent(self, e): if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): self.setState("Info", id, text) def warning(self, id=0, text=""): self.setState("Warning", id, text) def error(self, id=0, text=""): self.setState("Error", id, text) def setState(self, state_type, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[state_type]: self.widgetState[state_type].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[state_type]: self.widgetState[state_type].pop(id) changed = 1 else: self.widgetState[state_type][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(state_type, i, "") else: self.widgetStateChanged.emit(state_type, id, text or "") tooltip_lines = [] highest_type = None for a_type in ("Error", "Warning", "Info"): msgs_for_ids = self.widgetState.get(a_type) if not msgs_for_ids: continue msgs_for_ids = list(msgs_for_ids.values()) if not msgs_for_ids: continue tooltip_lines += msgs_for_ids if highest_type is None: highest_type = a_type if highest_type is None: self.set_warning_bar(None) elif len(tooltip_lines) == 1: msg = tooltip_lines[0] if "\n" in msg: self.set_warning_bar( highest_type, msg[:msg.index("\n")] + " (...)", msg) else: self.set_warning_bar( highest_type, tooltip_lines[0], tooltip_lines[0]) else: self.set_warning_bar( highest_type, "{} problems during execution".format(len(tooltip_lines)), "\n".join(tooltip_lines)) return changed def set_warning_bar(self, state_type, text=None, tooltip=None): colors = {"Error": ("#ffc6c6", "black", QStyle.SP_MessageBoxCritical), "Warning": ("#ffffc9", "black", QStyle.SP_MessageBoxWarning), "Info": ("#ceceff", "black", QStyle.SP_MessageBoxInformation)} current_height = self.height() if state_type is None: if not self.warning_bar.isHidden(): new_height = current_height - self.warning_bar.height() self.warning_bar.setVisible(False) self.resize(self.width(), new_height) return background, foreground, icon = colors[state_type] style = QApplication.instance().style() self.warning_icon.setPixmap(style.standardIcon(icon).pixmap(14, 14)) self.warning_bar.setStyleSheet( "background-color: {}; color: {};" "padding: 3px; padding-left: 6px; vertical-align: center". format(background, foreground)) self.warning_label.setText(text) self.warning_bar.setToolTip(tooltip) if self.warning_bar.isHidden(): self.warning_bar.setVisible(True) new_height = current_height + self.warning_bar.height() self.resize(self.width(), new_height) def widgetStateToHtml(self, info=True, warning=True, error=True): iconpaths = { "Info": gui.resource_filename("icons/information.png"), "Warning": gui.resource_filename("icons/warning.png"), "Error": gui.resource_filename("icons/error.png") } items = [] for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append('<img src="%s" style="float: left;"> %s' % (iconpaths[what], "\n".join(self.widgetState[what].values()))) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): info = QPixmap(gui.resource_filename("icons/information.png")) warning = QPixmap(gui.resource_filename("icons/warning.png")) error = QPixmap(gui.resource_filename("icons/error.png")) cls._cached__widget_state_icons = \ {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible())} def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the workflow signal manager. This is useful for instance if the widget does it's work in a separate thread or schedules processing from the event queue. In this case it can set the blocking flag in it's processNewSignals method schedule the task and return immediately. After the task has completed the widget can clear the flag and send the updated outputs. .. note:: Failure to clear this flag will block dependent nodes forever. """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self)
class SilhouettePlot(QtGui.QGraphicsWidget): """ A silhouette plot widget. """ #: Emitted when the current selection has changed selectionChanged = Signal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.setAcceptHoverEvents(True) self.__groups = [] self.__rowNamesVisible = True self.__barHeight = 3 self.__selectionRect = None self.__selection = numpy.asarray([], dtype=int) self.__selstate = None self.__pen = QtGui.QPen(Qt.NoPen) self.__brush = QtGui.QBrush(QtGui.QColor("#3FCFCF")) self.__layout = QtGui.QGraphicsGridLayout() self.__hoveredItem = None self.setLayout(self.__layout) self.layout().setColumnSpacing(0, 1.) def setScores(self, scores, labels, values, rownames=None): """ Set the silhouette scores/labels to for display. Arguments --------- scores : (N,) ndarray The silhouette scores. labels : (N,) ndarray A ndarray (dtype=int) of label/clusters indices. values : list of str A list of label/cluster names. rownames : list of str, optional A list (len == N) of row names. """ scores = numpy.asarray(scores, dtype=float) labels = numpy.asarray(labels, dtype=int) if rownames is not None: rownames = numpy.asarray(rownames, dtype=object) if not (scores.ndim == labels.ndim == 1): raise ValueError("scores and labels must be 1 dimensional") if scores.shape != labels.shape: raise ValueError("scores and labels must have the same shape") if rownames is not None and rownames.shape != scores.shape: raise ValueError("rownames must have the same size as scores") Ck = numpy.unique(labels) assert Ck[0] >= 0 and Ck[-1] < len(values) cluster_indices = [ numpy.flatnonzero(labels == i) for i in range(len(values)) ] cluster_indices = [ indices[numpy.argsort(scores[indices])[::-1]] for indices in cluster_indices ] groups = [ namespace( scores=scores[indices], indices=indices, label=label, rownames=(rownames[indices] if rownames is not None else None)) for indices, label in zip(cluster_indices, values) ] self.clear() self.__groups = groups self.__setup() def setRowNames(self, names): if names is not None: names = numpy.asarray(names, dtype=object) layout = self.layout() font = self.font() font.setPixelSize(self.__barHeight) for i, grp in enumerate(self.__groups): grp.rownames = names[grp.indices] if names is not None else None item = layout.itemAt(i + 1, 3) if grp.rownames is not None: item.setItems(grp.rownames) item.setVisible(self.__rowNamesVisible) else: item.setItems([]) item.setVisible(False) barplot = list(self.__plotItems())[i] baritems = barplot.items() if grp.rownames is None: tooltips = itertools.repeat("") else: tooltips = grp.rownames for bar, tooltip in zip(baritems, tooltips): bar.setToolTip(tooltip) self.layout().activate() def setRowNamesVisible(self, visible): if self.__rowNamesVisible != visible: self.__rowNamesVisible = visible for item in self.__textItems(): item.setVisible(visible) def rowNamesVisible(self): return self.__rowNamesVisible def setBarHeight(self, height): """ Set silhouette bar height (row height). """ if height != self.__barHeight: self.__barHeight = height for item in self.__plotItems(): item.setPreferredBarSize(height) font = self.font() font.setPixelSize(height) for item in self.__textItems(): item.setFont(font) def barHeight(self): """ Return the silhouette bar (row) height. """ return self.__barHeight def clear(self): """ Clear the widget state """ scene = self.scene() for child in self.childItems(): child.setParentItem(None) scene.removeItem(child) self.__groups = [] def __setup(self): # Setup the subwidgets/groups/layout smax = max( (numpy.max(g.scores) for g in self.__groups if g.scores.size), default=1) smin = min( (numpy.min(g.scores) for g in self.__groups if g.scores.size), default=-1) smin = min(smin, 0) font = self.font() font.setPixelSize(self.__barHeight) axispen = QtGui.QPen(Qt.black) ax = pg.AxisItem(parent=self, orientation="top", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.layout().addItem(ax, 0, 2) for i, group in enumerate(self.__groups): silhouettegroup = BarPlotItem(parent=self) silhouettegroup.setBrush(self.__brush) silhouettegroup.setPen(self.__pen) silhouettegroup.setDataRange(smin, smax) silhouettegroup.setPlotData(group.scores) silhouettegroup.setPreferredBarSize(self.__barHeight) silhouettegroup.setData(0, group.indices) self.layout().addItem(silhouettegroup, i + 1, 2) if group.label: line = QtGui.QFrame(frameShape=QtGui.QFrame.VLine) proxy = QtGui.QGraphicsProxyWidget(self) proxy.setWidget(line) self.layout().addItem(proxy, i + 1, 1) label = QtGui.QGraphicsSimpleTextItem(self) label.setText("{} ({})".format(escape(group.label), len(group.scores))) item = WrapperLayoutItem(label, Qt.Vertical, parent=self) self.layout().addItem(item, i + 1, 0, Qt.AlignCenter) textlist = TextListWidget(self, font=font) sp = textlist.sizePolicy() sp.setVerticalPolicy(QtGui.QSizePolicy.Ignored) textlist.setSizePolicy(sp) textlist.setParent(self) if group.rownames is not None: textlist.setItems(group.items) textlist.setVisible(self.__rowNamesVisible) else: textlist.setVisible(False) self.layout().addItem(textlist, i + 1, 3) ax = pg.AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.layout().addItem(ax, len(self.__groups) + 1, 2) def __updateTextSizeConstraint(self): # set/update fixed height constraint on the text annotation items so # it matches the silhouette's height for silitem, textitem in zip(self.__plotItems(), self.__textItems()): height = silitem.effectiveSizeHint(Qt.PreferredSize).height() textitem.setMaximumHeight(height) textitem.setMinimumHeight(height) def event(self, event): # Reimplemented if event.type() == QEvent.LayoutRequest and \ self.parentLayoutItem() is None: self.__updateTextSizeConstraint() self.resize(self.effectiveSizeHint(Qt.PreferredSize)) return super().event(event) def __setHoveredItem(self, item): # Set the current hovered `item` (:class:`QGraphicsRectItem`) if self.__hoveredItem is not item: if self.__hoveredItem is not None: self.__hoveredItem.setPen(QtGui.QPen(Qt.NoPen)) self.__hoveredItem = item if item is not None: item.setPen(QtGui.QPen(Qt.lightGray)) def hoverEnterEvent(self, event): # Reimplemented event.accept() def hoverMoveEvent(self, event): # Reimplemented event.accept() item = self.itemAtPos(event.pos()) self.__setHoveredItem(item) def hoverLeaveEvent(self, event): # Reimplemented self.__setHoveredItem(None) event.accept() def mousePressEvent(self, event): # Reimplemented if event.button() == Qt.LeftButton: if event.modifiers() & Qt.ControlModifier: saction = SelectAction.Toogle elif event.modifiers() & Qt.AltModifier: saction = SelectAction.Deselect elif event.modifiers() & Qt.ShiftModifier: saction = SelectAction.Select else: saction = SelectAction.Clear | SelectAction.Select self.__selstate = namespace( modifiers=event.modifiers(), selection=self.__selection, action=saction, rect=None, ) if saction & SelectAction.Clear: self.__selstate.selection = numpy.array([], dtype=int) self.setSelection(self.__selstate.selection) event.accept() def mouseMoveEvent(self, event): # Reimplemented if event.buttons() & Qt.LeftButton: assert self.__selstate is not None if self.__selectionRect is None: self.__selectionRect = QtGui.QGraphicsRectItem(self) rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos()).normalized()) if not rect.width(): rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7) rect = rect.intersected(self.contentsRect()) self.__selectionRect.setRect(rect) self.__selstate.rect = rect self.__selstate.action |= SelectAction.Current self.__setSelectionRect(rect, self.__selstate.action) event.accept() def mouseReleaseEvent(self, event): # Reimplemented if event.button() == Qt.LeftButton: if self.__selectionRect is not None: self.__selectionRect.setParentItem(None) if self.scene() is not None: self.scene().removeItem(self.__selectionRect) self.__selectionRect = None event.accept() rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos()).normalized()) if not rect.isValid(): rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7) rect = rect.intersected(self.contentsRect()) action = action = self.__selstate.action & ~SelectAction.Current self.__setSelectionRect(rect, action) self.__selstate = None def __setSelectionRect(self, rect, action): # Set the current mouse drag selection rectangle if not rect.isValid(): rect = rect.adjusted(-0.01, -0.01, 0.01, 0.01) rect = rect.intersected(self.contentsRect()) indices = self.__selectionIndices(rect) if action & SelectAction.Clear: selection = [] elif self.__selstate is not None: # Mouse drag selection is in progress. Update only the current # selection selection = self.__selstate.selection else: selection = self.__selection if action & SelectAction.Toogle: selection = numpy.setxor1d(selection, indices) elif action & SelectAction.Deselect: selection = numpy.setdiff1d(selection, indices) elif action & SelectAction.Select: selection = numpy.union1d(selection, indices) self.setSelection(selection) def __selectionIndices(self, rect): items = [ item for item in self.__plotItems() if item.geometry().intersects(rect) ] selection = [numpy.array([], dtype=int)] for item in items: indices = item.data(0) itemrect = item.geometry().intersected(rect) crect = item.contentsRect() itemrect = ( item.mapFromParent(itemrect).boundingRect().intersected(crect)) assert itemrect.top() >= 0 rowh = crect.height() / item.count() indextop = numpy.floor(itemrect.top() / rowh) indexbottom = numpy.ceil(itemrect.bottom() / rowh) selection.append(indices[int(indextop):int(indexbottom)]) return numpy.hstack(selection) def itemAtPos(self, pos): items = [ item for item in self.__plotItems() if item.geometry().contains(pos) ] if not items: return None else: item = items[0] crect = item.contentsRect() pos = item.mapFromParent(pos) if not crect.contains(pos): return None assert pos.x() >= 0 rowh = crect.height() / item.count() index = int(numpy.floor(pos.y() / rowh)) index = min(index, item.count() - 1) if index >= 0: return item.items()[index] else: return None def indexAtPos(self, pos): items = [ item for item in self.__plotItems() if item.geometry().contains(pos) ] if not items: return -1 else: item = items[0] indices = item.data(0) assert (isinstance(indices, numpy.ndarray) and indices.shape == (item.count(), )) crect = item.contentsRect() pos = item.mapFromParent(pos) if not crect.contains(pos): return -1 assert pos.x() >= 0 rowh = crect.height() / item.count() index = numpy.floor(pos.y() / rowh) index = min(index, indices.size - 1) if index >= 0: return indices[index] else: return -1 def __selectionChanged(self, selected, deselected): for item, grp in zip(self.__plotItems(), self.__groups): select = numpy.flatnonzero( numpy.in1d(grp.indices, selected, assume_unique=True)) items = item.items() if select.size: for i in select: items[i].setBrush(Qt.red) deselect = numpy.flatnonzero( numpy.in1d(grp.indices, deselected, assume_unique=True)) if deselect.size: for i in deselect: items[i].setBrush(self.__brush) def __plotItems(self): for i in range(len(self.__groups)): item = self.layout().itemAt(i + 1, 2) if item is not None: assert isinstance(item, BarPlotItem) yield item def __textItems(self): for i in range(len(self.__groups)): item = self.layout().itemAt(i + 1, 3) if item is not None: assert isinstance(item, TextListWidget) yield item def setSelection(self, indices): indices = numpy.unique(numpy.asarray(indices, dtype=int)) select = numpy.setdiff1d(indices, self.__selection) deselect = numpy.setdiff1d(self.__selection, indices) self.__selectionChanged(select, deselect) self.__selection = indices if deselect.size or select.size: self.selectionChanged.emit() def selection(self): return numpy.asarray(self.__selection, dtype=int)
class SliderLine(QGraphicsObject): """A movable slider line.""" valueChanged = Signal(float) linePressed = Signal() lineMoved = Signal() lineReleased = Signal() rangeChanged = Signal(float, float) def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0, length=10.0, **kwargs): self._orientation = orientation self._value = value self._length = length self._min = 0.0 self._max = 1.0 self._line = QLineF() self._pen = QPen() super().__init__(parent, **kwargs) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPen(make_pen(brush=QColor(50, 50, 50), width=1, cosmetic=True)) if self._orientation == Qt.Vertical: self.setCursor(Qt.SizeVerCursor) else: self.setCursor(Qt.SizeHorCursor) def setPen(self, pen): pen = QPen(pen) if self._pen != pen: self.prepareGeometryChange() self._pen = pen self._line = None self.update() def pen(self): return QPen(self._pen) def setValue(self, value): value = min(max(value, self._min), self._max) if self._value != value: self.prepareGeometryChange() self._value = value self._line = None self.valueChanged.emit(value) def value(self): return self._value def setRange(self, minval, maxval): maxval = max(minval, maxval) if minval != self._min or maxval != self._max: self._min = minval self._max = maxval self.rangeChanged.emit(minval, maxval) self.setValue(self._value) def setLength(self, length): if self._length != length: self.prepareGeometryChange() self._length = length self._line = None def length(self): return self._length def setOrientation(self, orientation): if self._orientation != orientation: self.prepareGeometryChange() self._orientation = orientation self._line = None if self._orientation == Qt.Vertical: self.setCursor(Qt.SizeVerCursor) else: self.setCursor(Qt.SizeHorCursor) def mousePressEvent(self, event): event.accept() self.linePressed.emit() def mouseMoveEvent(self, event): pos = event.pos() if self._orientation == Qt.Vertical: self.setValue(pos.y()) else: self.setValue(pos.x()) self.lineMoved.emit() event.accept() def mouseReleaseEvent(self, event): if self._orientation == Qt.Vertical: self.setValue(event.pos().y()) else: self.setValue(event.pos().x()) self.lineReleased.emit() event.accept() def boundingRect(self): if self._line is None: if self._orientation == Qt.Vertical: self._line = QLineF(0, self._value, self._length, self._value) else: self._line = QLineF(self._value, 0, self._value, self._length) r = QRectF(self._line.p1(), self._line.p2()) penw = self.pen().width() return r.adjusted(-penw, -penw, penw, penw) def paint(self, painter, *args): if self._line is None: self.boundingRect() painter.save() painter.setPen(self.pen()) painter.drawLine(self._line) painter.restore()
class ActionToolBar(QtGui.QFrame): iconSizeChanged = Signal(QtCore.QSize) actionTriggered = Signal(QtGui.QAction) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) layout = QtGui.QHBoxLayout(spacing=1) layout.setContentsMargins(0, 0, 0, 0) if "sizePolicy" not in kwargs: self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self.setLayout(layout) layout.addStretch() self._actions = [] def clear(self): for action in reversed(self.actions()): self.removeAction(action) def iconSize(self): if self._iconSize is None: style = self.style() pm = style.pixelMetric(QtGui.QStyle.PM_ToolBarIconSize) return QtCore.QSize(pm, pm) else: return self._iconSize def setIconSize(self, size): if self._iconSize != size: changed = self.iconSize() != size self._iconSize = size if changed: self.iconSizeChanged.emit(self.iconSize()) def buttonForAction(self, action): for ac, button in self._actions: if action is ac: return button return None def actionEvent(self, event): super().actionEvent(event) if event.type() == QEvent.ActionAdded: self._insertActionBefore(event.action(), event.before()) elif event.type() == QEvent.ActionRemoved: self._removeAction(event.action()) elif event.type() == QEvent.ActionChanged: self._updateAction(event.action()) def _insertActionBefore(self, action, before=None): index = len(self._actions) if action is not None: actions = [a for a, _ in self._actions] try: index = actions.index(before) except ValueError: pass button = self._button(action) self._actions.insert(index, (action, button)) self.layout().insertWidget(index, button) button.triggered.connect(self.actionTriggered) def _removeAction(self, action): actions = [a for a, _ in self._actions] try: index = actions.index(action) except ValueError: raise else: _, button = self._actions[index] self.layout().takeAt(index) button.hide() button.deleteLater() del self._actions[index] def _updateAction(self, action): pass def _button(self, action): b = ActionToolBarButton( toolButtonStyle=Qt.ToolButtonIconOnly, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) ) b.setDefaultAction(action) b.setPopupMode(QtGui.QToolButton.InstantPopup) return b
class ReportModel(QAbstractTableModel): need_evaluate_editor = Signal(bool, QModelIndex) def __init__(self): QAbstractTableModel.__init__(self) self.items = [] self.bisector = None self.single_runner = None def clear(self): self.beginResetModel() self.items = [] self.endResetModel() @Slot(object) def attach_bisector(self, bisector): bisector_slots = ('step_started', 'step_build_found', 'step_testing', 'step_finished', 'started', 'finished') downloader_slots = ('download_progress', ) if self.bisector: _bulk_action_slots('disconnect', bisector_slots, self.bisector, self) _bulk_action_slots('disconnect', downloader_slots, self.bisector.download_manager, self) if bisector: self.attach_single_runner(None) _bulk_action_slots('connect', bisector_slots, bisector, self) _bulk_action_slots('connect', downloader_slots, bisector.download_manager, self) self.bisector = bisector @Slot(object) def attach_single_runner(self, single_runner): sr_slots = ('started', 'step_build_found', 'step_testing') downloader_slots = ('download_progress', ) if self.single_runner: _bulk_action_slots('disconnect', sr_slots, self.single_runner, self) _bulk_action_slots('disconnect', downloader_slots, self.single_runner.download_manager, self) if single_runner: self.attach_bisector(None) _bulk_action_slots('connect', sr_slots, single_runner, self) _bulk_action_slots('connect', downloader_slots, single_runner.download_manager, self) self.single_runner = single_runner @Slot(object, int, int) def download_progress(self, dl, current, total): item = self.items[-1] item.state_text = 'Downloading' item.downloading = True item.set_progress(current, total) self.update_item(item) def get_item(self, index): return self.items[index.row()] def rowCount(self, parent=QModelIndex()): return len(self.items) def columnCount(self, parent=QModelIndex()): return 1 def data(self, index, role=Qt.DisplayRole): item = self.items[index.row()] if role == Qt.DisplayRole: return item.status_text() elif role == Qt.BackgroundRole: if isinstance(item, StepItem) and item.verdict: return VERDICT_TO_ROW_COLORS.get(str(item.verdict), GRAY_WHITE) else: return GRAY_WHITE return None def update_item(self, item): index = self.createIndex(self.items.index(item), 0) self.dataChanged.emit(index, index) def append_item(self, item): row = self.rowCount() self.beginInsertRows(QModelIndex(), row, row) self.items.append(item) self.endInsertRows() @Slot() def started(self): # when a bisection starts, insert an item to report it self.append_item(StartItem()) @Slot(object, int) def step_started(self, bisection): last_item = self.items[-1] if isinstance(last_item, StepItem): # update the pushlog for the last step last_item.update_pushlogurl(bisection) self.update_item(last_item) # and add a new step self.append_item(StepItem()) @Slot(object, int, object) def step_build_found(self, bisection, build_infos): last_item = self.items[-1] if isinstance(last_item, StartItem): # update the pushlog for the start step if hasattr(bisection, 'handler'): last_item.update_pushlogurl(bisection) self.update_item(last_item) else: # single runner case # TODO: rework report.py implementation... self.finished(None, None) # remove last item # and add the new step with build_infos item = StepItem() item.data.update(build_infos.to_dict()) self.append_item(item) else: # previous item is a step, just update it last_item.data.update(build_infos.to_dict()) self.update_item(last_item) @Slot(object, int, object) def step_testing(self, bisection, build_infos): last_item = self.items[-1] last_item.downloading = False last_item.waiting_evaluation = True last_item.state_text = 'Testing' self.update_item(last_item) if hasattr(bisection, 'handler'): # not a single runner index = self.createIndex(self.rowCount() - 1, 0) self.need_evaluate_editor.emit(True, index) @Slot(object, int, str) def step_finished(self, bisection, verdict): # step finished, just store the verdict item = self.items[-1] item.waiting_evaluation = False item.state_text = 'Tested' item.verdict = verdict self.update_item(item) if hasattr(bisection, 'handler'): # not a single runner index = self.createIndex(self.rowCount() - 1, 0) self.need_evaluate_editor.emit(False, index) @Slot(object, int) def finished(self, bisection, result): # remove the last insterted step index = len(self.items) - 1 self.beginRemoveRows(QModelIndex(), index, index) self.items.pop(index) self.endRemoveRows()
class TextAnnotation(Annotation): """Text annotation item for the canvas scheme. """ editingFinished = Signal() """Emitted when the editing is finished (i.e. the item loses focus).""" textEdited = Signal() """Emitted when the edited text changes.""" def __init__(self, parent=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) self.__textMargins = (2, 2, 2, 2) rect = self.geometry().translated(-self.pos()) self.__framePathItem = QGraphicsPathItem(self) self.__framePathItem.setPen(QPen(Qt.NoPen)) self.__textItem = GraphicsTextEdit(self) self.__textItem.setPlaceholderText(self.tr("Enter text here")) self.__textItem.setPos(2, 2) self.__textItem.setTextWidth(rect.width() - 4) self.__textItem.setTabChangesFocus(True) self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.setFont(self.font()) self.__textInteractionFlags = Qt.NoTextInteraction layout = self.__textItem.document().documentLayout() layout.documentSizeChanged.connect(self.__onDocumentSizeChanged) self.__updateFrame() def adjustSize(self): """Resize to a reasonable size. """ self.__textItem.setTextWidth(-1) self.__textItem.adjustSize() size = self.__textItem.boundingRect().size() left, top, right, bottom = self.textMargins() geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom)) self.setGeometry(geom) def setFramePen(self, pen): """Set the frame pen. By default Qt.NoPen is used (i.e. the frame is not shown). """ self.__framePathItem.setPen(pen) def framePen(self): """Return the frame pen. """ return self.__framePathItem.pen() def setFrameBrush(self, brush): """Set the frame brush. """ self.__framePathItem.setBrush(brush) def frameBrush(self): """Return the frame brush. """ return self.__framePathItem.brush() def setPlainText(self, text): """Set the annotation plain text. """ self.__textItem.setPlainText(text) def toPlainText(self): return self.__textItem.toPlainText() def setHtml(self, text): """Set the annotation rich text. """ self.__textItem.setHtml(text) def toHtml(self): return self.__textItem.toHtml() def setDefaultTextColor(self, color): """Set the default text color. """ self.__textItem.setDefaultTextColor(color) def defaultTextColor(self): return self.__textItem.defaultTextColor() def setTextMargins(self, left, top, right, bottom): """Set the text margins. """ margins = (left, top, right, bottom) if self.__textMargins != margins: self.__textMargins = margins self.__textItem.setPos(left, top) self.__textItem.setTextWidth( max(self.geometry().width() - left - right, 0)) def textMargins(self): """Return the text margins. """ return self.__textMargins def document(self): """Return the QTextDocument instance used internally. """ return self.__textItem.document() def setTextCursor(self, cursor): self.__textItem.setTextCursor(cursor) def textCursor(self): return self.__textItem.textCursor() def setTextInteractionFlags(self, flags): self.__textInteractionFlags = flags def textInteractionFlags(self): return self.__textInteractionFlags def setDefaultStyleSheet(self, stylesheet): self.document().setDefaultStyleSheet(stylesheet) def mouseDoubleClickEvent(self, event): Annotation.mouseDoubleClickEvent(self, event) if event.buttons() == Qt.LeftButton and \ self.__textInteractionFlags & Qt.TextEditable: self.startEdit() def startEdit(self): """Start the annotation text edit process. """ self.__textItem.setTextInteractionFlags(self.__textInteractionFlags) self.__textItem.setFocus(Qt.MouseFocusReason) # Install event filter to find out when the text item loses focus. self.__textItem.installSceneEventFilter(self) self.__textItem.document().contentsChanged.connect(self.textEdited) def endEdit(self): """End the annotation edit. """ self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.removeSceneEventFilter(self) self.__textItem.document().contentsChanged.disconnect(self.textEdited) cursor = self.__textItem.textCursor() cursor.clearSelection() self.__textItem.setTextCursor(cursor) self.editingFinished.emit() def __onDocumentSizeChanged(self, size): # The size of the text document has changed. Expand the text # control rect's height if the text no longer fits inside. try: rect = self.geometry() _, top, _, bottom = self.textMargins() if rect.height() < (size.height() + bottom + top): rect.setHeight(size.height() + bottom + top) self.setGeometry(rect) except Exception: log.error("error in __onDocumentSizeChanged", exc_info=True) def __updateFrame(self): rect = self.geometry() rect.moveTo(0, 0) path = QPainterPath() path.addRect(rect) self.__framePathItem.setPath(path) def resizeEvent(self, event): width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() QGraphicsWidget.resizeEvent(self, event) def sceneEventFilter(self, obj, event): if obj is self.__textItem and event.type() == QEvent.FocusOut: self.__textItem.focusOutEvent(event) self.endEdit() return True return Annotation.sceneEventFilter(self, obj, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: if self.isSelected(): self.setFramePen(QPen(Qt.DashDotLine)) else: self.setFramePen(QPen(Qt.NoPen)) return Annotation.itemChange(self, change, value) def changeEvent(self, event): if event.type() == QEvent.FontChange: self.__textItem.setFont(self.font()) Annotation.changeEvent(self, event)
class CanvasScene(QGraphicsScene): """ A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance. """ #: Signal emitted when a :class:`NodeItem` has been added to the scene. node_item_added = Signal(object) #: Signal emitted when a :class:`NodeItem` has been removed from the #: scene. node_item_removed = Signal(object) #: Signal emitted when a new :class:`LinkItem` has been added to the #: scene. link_item_added = Signal(object) #: Signal emitted when a :class:`LinkItem` has been removed. link_item_removed = Signal(object) #: Signal emitted when a :class:`Annotation` item has been added. annotation_added = Signal(object) #: Signal emitted when a :class:`Annotation` item has been removed. annotation_removed = Signal(object) #: Signal emitted when the position of a :class:`NodeItem` has changed. node_item_position_changed = Signal(object, QPointF) #: Signal emitted when an :class:`NodeItem` has been double clicked. node_item_double_clicked = Signal(object) #: An node item has been activated (clicked) node_item_activated = Signal(object) #: An node item has been hovered node_item_hovered = Signal(object) #: Link item has been hovered link_item_hovered = Signal(object) def __init__(self, *args, **kwargs): QGraphicsScene.__init__(self, *args, **kwargs) self.scheme = None self.registry = None # All node items self.__node_items = [] # Mapping from SchemeNodes to canvas items self.__item_for_node = {} # All link items self.__link_items = [] # Mapping from SchemeLinks to canvas items. self.__item_for_link = {} # All annotation items self.__annotation_items = [] # Mapping from SchemeAnnotations to canvas items. self.__item_for_annotation = {} # Is the scene editable self.editable = True # Anchor Layout self.__anchor_layout = AnchorLayout() self.addItem(self.__anchor_layout) self.__channel_names_visible = True self.__node_animation_enabled = True self.user_interaction_handler = None self.activated_mapper = QSignalMapper(self) self.activated_mapper.mapped[QObject].connect( lambda node: self.node_item_activated.emit(node) ) self.hovered_mapper = QSignalMapper(self) self.hovered_mapper.mapped[QObject].connect( lambda node: self.node_item_hovered.emit(node) ) self.position_change_mapper = QSignalMapper(self) self.position_change_mapper.mapped[QObject].connect( self._on_position_change ) log.info("'%s' intitialized." % self) def clear_scene(self): """ Clear (reset) the scene. """ if self.scheme is not None: self.scheme.node_added.disconnect(self.add_node) self.scheme.node_removed.disconnect(self.remove_node) self.scheme.link_added.disconnect(self.add_link) self.scheme.link_removed.disconnect(self.remove_link) self.scheme.annotation_added.disconnect(self.add_annotation) self.scheme.annotation_removed.disconnect(self.remove_annotation) self.scheme.node_state_changed.disconnect( self.on_widget_state_change ) self.scheme.channel_state_changed.disconnect( self.on_link_state_change ) # Remove all items to make sure all signals from scheme items # to canvas items are disconnected. for annot in self.scheme.annotations: if annot in self.__item_for_annotation: self.remove_annotation(annot) for link in self.scheme.links: if link in self.__item_for_link: self.remove_link(link) for node in self.scheme.nodes: if node in self.__item_for_node: self.remove_node(node) self.scheme = None self.__node_items = [] self.__item_for_node = {} self.__link_items = [] self.__item_for_link = {} self.__annotation_items = [] self.__item_for_annotation = {} self.__anchor_layout.deleteLater() self.user_interaction_handler = None self.clear() log.info("'%s' cleared." % self) def set_scheme(self, scheme): """ Set the scheme to display. Populates the scene with nodes and links already in the scheme. Any further change to the scheme will be reflected in the scene. Parameters ---------- scheme : :class:`~.scheme.Scheme` """ if self.scheme is not None: # Clear the old scheme self.clear_scene() log.info("Setting scheme '%s' on '%s'" % (scheme, self)) self.scheme = scheme if self.scheme is not None: self.scheme.node_added.connect(self.add_node) self.scheme.node_removed.connect(self.remove_node) self.scheme.link_added.connect(self.add_link) self.scheme.link_removed.connect(self.remove_link) self.scheme.annotation_added.connect(self.add_annotation) self.scheme.annotation_removed.connect(self.remove_annotation) self.scheme.node_state_changed.connect( self.on_widget_state_change ) self.scheme.channel_state_changed.connect( self.on_link_state_change ) self.scheme.topology_changed.connect(self.on_scheme_change) for node in scheme.nodes: self.add_node(node) for link in scheme.links: self.add_link(link) for annot in scheme.annotations: self.add_annotation(annot) def set_registry(self, registry): """ Set the widget registry. """ # TODO: Remove/Deprecate. Is used only to get the category/background # color. That should be part of the SchemeNode/WidgetDescription. log.info("Setting registry '%s on '%s'." % (registry, self)) self.registry = registry def set_anchor_layout(self, layout): """ Set an :class:`~.layout.AnchorLayout` """ if self.__anchor_layout != layout: if self.__anchor_layout: self.__anchor_layout.deleteLater() self.__anchor_layout = None self.__anchor_layout = layout def anchor_layout(self): """ Return the anchor layout instance. """ return self.__anchor_layout def set_channel_names_visible(self, visible): """ Set the channel names visibility. """ self.__channel_names_visible = visible for link in self.__link_items: link.setChannelNamesVisible(visible) def channel_names_visible(self): """ Return the channel names visibility state. """ return self.__channel_names_visible def set_node_animation_enabled(self, enabled): """ Set node animation enabled state. """ if self.__node_animation_enabled != enabled: self.__node_animation_enabled = enabled for node in self.__node_items: node.setAnimationEnabled(enabled) def add_node_item(self, item): """ Add a :class:`.NodeItem` instance to the scene. """ if item in self.__node_items: raise ValueError("%r is already in the scene." % item) if item.pos().isNull(): if self.__node_items: pos = self.__node_items[-1].pos() + QPointF(150, 0) else: pos = QPointF(150, 150) item.setPos(pos) item.setFont(self.font()) # Set signal mappings self.activated_mapper.setMapping(item, item) item.activated.connect(self.activated_mapper.map) self.hovered_mapper.setMapping(item, item) item.hovered.connect(self.hovered_mapper.map) self.position_change_mapper.setMapping(item, item) item.positionChanged.connect(self.position_change_mapper.map) self.addItem(item) self.__node_items.append(item) self.node_item_added.emit(item) log.info("Added item '%s' to '%s'" % (item, self)) return item def add_node(self, node): """ Add and return a default constructed :class:`.NodeItem` for a :class:`SchemeNode` instance `node`. If the `node` is already in the scene do nothing and just return its item. """ if node in self.__item_for_node: # Already added return self.__item_for_node[node] item = self.new_node_item(node.description) if node.position: pos = QPointF(*node.position) item.setPos(pos) item.setTitle(node.title) item.setProcessingState(node.processing_state) item.setProgress(node.progress) for message in node.state_messages(): item.setStateMessage(message) item.setStatusMessage(node.status_message()) self.__item_for_node[node] = item node.position_changed.connect(self.__on_node_pos_changed) node.title_changed.connect(item.setTitle) node.progress_changed.connect(item.setProgress) node.processing_state_changed.connect(item.setProcessingState) node.state_message_changed.connect(item.setStateMessage) node.status_message_changed.connect(item.setStatusMessage) return self.add_node_item(item) def new_node_item(self, widget_desc, category_desc=None): """ Construct an new :class:`.NodeItem` from a `WidgetDescription`. Optionally also set `CategoryDescription`. """ item = items.NodeItem() item.setWidgetDescription(widget_desc) if category_desc is None and self.registry and widget_desc.category: category_desc = self.registry.category(widget_desc.category) if category_desc is None and self.registry is not None: try: category_desc = self.registry.category(widget_desc.category) except KeyError: pass if category_desc is not None: item.setWidgetCategory(category_desc) item.setAnimationEnabled(self.__node_animation_enabled) return item def remove_node_item(self, item): """ Remove `item` (:class:`.NodeItem`) from the scene. """ self.activated_mapper.removeMappings(item) self.hovered_mapper.removeMappings(item) self.position_change_mapper.removeMappings(item) item.hide() self.removeItem(item) self.__node_items.remove(item) self.node_item_removed.emit(item) log.info("Removed item '%s' from '%s'" % (item, self)) def remove_node(self, node): """ Remove the :class:`.NodeItem` instance that was previously constructed for a :class:`SchemeNode` `node` using the `add_node` method. """ item = self.__item_for_node.pop(node) node.position_changed.disconnect(self.__on_node_pos_changed) node.title_changed.disconnect(item.setTitle) node.progress_changed.disconnect(item.setProgress) node.processing_state_changed.disconnect(item.setProcessingState) node.state_message_changed.disconnect(item.setStateMessage) self.remove_node_item(item) def node_items(self): """ Return all :class:`.NodeItem` instances in the scene. """ return list(self.__node_items) def add_link_item(self, item): """ Add a link (:class:`.LinkItem`) to the scene. """ if item.scene() is not self: self.addItem(item) item.setFont(self.font()) self.__link_items.append(item) self.link_item_added.emit(item) log.info("Added link %r -> %r to '%s'" % \ (item.sourceItem.title(), item.sinkItem.title(), self)) self.__anchor_layout.invalidateLink(item) return item def add_link(self, scheme_link): """ Create and add a :class:`.LinkItem` instance for a :class:`SchemeLink` instance. If the link is already in the scene do nothing and just return its :class:`.LinkItem`. """ if scheme_link in self.__item_for_link: return self.__item_for_link[scheme_link] source = self.__item_for_node[scheme_link.source_node] sink = self.__item_for_node[scheme_link.sink_node] item = self.new_link_item(source, scheme_link.source_channel, sink, scheme_link.sink_channel) item.setEnabled(scheme_link.enabled) scheme_link.enabled_changed.connect(item.setEnabled) if scheme_link.is_dynamic(): item.setDynamic(True) item.setDynamicEnabled(scheme_link.dynamic_enabled) scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled) item.setRuntimeState(scheme_link.runtime_state()) scheme_link.state_changed.connect(item.setRuntimeState) self.add_link_item(item) self.__item_for_link[scheme_link] = item return item def new_link_item(self, source_item, source_channel, sink_item, sink_channel): """ Construct and return a new :class:`.LinkItem` """ item = items.LinkItem() item.setSourceItem(source_item) item.setSinkItem(sink_item) def channel_name(channel): if isinstance(channel, str): return channel else: return channel.name source_name = channel_name(source_channel) sink_name = channel_name(sink_channel) fmt = "<b>{0}</b> \u2192 <b>{1}</b>" item.setToolTip( fmt.format(escape(source_name), escape(sink_name)) ) item.setSourceName(source_name) item.setSinkName(sink_name) item.setChannelNamesVisible(self.__channel_names_visible) return item def remove_link_item(self, item): """ Remove a link (:class:`.LinkItem`) from the scene. """ # Invalidate the anchor layout. self.__anchor_layout.invalidateAnchorItem( item.sourceItem.outputAnchorItem ) self.__anchor_layout.invalidateAnchorItem( item.sinkItem.inputAnchorItem ) self.__link_items.remove(item) # Remove the anchor points. item.removeLink() self.removeItem(item) self.link_item_removed.emit(item) log.info("Removed link '%s' from '%s'" % (item, self)) return item def remove_link(self, scheme_link): """ Remove a :class:`.LinkItem` instance that was previously constructed for a :class:`SchemeLink` instance `link` using the `add_link` method. """ item = self.__item_for_link.pop(scheme_link) scheme_link.enabled_changed.disconnect(item.setEnabled) if scheme_link.is_dynamic(): scheme_link.dynamic_enabled_changed.disconnect( item.setDynamicEnabled ) scheme_link.state_changed.disconnect(item.setRuntimeState) self.remove_link_item(item) def link_items(self): """ Return all :class:`.LinkItem`\s in the scene. """ return list(self.__link_items) def add_annotation_item(self, annotation): """ Add an :class:`.Annotation` item to the scene. """ self.__annotation_items.append(annotation) self.addItem(annotation) self.annotation_added.emit(annotation) return annotation def add_annotation(self, scheme_annot): """ Create a new item for :class:`SchemeAnnotation` and add it to the scene. If the `scheme_annot` is already in the scene do nothing and just return its item. """ if scheme_annot in self.__item_for_annotation: # Already added return self.__item_for_annotation[scheme_annot] if isinstance(scheme_annot, scheme.SchemeTextAnnotation): item = items.TextAnnotation() item.setPlainText(scheme_annot.text) x, y, w, h = scheme_annot.rect item.setPos(x, y) item.resize(w, h) item.setTextInteractionFlags(Qt.TextEditorInteraction) font = font_from_dict(scheme_annot.font, item.font()) item.setFont(font) scheme_annot.text_changed.connect(item.setPlainText) elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation): item = items.ArrowAnnotation() start, end = scheme_annot.start_pos, scheme_annot.end_pos item.setLine(QLineF(QPointF(*start), QPointF(*end))) item.setColor(QColor(scheme_annot.color)) scheme_annot.geometry_changed.connect( self.__on_scheme_annot_geometry_change ) self.add_annotation_item(item) self.__item_for_annotation[scheme_annot] = item return item def remove_annotation_item(self, annotation): """ Remove an :class:`.Annotation` instance from the scene. """ self.__annotation_items.remove(annotation) self.removeItem(annotation) self.annotation_removed.emit(annotation) def remove_annotation(self, scheme_annotation): """ Remove an :class:`.Annotation` instance that was previously added using :func:`add_anotation`. """ item = self.__item_for_annotation.pop(scheme_annotation) scheme_annotation.geometry_changed.disconnect( self.__on_scheme_annot_geometry_change ) if isinstance(scheme_annotation, scheme.SchemeTextAnnotation): scheme_annotation.text_changed.disconnect( item.setPlainText ) self.remove_annotation_item(item) def annotation_items(self): """ Return all :class:`.Annotation` items in the scene. """ return self.__annotation_items def item_for_annotation(self, scheme_annotation): return self.__item_for_annotation[scheme_annotation] def annotation_for_item(self, item): rev = dict(reversed(item) \ for item in self.__item_for_annotation.items()) return rev[item] def commit_scheme_node(self, node): """ Commit the `node` into the scheme. """ if not self.editable: raise Exception("Scheme not editable.") if node not in self.__item_for_node: raise ValueError("No 'NodeItem' for node.") item = self.__item_for_node[node] try: self.scheme.add_node(node) except Exception: log.error("An error occurred while committing node '%s'", node, exc_info=True) # Cleanup (remove the node item) self.remove_node_item(item) raise log.info("Commited node '%s' from '%s' to '%s'" % \ (node, self, self.scheme)) def commit_scheme_link(self, link): """ Commit a scheme link. """ if not self.editable: raise Exception("Scheme not editable") if link not in self.__item_for_link: raise ValueError("No 'LinkItem' for link.") self.scheme.add_link(link) log.info("Commited link '%s' from '%s' to '%s'" % \ (link, self, self.scheme)) def node_for_item(self, item): """ Return the `SchemeNode` for the `item`. """ rev = dict([(v, k) for k, v in self.__item_for_node.items()]) return rev[item] def item_for_node(self, node): """ Return the :class:`NodeItem` instance for a :class:`SchemeNode`. """ return self.__item_for_node[node] def link_for_item(self, item): """ Return the `SchemeLink for `item` (:class:`LinkItem`). """ rev = dict([(v, k) for k, v in self.__item_for_link.items()]) return rev[item] def item_for_link(self, link): """ Return the :class:`LinkItem` for a :class:`SchemeLink` """ return self.__item_for_link[link] def selected_node_items(self): """ Return the selected :class:`NodeItem`'s. """ return [item for item in self.__node_items if item.isSelected()] def selected_annotation_items(self): """ Return the selected :class:`Annotation`'s """ return [item for item in self.__annotation_items if item.isSelected()] def node_links(self, node_item): """ Return all links from the `node_item` (:class:`NodeItem`). """ return self.node_output_links(node_item) + \ self.node_input_links(node_item) def node_output_links(self, node_item): """ Return a list of all output links from `node_item`. """ return [link for link in self.__link_items if link.sourceItem == node_item] def node_input_links(self, node_item): """ Return a list of all input links for `node_item`. """ return [link for link in self.__link_items if link.sinkItem == node_item] def neighbor_nodes(self, node_item): """ Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes. """ neighbors = list(map(attrgetter("sourceItem"), self.node_input_links(node_item))) neighbors.extend(map(attrgetter("sinkItem"), self.node_output_links(node_item))) return neighbors def on_widget_state_change(self, widget, state): pass def on_link_state_change(self, link, state): pass def on_scheme_change(self, ): pass def _on_position_change(self, item): # Invalidate the anchor point layout and schedule a layout. self.__anchor_layout.invalidateNode(item) self.node_item_position_changed.emit(item, item.pos()) def __on_node_pos_changed(self, pos): node = self.sender() item = self.__item_for_node[node] item.setPos(*pos) def __on_scheme_annot_geometry_change(self): annot = self.sender() item = self.__item_for_annotation[annot] if isinstance(annot, scheme.SchemeTextAnnotation): item.setGeometry(QRectF(*annot.rect)) elif isinstance(annot, scheme.SchemeArrowAnnotation): p1 = item.mapFromScene(QPointF(*annot.start_pos)) p2 = item.mapFromScene(QPointF(*annot.end_pos)) item.setLine(QLineF(p1, p2)) else: pass def item_at(self, pos, type_or_tuple=None, buttons=0): """Return the item at `pos` that is an instance of the specified type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given only return the item if it is the top level item that would accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`). """ rect = QRectF(pos, QSizeF(1, 1)) items = self.items(rect) if buttons: items = itertools.dropwhile( lambda item: not item.acceptedMouseButtons() & buttons, items ) items = list(items)[:1] if type_or_tuple: items = [i for i in items if isinstance(i, type_or_tuple)] return items[0] if items else None if PYQT_VERSION < 0x40900: # For QGraphicsObject subclasses items, itemAt ... return a # QGraphicsItem wrapper instance and not the actual class instance. def itemAt(self, *args, **kwargs): item = QGraphicsScene.itemAt(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def items(self, *args, **kwargs): items = QGraphicsScene.items(self, *args, **kwargs) return list(map(toGraphicsObjectIfPossible, items)) def selectedItems(self, *args, **kwargs): return list(map(toGraphicsObjectIfPossible, QGraphicsScene.selectedItems(self, *args, **kwargs))) def collidingItems(self, *args, **kwargs): return list(map(toGraphicsObjectIfPossible, QGraphicsScene.collidingItems(self, *args, **kwargs))) def focusItem(self, *args, **kwargs): item = QGraphicsScene.focusItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mouseGrabberItem(self, *args, **kwargs): item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mousePressEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mousePressEvent(event): return # Right (context) click on the node item. If the widget is not # in the current selection then select the widget (only the widget). # Else simply return and let customContextMenuReqested signal # handle it shape_item = self.item_at(event.scenePos(), items.NodeItem) if shape_item and event.button() == Qt.RightButton and \ shape_item.flags() & QGraphicsItem.ItemIsSelectable: if not shape_item.isSelected(): self.clearSelection() shape_item.setSelected(True) return QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseMoveEvent(event): return return QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseReleaseEvent(event): return return QGraphicsScene.mouseReleaseEvent(self, event) def mouseDoubleClickEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseDoubleClickEvent(event): return return QGraphicsScene.mouseDoubleClickEvent(self, event) def keyPressEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.keyPressEvent(event): return return QGraphicsScene.keyPressEvent(self, event) def keyReleaseEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.keyReleaseEvent(event): return return QGraphicsScene.keyReleaseEvent(self, event) def set_user_interaction_handler(self, handler): if self.user_interaction_handler and \ not self.user_interaction_handler.isFinished(): self.user_interaction_handler.cancel() log.info("Setting interaction '%s' to '%s'" % (handler, self)) self.user_interaction_handler = handler if handler: handler.start() def event(self, event): # TODO: change the base class of Node/LinkItem to QGraphicsWidget. # It already handles font changes. if event.type() == QEvent.FontChange: self.__update_font() return QGraphicsScene.event(self, event) def __update_font(self): font = self.font() for item in self.__node_items + self.__link_items: item.setFont(font) def __str__(self): return "%s(objectName=%r, ...)" % \ (type(self).__name__, str(self.objectName()))
class Settings(QObject, MutableMapping): """ A `dict` like interface to a QSettings store. """ __metaclass__ = QABCMeta valueChanged = Signal(unicode, object) valueAdded = Signal(unicode, object) keyRemoved = Signal(unicode) def __init__(self, parent=None, defaults=(), path=None, store=None): QObject.__init__(self, parent) if store is None: store = QSettings() path = path = (path or "").rstrip("/") self.__path = path self.__defaults = dict([(slot.key, slot) for slot in defaults]) self.__store = store def __key(self, key): """ Return the full key (including group path). """ if self.__path: return "/".join([self.__path, key]) else: return key def __delitem__(self, key): """ Delete the setting for key. If key is a group remove the whole group. .. note:: defaults cannot be deleted they are instead reverted to their original state. """ if key not in self: raise KeyError(key) if self.isgroup(key): group = self.group(key) for key in group: del group[key] else: fullkey = self.__key(key) oldValue = self.get(key) if self.__store.contains(fullkey): self.__store.remove(fullkey) newValue = None if fullkey in self.__defaults: newValue = self.__defaults[fullkey].default_value etype = SettingChangedEvent.SettingChanged else: etype = SettingChangedEvent.SettingRemoved QCoreApplication.sendEvent( self, SettingChangedEvent(etype, key, newValue, oldValue)) def __value(self, fullkey, value_type): typesafe = value_type is not None if value_type is None: value = toPyObject(self.__store.value(fullkey)) else: try: value = self.__store.value(fullkey, type=value_type) except TypeError: # In case the value was pickled in a type unsafe mode value = toPyObject(self.__store.value(fullkey)) typesafe = False if not typesafe: if isinstance(value, _pickledvalue): value = value.value else: log.warning( "value for key %r is not a '_pickledvalue' (%r)," "possible loss of type information.", fullkey, type(value)) return value def __setValue(self, fullkey, value, value_type=None): typesafe = value_type is not None if not typesafe: # value is stored in a _pickledvalue wrapper to force PyQt # to store it in a pickled format so we don't lose the type # TODO: Could check if QSettings.Format stores type info. value = _pickledvalue(value) self.__store.setValue(fullkey, value) def __getitem__(self, key): """ Get the setting for key. """ if key not in self: raise KeyError(key) if self.isgroup(key): raise KeyError("{0!r} is a group".format(key)) fullkey = self.__key(key) slot = self.__defaults.get(fullkey, None) if self.__store.contains(fullkey): value = self.__value(fullkey, slot.value_type if slot else None) else: value = slot.default_value return value def __setitem__(self, key, value): """ Set the setting for key. """ if not isinstance(key, basestring): raise TypeError(key) fullkey = self.__key(key) value_type = None if fullkey in self.__defaults: value_type = self.__defaults[fullkey].value_type if not isinstance(value, value_type): value = qt_to_mapped_type(value) if not isinstance(value, value_type): raise TypeError("Expected {0!r} got {1!r}".format( value_type.__name__, type(value).__name__)) if key in self: oldValue = self.get(key) etype = SettingChangedEvent.SettingChanged else: oldValue = None etype = SettingChangedEvent.SettingAdded self.__setValue(fullkey, value, value_type) QCoreApplication.sendEvent( self, SettingChangedEvent(etype, key, value, oldValue)) def __contains__(self, key): """ Return `True` if settings contain the `key`, False otherwise. """ fullkey = self.__key(key) return self.__store.contains(fullkey) or (fullkey in self.__defaults) def __iter__(self): """Return an iterator over over all keys. """ keys = map(unicode, self.__store.allKeys()) + \ self.__defaults.keys() if self.__path: path = self.__path + "/" keys = filter(lambda key: key.startswith(path), keys) keys = [key[len(path):] for key in keys] return iter(sorted(set(keys))) def __len__(self): return len(list(iter(self))) def group(self, path): if self.__path: path = "/".join([self.__path, path]) return Settings(self, self.__defaults.values(), path, self.__store) def isgroup(self, key): """ Is the `key` a settings group i.e. does it have subkeys. """ if key not in self: raise KeyError("{0!r} is not a valid key".format(key)) return len(self.group(key)) > 0 def isdefault(self, key): """ Is the value for key the default. """ if key not in self: raise KeyError(key) return not self.__store.contains(self.__key(key)) def clear(self): """ Clear the settings and restore the defaults. """ self.__store.clear() def add_default_slot(self, default): """ Add a default slot to the settings This also replaces any previously set value for the key. """ value = default.default_value oldValue = None etype = SettingChangedEvent.SettingAdded key = default.key if key in self: oldValue = self.get(key) etype = SettingChangedEvent.SettingChanged if not self.isdefault(key): # Replacing a default value. self.__store.remove(self.__key(key)) self.__defaults[key] = default event = SettingChangedEvent(etype, key, value, oldValue) QCoreApplication.sendEvent(self, event) def get_default_slot(self, key): return self.__defaults[self.__key(key)] def values(self): """ Return a list over of all values in the settings. """ return MutableMapping.values(self) def customEvent(self, event): QObject.customEvent(self, event) if isinstance(event, SettingChangedEvent): if event.type() == SettingChangedEvent.SettingChanged: self.valueChanged.emit(event.key(), event.value()) elif event.type() == SettingChangedEvent.SettingAdded: self.valueAdded.emit(event.key(), event.value()) elif event.type() == SettingChangedEvent.SettingRemoved: self.keyRemoved.emit(event.key()) parent = self.parent() if isinstance(parent, Settings): # Assumption that the parent is a parent setting group. parent.customEvent( SettingChangedEvent(event.type(), "/".join([self.__path, event.key()]), event.value(), event.oldValue()))
class OWMPR(OWWidget): name = 'ModelMap Projection Rank' description = 'Ranking projections by estimating projection quality' icon = "icons/ModelMap.svg" inputs = [('Data', Table, 'set_data', Default)] outputs = [('Features', AttributeList)] want_main_area = False settingsHandler = DomainContextHandler() variable_changed = Signal() def __init__(self): super().__init__() self.data = None self.progress = None self.infoa = gui.widgetLabel(self.controlArea, "No data loaded.") self.projectionTable = QTableView() self.controlArea.layout().addWidget(self.projectionTable) self.projectionTable.setSelectionBehavior(QTableView.SelectRows) self.projectionTable.setSelectionMode(QTableView.SingleSelection) self.projectionTable.setSortingEnabled(True) self.projectionTableModel = QStandardItemModel(self) self.projectionTableModel.setHorizontalHeaderLabels( ["P-Index", "", ""]) self.projectionTable.setModel(self.projectionTableModel) self.projectionTable.setColumnWidth(0, 90) self.projectionTable.sortByColumn(0, Qt.DescendingOrder) self.projectionTable.selectionModel().selectionChanged.connect( self.on_selection_changed) gui.button(self.controlArea, self, "Rank Projections", callback=self.rank, default=True) self.resize(370, 600) def set_data(self, data): self.data = data self.infoa.setText("Data set: {}".format(data.name) if self. data else "No data loaded.") def rank(self): if self.progress: return disc = Orange.preprocess.discretize.EqualWidth(n=10) ndomain = Orange.data.Domain([ disc(self.data, attr) if type(attr) == Orange.data.variable.ContinuousVariable else attr for attr in self.data.domain.attributes ], self.data.domain.class_vars) t = self.data.from_table(ndomain, self.data) attrs = t.domain.attributes tables = {} l = 0 self.progress = gui.ProgressBar(self, len(attrs) * (len(attrs) - 1) / 2) for i in range(len(attrs)): for j in range(i): ct = np.array( contingency.get_contingency(t, attrs[j], attrs[i])) pindex, _, _ = p_index(ct) tables[i, j] = ct item = QStandardItem() item.setData(float(pindex), Qt.DisplayRole) self.projectionTableModel.setItem(l, 0, item) item = QStandardItem() item.setData(attrs[i].name, Qt.DisplayRole) self.projectionTableModel.setItem(l, 1, item) item = QStandardItem() item.setData(attrs[j].name, Qt.DisplayRole) self.projectionTableModel.setItem(l, 2, item) self.progress.advance() l += 1 self.progress.finish() self.progress = None def on_selection_changed(self, selected, deselected): """Called when the ranks view selection changes.""" a1 = selected.indexes()[1].data().replace('D_', '') a2 = selected.indexes()[2].data().replace('D_', '') d = self.data.domain self.send("Features", AttributeList([d[a1], d[a2]]))
class GuiBisector(QObject, Bisector): started = Signal() finished = Signal(object, int) choose_next_build = Signal() step_started = Signal(object) step_build_found = Signal(object, object) step_testing = Signal(object, object) step_finished = Signal(object, str) handle_merge = Signal(object, str, str, str) def __init__(self, fetch_config, test_runner, download_manager, download_in_background=True): QObject.__init__(self) Bisector.__init__(self, fetch_config, test_runner, download_manager) self.bisection = None self.mid = None self.build_infos = None self._bisect_args = None self.error = None self._next_build_index = None self.download_in_background = download_in_background self.index_promise = None self._persist_files = () self.should_stop = threading.Event() self.download_manager.download_finished.connect( self._build_dl_finished) self.test_runner.evaluate_finished.connect(self._evaluate_finished) def _finish_on_exception(self, bisection): self.error = sys.exc_info() self.finished.emit(bisection, Bisection.EXCEPTION) @Slot() def bisect(self): # this is a slot so it will be called in the thread self.started.emit() try: Bisector.bisect(self, *self._bisect_args) except MozRegressionError: self._finish_on_exception(None) @Slot() def bisect_further(self): assert self.bisection self.started.emit() handler = self.bisection.handler try: nhandler = InboundHandler(find_fix=self.bisection.handler.find_fix) Bisector.bisect(self, nhandler, handler.good_revision, handler.bad_revision, expand=DEFAULT_EXPAND, interrupt=self.should_stop.is_set) except MozRegressionError: self._finish_on_exception(None) except StopIteration: self.finished.emit(None, Bisection.USER_EXIT) @Slot() def check_merge(self): handler = self.bisection.handler try: result = handler.handle_merge() except MozRegressionError: self._finish_on_exception(None) return if result is None: self.bisection.no_more_merge = True self.finished.emit(self.bisection, Bisection.FINISHED) else: self.handle_merge.emit(self.bisection, *result) def _bisect(self, handler, build_range): self.bisection = Bisection(handler, build_range, self.download_manager, self.test_runner, dl_in_background=False, approx_chooser=self.approx_chooser) self._bisect_next() @Slot() def _bisect_next(self): # this is executed in the working thread if self.test_runner.verdict != 'r': try: self.mid = self.bisection.search_mid_point( interrupt=self.should_stop.is_set) except MozRegressionError: self._finish_on_exception(self.bisection) return except StopIteration: return # if our last answer was skip, and that the next build # to use is not chosen yet, ask to choose it. if (self._next_build_index is None and self.test_runner.verdict == 's' and len(self.bisection.build_range) > 3): self.choose_next_build.emit() return if self._next_build_index is not None: # here user asked for specific build (eg from choose_next_build) self.mid = self._next_build_index # this will download build infos if required if self.bisection.build_range[self.mid] is False: # in case no build info is found, ask to choose again self.choose_next_build.emit() return self._next_build_index = None self.step_started.emit(self.bisection) result = self.bisection.init_handler(self.mid) if result != Bisection.RUNNING: self.finished.emit(self.bisection, result) else: self.build_infos = self.bisection.handler.build_range[self.mid] found, self.mid, self.build_infos, self._persist_files = \ self.bisection._find_approx_build(self.mid, self.build_infos) if not found: self.download_manager.focus_download(self.build_infos) self.step_build_found.emit(self.bisection, self.build_infos) if found: # to continue the bisection, act as if it was downloaded self._build_dl_finished(None, self.build_infos.build_file) @Slot() def _evaluate(self): # this is called in the working thread, so installation does not # block the ui. # download in background, if desired and that last verdict was not # a skip. if self.download_in_background and self.test_runner.verdict != 's': self.index_promise = IndexPromise( self.mid, self.bisection._download_next_builds, args=(self._persist_files, )) # run the build evaluation self.bisection.evaluate(self.build_infos) # wait for the next index in the thread if any if self.index_promise: self.index_promise() # if there was an error, stop the possible downloads if self.test_runner.run_error: self.download_manager.cancel() self.download_manager.wait(raise_if_error=False) if not self.test_runner.run_error: self.step_testing.emit(self.bisection, self.build_infos) @Slot(object, str) def _build_dl_finished(self, dl, dest): # here we are not in the working thread, since the connection was # done in the constructor if not dest == self.build_infos.build_file: return if dl is not None and (dl.is_canceled() or dl.error()): # todo handle this return # call this in the thread QTimer.singleShot(0, self._evaluate) @Slot() def _evaluate_finished(self): # here we are not in the working thread, since the connection was # done in the constructor if self.index_promise: self.mid = self.index_promise() self.index_promise = None self.step_finished.emit(self.bisection, self.test_runner.verdict) result = self.bisection.handle_verdict(self.mid, self.test_runner.verdict) if result != Bisection.RUNNING: self.finished.emit(self.bisection, result) else: # call this in the thread QTimer.singleShot(0, self._bisect_next)