class MicroViewWidget(mvBase): __clsName = "MicroViewWidget" def tr(self, string): return QCoreApplication.translate(self.__clsName, string) def __init__(self, parent=None): super().__init__(parent) # Go before setupUi for QMetaObject.connectSlotsByName to work self._scene = MicroViewScene(self) self._scene.setObjectName("scene") self._ui = mvClass() self._ui.setupUi(self) self._ui.view.setScene(self._scene) # Apparently this is necessary with Qt5, as otherwise updating fails # on image change; there are white rectangles on the updated area # until the mouse is moved in or out of the view self._ui.view.setViewportUpdateMode( QGraphicsView.BoundingRectViewportUpdate) self._ui.view.setRenderHints(QPainter.Antialiasing) self._scene.imageItem.signals.mouseMoved.connect( self._updateCurrentPixelInfo) self._imageData = np.array([]) self._intensityMin = None self._intensityMax = None self._sliderFactor = 1 self._ui.autoButton.pressed.connect(self.autoIntensity) self._playing = False self._playTimer = QTimer() if not (qtpy.PYQT4 or qtpy.PYSIDE): self._playTimer.setTimerType(Qt.PreciseTimer) self._playTimer.setSingleShot(False) # set up preview button self._locEnabledStr = "Localizations are shown" self._locDisabledStr = "Localizations are not shown" self._ui.locButton.setToolTip(self.tr(self._locEnabledStr)) self._ui.locButton.toggled.connect(self.showLocalizationsChanged) # connect signals and slots self._ui.framenoBox.valueChanged.connect(self.selectFrame) self._ui.playButton.pressed.connect( lambda: self.setPlaying(not self._playing)) self._playTimer.timeout.connect(self.nextFrame) self._ui.zoomInButton.pressed.connect(self.zoomIn) self._ui.zoomOriginalButton.pressed.connect(self.zoomOriginal) self._ui.zoomOutButton.pressed.connect(self.zoomOut) self._ui.zoomFitButton.pressed.connect(self.zoomFit) self._scene.roiChanged.connect(self.roiChanged) # set button icons self._ui.locButton.setIcon(QIcon.fromTheme("view-preview")) self._ui.zoomOutButton.setIcon(QIcon.fromTheme("zoom-out")) self._ui.zoomOriginalButton.setIcon(QIcon.fromTheme("zoom-original")) self._ui.zoomFitButton.setIcon(QIcon.fromTheme("zoom-fit-best")) self._ui.zoomInButton.setIcon(QIcon.fromTheme("zoom-in")) self._ui.roiButton.setIcon(QIcon.fromTheme("draw-polygon")) self._playIcon = QIcon.fromTheme("media-playback-start") self._pauseIcon = QIcon.fromTheme("media-playback-pause") self._ui.playButton.setIcon(self._playIcon) # these are to be setEnable(False)'ed if there is no image sequence self._noImsDisable = [ self._ui.zoomOutButton, self._ui.zoomOriginalButton, self._ui.zoomFitButton, self._ui.zoomInButton, self._ui.roiButton, self._ui.view, self._ui.pixelInfo, self._ui.frameSelector, self._ui.contrastGroup ] # initialize image data self._locDataGood = None self._locDataBad = None self._locMarkers = None self.setImageSequence(None) def setRoi(self, roi): self._scene.roi = roi roiChanged = Signal(QPolygonF) @Property(QPolygonF, fset=setRoi, doc="Polygon describing the region of interest (ROI)") def roi(self): return self._scene.roi def setImageSequence(self, ims): self._locDataGood = None self._locDataBad = None if ims is None: self._ims = None self._imageData = None for w in self._noImsDisable: w.setEnabled(False) self.drawImage() self.drawLocalizations() return for w in self._noImsDisable: w.setEnabled(True) self._ui.framenoBox.setMaximum(len(ims)) self._ui.framenoSlider.setMaximum(len(ims)) self._ims = ims try: self._imageData = self._ims[self._ui.framenoBox.value() - 1] except Exception: self.frameReadError.emit(self._ui.framenoBox.value() - 1) return if np.issubdtype(self._imageData.dtype, np.floating): # ugly hack; get min and max corresponding to integer types based # on the range of values in the first image min = self._imageData.min() if min < 0: types = (np.int8, np.int16, np.int32, np.int64) else: types = (np.uint8, np.uint16, np.uint32, np.uint64) max = self._imageData.max() if min >= 0. and max <= 1.: min = 0 max = 1 else: for t in types: ii = np.iinfo(t) if min >= ii.min and max <= ii.max: min = ii.min max = ii.max break else: min = np.iinfo(ims.pixel_type).min max = np.iinfo(ims.pixel_type).max if min == 0. and max == 1.: self._ui.minSlider.setRange(0, 1000) self._ui.maxSlider.setRange(0, 1000) self._ui.minSpinBox.setDecimals(3) self._ui.minSpinBox.setRange(0, 1) self._ui.maxSpinBox.setDecimals(3) self._ui.maxSpinBox.setRange(0, 1) self._sliderFactor = 1000 else: self._ui.minSlider.setRange(min, max) self._ui.maxSlider.setRange(min, max) self._ui.minSpinBox.setDecimals(0) self._ui.minSpinBox.setRange(min, max) self._ui.maxSpinBox.setDecimals(0) self._ui.maxSpinBox.setRange(min, max) self._sliderFactor = 1 if (self._intensityMin is None) or (self._intensityMax is None): self.autoIntensity() else: self.drawImage() self._scene.setSceneRect(self._scene.itemsBoundingRect()) self.currentFrameChanged.emit() @Slot(int) def on_minSlider_valueChanged(self, val): self._ui.minSpinBox.setValue(float(val) / self._sliderFactor) @Slot(int) def on_maxSlider_valueChanged(self, val): self._ui.maxSpinBox.setValue(float(val) / self._sliderFactor) @Slot(float) def on_minSpinBox_valueChanged(self, val): self._ui.minSlider.setValue(round(val * self._sliderFactor)) self.setMinIntensity(val) @Slot(float) def on_maxSpinBox_valueChanged(self, val): self._ui.maxSlider.setValue(round(val * self._sliderFactor)) self.setMaxIntensity(val) @Slot(pd.DataFrame) def setLocalizationData(self, good, bad): self._locDataGood = good self._locDataBad = bad self.drawLocalizations() def setPlaying(self, play): if self._ims is None: return if play == self._playing: return if play: self._playTimer.setInterval(1000 / self._ui.fpsBox.value()) self._playTimer.start() else: self._playTimer.stop() self._ui.fpsBox.setEnabled(not play) self._ui.framenoBox.setEnabled(not play) self._ui.framenoSlider.setEnabled(not play) self._ui.framenoLabel.setEnabled(not play) self._ui.playButton.setIcon( self._pauseIcon if play else self._playIcon) self._playing = play def drawImage(self): if self._imageData is None: self._scene.setImage(QPixmap()) return img_buf = self._imageData.astype(np.float) if (self._intensityMin is None) or (self._intensityMax is None): self._intensityMin = np.min(img_buf) self._intensityMax = np.max(img_buf) img_buf -= float(self._intensityMin) img_buf *= 255. / float(self._intensityMax - self._intensityMin) np.clip(img_buf, 0., 255., img_buf) # convert grayscale to RGB 32bit # far faster than calling img_buf.astype(np.uint8).repeat(4) qi = np.empty((img_buf.shape[0], img_buf.shape[1], 4), dtype=np.uint8) qi[:, :, 0] = qi[:, :, 1] = qi[:, :, 2] = qi[:, :, 3] = img_buf # prevent QImage from being garbage collected self._qImg = QImage(qi, self._imageData.shape[1], self._imageData.shape[0], QImage.Format_RGB32) self._scene.setImage(self._qImg) def drawLocalizations(self): if isinstance(self._locMarkers, QGraphicsItem): self._scene.removeItem(self._locMarkers) self._locMarkers = None if not self.showLocalizations: return try: sel = self._locDataGood["frame"] == self._ui.framenoBox.value() - 1 dGood = self._locDataGood[sel] except Exception: return try: sel = self._locDataBad["frame"] == self._ui.framenoBox.value() - 1 dBad = self._locDataBad[sel] except Exception: pass markerList = [] for n, d in dBad.iterrows(): markerList.append(LocalizationMarker(d, Qt.red)) for n, d in dGood.iterrows(): markerList.append(LocalizationMarker(d, Qt.green)) self._locMarkers = self._scene.createItemGroup(markerList) @Slot() def autoIntensity(self): if self._imageData is None: return self._intensityMin = np.min(self._imageData) self._intensityMax = np.max(self._imageData) if self._intensityMin == self._intensityMax: if self._intensityMax == 0: self._intensityMax = 1 else: self._intensityMin = self._intensityMax - 1 self._ui.minSlider.setValue(self._intensityMin) self._ui.maxSlider.setValue(self._intensityMax) self.drawImage() @Slot(int) def setMinIntensity(self, v): self._intensityMin = min(v, self._intensityMax - 1) self._ui.minSlider.setValue(self._intensityMin) self.drawImage() @Slot(int) def setMaxIntensity(self, v): self._intensityMax = max(v, self._intensityMin + 1) self._ui.maxSlider.setValue(self._intensityMax) self.drawImage() currentFrameChanged = Signal() @Slot(int) def selectFrame(self, frameno): if self._ims is None: return try: self._imageData = self._ims[frameno - 1] except Exception: self.frameReadError.emit(frameno - 1) self.currentFrameChanged.emit() self.drawImage() self.drawLocalizations() frameReadError = Signal(int) @Slot() def nextFrame(self): if self._ims is None: return next = self._ui.framenoBox.value() + 1 if next > self._ui.framenoBox.maximum(): next = 1 self._ui.framenoBox.setValue(next) @Slot() def zoomIn(self): self._ui.view.scale(1.5, 1.5) @Slot() def zoomOut(self): self._ui.view.scale(2. / 3., 2. / 3.) @Slot() def zoomOriginal(self): self._ui.view.setTransform(QTransform()) @Slot() def zoomFit(self): self._ui.view.fitInView(self._scene.imageItem, Qt.KeepAspectRatio) def getCurrentFrame(self): return self._imageData @Property(int, doc="Number of the currently displayed frame") def currentFrameNumber(self): return self._ui.framenoBox.value() - 1 def _updateCurrentPixelInfo(self, x, y): if x >= self._imageData.shape[1] or y >= self._imageData.shape[0]: # Sometimes, when hitting the border of the image, the coordinates # are out of range return self._ui.posLabel.setText("({x}, {y})".format(x=x, y=y)) self._ui.intLabel.setText(locale.str(self._imageData[y, x])) @Slot(bool) def on_roiButton_toggled(self, checked): self._scene.roiMode = checked @Slot(bool) def on_scene_roiModeChanged(self, enabled): self._ui.roiButton.setChecked(enabled) def setShowLocalizations(self, show): self._ui.locButton.setChecked(show) showLocalizationsChanged = Signal(bool) @Property(bool, fset=setShowLocalizations, notify=showLocalizationsChanged) def showLocalizations(self): return self._ui.locButton.isChecked() def on_locButton_toggled(self, checked): tooltip = (self.tr(self._locEnabledStr) if checked else self.tr(self._locDisabledStr)) self._ui.locButton.setToolTip(tooltip) self.drawLocalizations()
class PyDMEmbeddedDisplay(QFrame, PyDMPrimitiveWidget): """ A QFrame capable of rendering a PyDM Display Parameters ---------- parent : QWidget The parent widget for the Label """ def __init__(self, parent=None): QFrame.__init__(self, parent) PyDMPrimitiveWidget.__init__(self) self.app = QApplication.instance() self._filename = None self._macros = None self._embedded_widget = None self._disconnect_when_hidden = True self._is_connected = False self._only_load_when_shown = True self._needs_load = True self._load_error_timer = None self._load_error = None self.layout = QVBoxLayout(self) self.err_label = QLabel(self) self.err_label.setAlignment(Qt.AlignHCenter) self.layout.addWidget(self.err_label) self.layout.setContentsMargins(0, 0, 0, 0) self.err_label.hide() if not is_pydm_app(): self.setFrameShape(QFrame.Box) else: self.setFrameShape(QFrame.NoFrame) def minimumSizeHint(self): """ This property holds the recommended minimum size for the widget. Returns ------- QSize """ # This is totally arbitrary, I just want *some* visible nonzero size return QSize(100, 100) @Property(str) def macros(self): """ JSON-formatted string containing macro variables to pass to the embedded file. Returns ------- str """ if self._macros is None: return "" return self._macros @macros.setter def macros(self, new_macros): """ JSON-formatted string containing macro variables to pass to the embedded file. .. warning:: If the macros property is not defined before the filename property, The widget will not have any macros defined when it loads the embedded file. This behavior will be fixed soon. Parameters ---------- new_macros : str """ new_macros = str(new_macros) if new_macros != self._macros: self._macros = new_macros self._needs_load = True self.load_if_needed() @Property(str) def filename(self): """ Filename of the display to embed. Returns ------- str """ if self._filename is None: return "" return self._filename @filename.setter def filename(self, filename): """ Filename of the display to embed. Parameters ---------- filename : str """ filename = str(filename) if filename != self._filename: self._filename = filename self._needs_load = True if is_qt_designer(): if self._load_error_timer: # Kill the timer here. If new filename still causes the problem, it will be restarted self._load_error_timer.stop() self._load_error_timer = None self.clear_error_text() self.load_if_needed() def parsed_macros(self): """ Dictionary containing the key value pair for each macro specified. Returns -------- dict """ parent_display = self.find_parent_display() parent_macros = {} if parent_display: parent_macros = copy.copy(parent_display.macros()) widget_macros = macro.parse_macro_string(self.macros) parent_macros.update(widget_macros) return parent_macros def load_if_needed(self): if self._needs_load and (not self._only_load_when_shown or self.isVisible() or is_qt_designer()): self.embedded_widget = self.open_file() def open_file(self, force=False): """ Opens the widget specified in the widget's filename property. Returns ------- display : QWidget """ if (not force) and (not self._needs_load): return if not self.filename: return try: parent_display = self.find_parent_display() base_path = "" if parent_display: base_path = os.path.dirname(parent_display.loaded_file()) fname = find_file(self.filename, base_path=base_path) w = load_file(fname, macros=self.parsed_macros(), target=None) self._needs_load = False self.clear_error_text() return w except Exception as e: self._load_error = e if self._load_error_timer: self._load_error_timer.stop() self._load_error_timer = QTimer(self) self._load_error_timer.setSingleShot(True) self._load_error_timer.setTimerType(Qt.VeryCoarseTimer) self._load_error_timer.timeout.connect( self._display_designer_load_error) self._load_error_timer.start(1000) return None def clear_error_text(self): if self._load_error_timer: self._load_error_timer.stop() self.err_label.clear() self.err_label.hide() def display_error_text(self, e): self.err_label.setText( "Could not open {filename}.\nError: {err}".format( filename=self._filename, err=e)) self.err_label.show() @property def embedded_widget(self): """ The embedded widget being displayed. Returns ------- QWidget """ return self._embedded_widget @embedded_widget.setter def embedded_widget(self, new_widget): """ Defines the embedded widget to display inside the QFrame Parameters ---------- new_widget : QWidget """ should_reconnect = False if new_widget is self._embedded_widget: return if self._embedded_widget is not None: self.layout.removeWidget(self._embedded_widget) self._embedded_widget.deleteLater() self._embedded_widget = None if new_widget is not None: self._embedded_widget = new_widget self._embedded_widget.setParent(self) self.layout.addWidget(self._embedded_widget) self.err_label.hide() self._embedded_widget.show() self._is_connected = True def connect(self): """ Establish the connection between the embedded widget and the channels associated with it. """ if self._is_connected or self.embedded_widget is None: return establish_widget_connections(self.embedded_widget) self._is_connected = True def disconnect(self): """ Disconnects the embedded widget from the channels associated with it. """ if not self._is_connected or self.embedded_widget is None: return close_widget_connections(self.embedded_widget) self._is_connected = False @Property(bool) def loadWhenShown(self): """ If True, only load and display the file once the PyDMEmbeddedDisplayWidget is visible on screen. This is very useful if you have many different PyDMEmbeddedWidgets in different tabs of a QTabBar or PyDMTabBar: only the tab that the user is looking at will be loaded, which can greatly speed up the launch time of a display. If this property is changed from 'True' to 'False', and the file has not been loaded yet, it will be loaded immediately. Returns ------- bool """ return self._only_load_when_shown @loadWhenShown.setter def loadWhenShown(self, val): self._only_load_when_shown = val self.load_if_needed() @Property(bool) def disconnectWhenHidden(self): """ Disconnect from PVs when this widget is not visible. Returns ------- bool """ return self._disconnect_when_hidden @disconnectWhenHidden.setter def disconnectWhenHidden(self, disconnect_when_hidden): """ Disconnect from PVs when this widget is not visible. Parameters ---------- disconnect_when_hidden : bool """ self._disconnect_when_hidden = disconnect_when_hidden def showEvent(self, e): """ Show events are sent to widgets that become visible on the screen. Parameters ---------- event : QShowEvent """ if self._only_load_when_shown: w = self.open_file() if w: self.embedded_widget = w if self.disconnectWhenHidden: self.connect() def hideEvent(self, e): """ Hide events are sent to widgets that become invisible on the screen. Parameters ---------- event : QHideEvent """ if self.disconnectWhenHidden: self.disconnect() def _display_designer_load_error(self): self._load_error_timer = None logger.exception("Exception while opening embedded display file.", exc_info=self._load_error) if self._load_error: self.display_error_text(self._load_error)
class FilterWidget(filterBase): __clsName = "LocFilter" filterChangeDelay = 200 varNameRex = re.compile(r"\{(\w*)\}") def tr(self, string): return QCoreApplication.translate(self.__clsName, string) def __init__(self, parent=None): super().__init__(parent) self._ui = filterClass() self._ui.setupUi(self) self._delayTimer = QTimer(self) self._delayTimer.setInterval(self.filterChangeDelay) self._delayTimer.setSingleShot(True) if not (qtpy.PYQT4 or qtpy.PYSIDE): self._delayTimer.setTimerType(Qt.PreciseTimer) self._delayTimer.timeout.connect(self.filterChanged) self._ui.filterEdit.textChanged.connect(self._delayTimer.start) self._menu = QMenu() self._menu.triggered.connect(self._addVariable) filterChanged = Signal() @Slot(list) def setVariables(self, var): self._menu.clear() for v in var: self._menu.addAction(v) def setFilterString(self, filt): self._ui.filterEdit.setPlainText(filt) @Property(str, fset=setFilterString, doc="String describing the filter") def filterString(self): s = self._ui.filterEdit.toPlainText() return self.varNameRex.subn("\\1", s)[0] def getFilter(self): filterStr = self.filterString filterStrList = filterStr.split("\n") def filterFunc(data): filter = np.ones(len(data), dtype=bool) for f in filterStrList: with suppress(Exception): filter &= data.eval(f, local_dict={}, global_dict={}) return filter return filterFunc @Slot(QAction) def _addVariable(self, act): self._ui.filterEdit.textCursor().insertText(act.text()) @Slot(str) def on_showVarLabel_linkActivated(self, link): if not self._menu.isEmpty(): self._menu.exec(QCursor.pos())