def _wait_reply(self, call_id, call_name, timeout): """Wait for the other side reply.""" if call_id in self._reply_inbox: return # Create event loop to wait with wait_loop = QEventLoop() self._sig_got_reply.connect(wait_loop.quit) wait_timeout = QTimer() wait_timeout.setSingleShot(True) wait_timeout.timeout.connect(wait_loop.quit) # Wait until the kernel returns the value wait_timeout.start(timeout * 1000) while len(self._reply_waitlist) > 0: if not wait_timeout.isActive(): self._sig_got_reply.disconnect(wait_loop.quit) if call_id in self._reply_waitlist: raise TimeoutError("Timeout while waiting for {}".format( self._reply_waitlist)) return wait_loop.exec_() wait_timeout.stop() self._sig_got_reply.disconnect(wait_loop.quit)
class Clock_Node(NodeBase): title = 'clock' init_inputs = [ NodeInputBP(dtype=dtypes.Float(default=0.1), label='delay'), NodeInputBP(dtype=dtypes.Integer(default=-1, bounds=(-1, 1000)), label='iterations'), ] init_outputs = [NodeOutputBP('exec')] color = '#5d95de' main_widget_class = widgets.ClockNode_MainWidget main_widget_pos = 'below ports' def __init__(self, params): super().__init__(params) self.actions['start'] = {'method': self.start} self.actions['stop'] = {'method': self.stop} if self.session.gui: from qtpy.QtCore import QTimer self.timer = QTimer(self) self.timer.timeout.connect(self.timeouted) self.iteration = 0 def timeouted(self): self.exec_output(0) self.iteration += 1 if -1 < self.input(1) <= self.iteration: self.stop() def start(self): if self.session.gui: self.timer.setInterval(self.input(0) * 1000) self.timer.start() else: import time for i in range(self.input(1)): self.exec_output(0) time.sleep(self.input(0)) def stop(self): self.iteration = 0 if self.session.gui: self.timer.stop() def toggle(self): # triggered from main widget if self.session.gui: if self.timer.isActive(): self.stop() else: self.start() def update_event(self, inp=-1): if self.session.gui: self.timer.setInterval(self.input(0) * 1000) def remove_event(self): self.stop()
def _wait(self, condition, signal, timeout_msg, timeout): """ Wait until condition() is True by running an event loop. signal: qt signal that should interrupt the event loop. timeout_msg: Message to display in case of a timeout. timeout: time in seconds before a timeout """ if condition(): return # Create event loop to wait with wait_loop = QEventLoop() signal.connect(wait_loop.quit) wait_timeout = QTimer() wait_timeout.setSingleShot(True) wait_timeout.timeout.connect(wait_loop.quit) # Wait until the kernel returns the value wait_timeout.start(timeout * 1000) while not condition(): if not wait_timeout.isActive(): signal.disconnect(wait_loop.quit) if not condition(): raise TimeoutError(timeout_msg) return wait_loop.exec_() wait_timeout.stop() signal.disconnect(wait_loop.quit)
class SignalThrottler(QObject): def __init__(self, interval): QObject.__init__(self) self.timer = QTimer() self.timer.setInterval(interval) self.hasPendingEmission = False self.timer.timeout.connect(self.maybeEmitTriggered) def maybeEmitTriggered(self): if self.hasPendingEmission: self.emit_triggered() @Slot() def throttle(self): self.hasPendingEmission = True if not self.timer.isActive(): self.timer.start() def emit_triggered(self): self.hasPendingEmission = False self.triggered.emit() triggered = Signal() timeoutChanged = Signal(int) timerTypeChanged = Signal(Qt.TimerType)
class MatplotlibDataViewer(MatplotlibViewerMixin, DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self.axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs) MatplotlibViewerMixin.setup_callbacks(self) self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop()
class BufferedItemModel(QStandardItemModel): __row_appended = Signal() def __init__(self, parent=None, limit=None, refresh=20): super().__init__(parent) self.limit = limit self.buffer = list() self.timer = QTimer() self.timer.setSingleShot(True) self.timer.setInterval(refresh) @Slot() @helpers.connect_slot(self.__row_appended) def __on_row_appended(): if not self.timer.isActive(): self.timer.start() @Slot() @helpers.connect_slot(self.timer.timeout) def __on_timer_timeout(): self.__dump_buffer() def __dump_buffer(self): self.insertRows(self.rowCount(), len( self.buffer)) # Append rows for each item in the buffer # Set the items for each new row for offset, item in enumerate(self.buffer): self.setItem(self.rowCount() - len(self.buffer) + offset, 0, item) self.buffer.clear() # Reset the buffer def __apply_limit(self): if self.rowCount() > self.limit: # Remove rows from the beginning, count being the number of rows over the limit self.removeRows(0, self.rowCount() - self.limit) def insertRows(self, row, count, _=None): super().insertRows(row, count) if self.limit: self.__apply_limit() def appendRow(self, item): # Append the QStandardItem to the internal list to be popped into the model on the next timeout self.buffer.append(item) self.__row_appended.emit()
class ClickableSvgItem(SvgItem): def __init__(self, id, renderer, signal: NodeSignal, node_name: str, parent=None): super().__init__(id, renderer, parent) self.__signal = signal self.__node_name = node_name self.__timer = QTimer(self) self.__timer.setSingleShot(True) self.__timer.timeout.connect(self.__on_single_click) self.__double_click_interval = QApplication.doubleClickInterval() self.setAcceptHoverEvents(True) def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: event.accept() def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): event.accept() if not self.__timer.isActive(): self.__timer.start(self.__double_click_interval) else: self.__timer.stop() self.__on_double_click() def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: event.accept() self.__timer.stop() self.__on_double_click() def __on_single_click(self): self.__signal.on_click.emit(self.__node_name) def __on_double_click(self): self.__signal.on_double_click.emit(self.__node_name) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): self.__signal.on_context.emit(self.__node_name, event.screenPos()) def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): self.setToolTip(self.__node_name) def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): self.setToolTip('')
def _wait(self, condition, signal, timeout_msg, timeout): """ Wait until condition() is True by running an event loop. signal: qt signal that should interrupt the event loop. timeout_msg: Message to display in case of a timeout. timeout: time in seconds before a timeout """ # Exit if condition is fulfilled or the kernel is dead. if condition(): return if not self.kernel_client.is_alive(): raise RuntimeError("Kernel is dead") # Create event loop to wait with wait_loop = QEventLoop() wait_timeout = QTimer() wait_timeout.setSingleShot(True) # Connect signals to stop kernel loop wait_timeout.timeout.connect(wait_loop.quit) self.kernel_client.hb_channel.kernel_died.connect(wait_loop.quit) signal.connect(wait_loop.quit) # Wait until the kernel returns the value wait_timeout.start(timeout * 1000) while not condition(): if not wait_timeout.isActive(): signal.disconnect(wait_loop.quit) self.kernel_client.hb_channel.kernel_died.disconnect( wait_loop.quit) if condition(): return if not self.kernel_client.is_alive(): raise RuntimeError("Kernel is dead") raise TimeoutError(timeout_msg) wait_loop.exec_() wait_timeout.stop() signal.disconnect(wait_loop.quit) self.kernel_client.hb_channel.kernel_died.disconnect( wait_loop.quit)
class BasePlot(PlotWidget, PyDMPrimitiveWidget): crosshair_position_updated = Signal(float, float) def __init__(self, parent=None, background='default', axisItems=None): PlotWidget.__init__(self, parent=parent, background=background, axisItems=axisItems) PyDMPrimitiveWidget.__init__(self) self.plotItem = self.getPlotItem() self.plotItem.hideButtons() self._auto_range_x = None self.setAutoRangeX(True) self._auto_range_y = None self.setAutoRangeY(True) self._min_x = 0.0 self._max_x = 1.0 self._min_y = 0.0 self._max_y = 1.0 self._show_x_grid = None self.setShowXGrid(False) self._show_y_grid = None self.setShowYGrid(False) self._show_right_axis = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawPlot) self._redraw_rate = 30 # Redraw at 30 Hz by default. self.maxRedrawRate = self._redraw_rate self._curves = [] self._title = None self._show_legend = False self._legend = self.addLegend() self._legend.hide() # Drawing crosshair on the ViewBox self.vertical_crosshair_line = None self.horizontal_crosshair_line = None self.crosshair_movement_proxy = None def addCurve(self, plot_item, curve_color=None): if curve_color is None: curve_color = utilities.colors.default_colors[ len(self._curves) % len(utilities.colors.default_colors)] plot_item.color_string = curve_color self._curves.append(plot_item) self.addItem(plot_item) self.redraw_timer.start() # Connect channels for chan in plot_item.channels(): if chan: chan.connect() # self._legend.addItem(plot_item, plot_item.curve_name) def removeCurve(self, plot_item): self.removeItem(plot_item) self._curves.remove(plot_item) if len(self._curves) < 1: self.redraw_timer.stop() # Disconnect channels for chan in plot_item.channels(): if chan: chan.disconnect() def removeCurveWithName(self, name): for curve in self._curves: if curve.name() == name: self.removeCurve(curve) def removeCurveAtIndex(self, index): curve_to_remove = self._curves[index] self.removeCurve(curve_to_remove) def setCurveAtIndex(self, index, new_curve): old_curve = self._curves[index] self._curves[index] = new_curve # self._legend.addItem(new_curve, new_curve.name()) self.removeCurve(old_curve) def curveAtIndex(self, index): return self._curves[index] def curves(self): return self._curves def clear(self): legend_items = [label.text for (sample, label) in self._legend.items] for item in legend_items: self._legend.removeItem(item) self.plotItem.clear() self._curves = [] @Slot() def redrawPlot(self): pass def getShowXGrid(self): return self._show_x_grid def setShowXGrid(self, value, alpha=None): self._show_x_grid = value self.showGrid(x=self._show_x_grid, alpha=alpha) def resetShowXGrid(self): self.setShowXGrid(False) showXGrid = Property("bool", getShowXGrid, setShowXGrid, resetShowXGrid) def getShowYGrid(self): return self._show_y_grid def setShowYGrid(self, value, alpha=None): self._show_y_grid = value self.showGrid(y=self._show_y_grid, alpha=alpha) def resetShowYGrid(self): self.setShowYGrid(False) showYGrid = Property("bool", getShowYGrid, setShowYGrid, resetShowYGrid) def getBackgroundColor(self): return self.backgroundBrush().color() def setBackgroundColor(self, color): if self.backgroundBrush().color() != color: self.setBackgroundBrush(QBrush(color)) backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor) def getAxisColor(self): return self.getAxis('bottom')._pen.color() def setAxisColor(self, color): if self.getAxis('bottom')._pen.color() != color: self.getAxis('bottom').setPen(color) self.getAxis('left').setPen(color) self.getAxis('top').setPen(color) self.getAxis('right').setPen(color) axisColor = Property(QColor, getAxisColor, setAxisColor) def getBottomAxisLabel(self): return self.getAxis('bottom').labelText def getShowRightAxis(self): """ Provide whether the right y-axis is being shown. Returns : bool ------- True if the graph shows the right y-axis. False if not. """ return self._show_right_axis def setShowRightAxis(self, show): """ Set whether the graph should show the right y-axis. Parameters ---------- show : bool True for showing the right axis; False is for not showing. """ if show: self.showAxis("right") else: self.hideAxis("right") self._show_right_axis = show showRightAxis = Property("bool", getShowRightAxis, setShowRightAxis) def getPlotTitle(self): if self._title is None: return "" return str(self._title) def setPlotTitle(self, value): self._title = str(value) if len(self._title) < 1: self._title = None self.setTitle(self._title) def resetPlotTitle(self): self._title = None self.setTitle(self._title) title = Property(str, getPlotTitle, setPlotTitle, resetPlotTitle) def getShowLegend(self): """ Check if the legend is being shown. Returns : bool ------- True if the legend is displayed on the graph; False if not. """ return self._show_legend def setShowLegend(self, value): """ Set to display the legend on the graph. Parameters ---------- value : bool True to display the legend; False is not. """ self._show_legend = value if self._show_legend: if self._legend is None: self._legend = self.addLegend() else: self._legend.show() else: if self._legend is not None: self._legend.hide() def resetShowLegend(self): """ Reset the legend display status to hidden. """ self.setShowLegend(False) showLegend = Property(bool, getShowLegend, setShowLegend, resetShowLegend) def getAutoRangeX(self): return self._auto_range_x def setAutoRangeX(self, value): self._auto_range_x = value if self._auto_range_x: self.plotItem.enableAutoRange(ViewBox.XAxis, enable=self._auto_range_x) def resetAutoRangeX(self): self.setAutoRangeX(True) def getAutoRangeY(self): return self._auto_range_y def setAutoRangeY(self, value): self._auto_range_y = value if self._auto_range_y: self.plotItem.enableAutoRange(ViewBox.YAxis, enable=self._auto_range_y) def resetAutoRangeY(self): self.setAutoRangeY(True) def getMinXRange(self): """ Minimum X-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[0][0] def setMinXRange(self, new_min_x_range): """ Set the minimum X-axis value visible on the plot. Parameters ------- new_min_x_range : float """ if self._auto_range_x: return self._min_x = new_min_x_range self.plotItem.setXRange(self._min_x, self._max_x, padding=0) def getMaxXRange(self): """ Maximum X-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[0][1] def setMaxXRange(self, new_max_x_range): """ Set the Maximum X-axis value visible on the plot. Parameters ------- new_max_x_range : float """ if self._auto_range_x: return self._max_x = new_max_x_range self.plotItem.setXRange(self._min_x, self._max_x, padding=0) def getMinYRange(self): """ Minimum Y-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[1][0] def setMinYRange(self, new_min_y_range): """ Set the minimum Y-axis value visible on the plot. Parameters ------- new_min_y_range : float """ if self._auto_range_y: return self._min_y = new_min_y_range self.plotItem.setYRange(self._min_y, self._max_y, padding=0) def getMaxYRange(self): """ Maximum Y-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[1][1] def setMaxYRange(self, new_max_y_range): """ Set the maximum Y-axis value visible on the plot. Parameters ------- new_max_y_range : float """ if self._auto_range_y: return self._max_y = new_max_y_range self.plotItem.setYRange(self._min_y, self._max_y, padding=0) @Property(bool) def mouseEnabledX(self): """ Whether or not mouse interactions are enabled for the X-axis. Returns ------- bool """ return self.plotItem.getViewBox().state['mouseEnabled'][0] @mouseEnabledX.setter def mouseEnabledX(self, x_enabled): """ Whether or not mouse interactions are enabled for the X-axis. Parameters ------- x_enabled : bool """ self.plotItem.setMouseEnabled(x=x_enabled) @Property(bool) def mouseEnabledY(self): """ Whether or not mouse interactions are enabled for the Y-axis. Returns ------- bool """ return self.plotItem.getViewBox().state['mouseEnabled'][1] @mouseEnabledY.setter def mouseEnabledY(self, y_enabled): """ Whether or not mouse interactions are enabled for the Y-axis. Parameters ------- y_enabled : bool """ self.plotItem.setMouseEnabled(y=y_enabled) @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0/self._redraw_rate)*1000)) def pausePlotting(self): self.redraw_timer.stop() if self.redraw_timer.isActive() else self.redraw_timer.start() return self.redraw_timer.isActive() def mouseMoved(self, evt): """ A handler for the crosshair feature. Every time the mouse move, the mouse coordinates are updated, and the horizontal and vertical hairlines will be redrawn at the new coordinate. If a PyDMDisplay object is available, that display will also have the x- and y- values to update on the UI. Parameters ------- evt: MouseEvent The mouse event type, from which the mouse coordinates are obtained. """ pos = evt[0] if self.sceneBoundingRect().contains(pos): mouse_point = self.getViewBox().mapSceneToView(pos) self.vertical_crosshair_line.setPos(mouse_point.x()) self.horizontal_crosshair_line.setPos(mouse_point.y()) self.crosshair_position_updated.emit(mouse_point.x(), mouse_point.y()) def enableCrosshair(self, is_enabled, starting_x_pos, starting_y_pos, vertical_angle=90, horizontal_angle=0, vertical_movable=False, horizontal_movable=False): """ Enable the crosshair to be drawn on the ViewBox. Parameters ---------- is_enabled : bool True is to draw the crosshair, False is to not draw. starting_x_pos : float The x coordinate where to start the vertical crosshair line. starting_y_pos : float The y coordinate where to start the horizontal crosshair line. vertical_angle : float The angle to tilt the vertical crosshair line. Default at 90 degrees. horizontal_angle The angle to tilt the horizontal crosshair line. Default at 0 degrees. vertical_movable : bool True if the vertical line can be moved by the user; False is not. horizontal_movable False if the horizontal line can be moved by the user; False is not. """ if is_enabled: self.vertical_crosshair_line = InfiniteLine(pos=starting_x_pos, angle=vertical_angle, movable=vertical_movable) self.horizontal_crosshair_line = InfiniteLine(pos=starting_y_pos, angle=horizontal_angle, movable=horizontal_movable) self.plotItem.addItem(self.vertical_crosshair_line) self.plotItem.addItem(self.horizontal_crosshair_line) self.crosshair_movement_proxy = SignalProxy(self.plotItem.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved) else: if self.vertical_crosshair_line: self.plotItem.removeItem(self.vertical_crosshair_line) if self.horizontal_crosshair_line: self.plotItem.removeItem(self.horizontal_crosshair_line) if self.crosshair_movement_proxy: self.crosshair_movement_proxy.disconnect()
class QWaitingSpinner(QWidget): def __init__(self, parent, centerOnParent=True, disableParentWhenSpinning=False, modality=Qt.NonModal): # super().__init__(parent) QWidget.__init__(self, parent) self._centerOnParent = centerOnParent self._disableParentWhenSpinning = disableParentWhenSpinning # WAS IN initialize() self._color = QColor(Qt.black) self._roundness = 100.0 self._minimumTrailOpacity = 3.14159265358979323846 self._trailFadePercentage = 80.0 self._trailSizeDecreasing = False self._revolutionsPerSecond = 1.57079632679489661923 self._numberOfLines = 20 self._lineLength = 10 self._lineWidth = 2 self._innerRadius = 10 self._currentCounter = 0 self._isSpinning = False self._timer = QTimer(self) self._timer.timeout.connect(self.rotate) self.updateSize() self.updateTimer() self.hide() # END initialize() self.setWindowModality(modality) self.setAttribute(Qt.WA_TranslucentBackground) def paintEvent(self, QPaintEvent): self.updatePosition() painter = QPainter(self) painter.fillRect(self.rect(), Qt.transparent) painter.setRenderHint(QPainter.Antialiasing, True) if self._currentCounter >= self._numberOfLines: self._currentCounter = 0 painter.setPen(Qt.NoPen) for i in range(0, self._numberOfLines): painter.save() painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength) rotateAngle = float(360 * i) / float(self._numberOfLines) painter.rotate(rotateAngle) painter.translate(self._innerRadius, 0) distance = self.lineCountDistanceFromPrimary( i, self._currentCounter, self._numberOfLines) color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage, self._minimumTrailOpacity, self._color) # Compute the scaling factor to apply to the size and thickness # of the lines in the trail. if self._trailSizeDecreasing: sf = (self._numberOfLines - distance) / self._numberOfLines else: sf = 1 painter.setBrush(color) rect = QRect(0, round(-self._lineWidth / 2), round(sf * self._lineLength), round(sf * self._lineWidth)) painter.drawRoundedRect(rect, self._roundness, self._roundness, Qt.RelativeSize) painter.restore() def start(self): self.updatePosition() self._isSpinning = True self.show() if self.parentWidget and self._disableParentWhenSpinning: self.parentWidget().setEnabled(False) if not self._timer.isActive(): self._timer.start() self._currentCounter = 0 def stop(self): self._isSpinning = False self.hide() if self.parentWidget() and self._disableParentWhenSpinning: self.parentWidget().setEnabled(True) if self._timer.isActive(): self._timer.stop() self._currentCounter = 0 def setNumberOfLines(self, lines): self._numberOfLines = lines self._currentCounter = 0 self.updateTimer() def setLineLength(self, length): self._lineLength = length self.updateSize() def setLineWidth(self, width): self._lineWidth = width self.updateSize() def setInnerRadius(self, radius): self._innerRadius = radius self.updateSize() def color(self): return self._color def roundness(self): return self._roundness def minimumTrailOpacity(self): return self._minimumTrailOpacity def trailFadePercentage(self): return self._trailFadePercentage def revolutionsPersSecond(self): return self._revolutionsPerSecond def numberOfLines(self): return self._numberOfLines def lineLength(self): return self._lineLength def isTrailSizeDecreasing(self): """ Return whether the length and thickness of the trailing lines are decreasing. """ return self._trailSizeDecreasing def lineWidth(self): return self._lineWidth def innerRadius(self): return self._innerRadius def isSpinning(self): return self._isSpinning def setRoundness(self, roundness): self._roundness = max(0.0, min(100.0, roundness)) def setColor(self, color=Qt.black): self._color = QColor(color) def setRevolutionsPerSecond(self, revolutionsPerSecond): self._revolutionsPerSecond = revolutionsPerSecond self.updateTimer() def setTrailFadePercentage(self, trail): self._trailFadePercentage = trail def setTrailSizeDecreasing(self, value): """ Set whether the length and thickness of the trailing lines are decreasing. """ self._trailSizeDecreasing = value def setMinimumTrailOpacity(self, minimumTrailOpacity): self._minimumTrailOpacity = minimumTrailOpacity def rotate(self): self._currentCounter += 1 if self._currentCounter >= self._numberOfLines: self._currentCounter = 0 self.update() def updateSize(self): size = int((self._innerRadius + self._lineLength) * 2) self.setFixedSize(size, size) def updateTimer(self): self._timer.setInterval( int(1000 / (self._numberOfLines * self._revolutionsPerSecond))) def updatePosition(self): if self.parentWidget() and self._centerOnParent: self.move( int(self.parentWidget().width() / 2 - self.width() / 2), int(self.parentWidget().height() / 2 - self.height() / 2)) def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines): distance = primary - current if distance < 0: distance += totalNrOfLines return distance def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput): color = QColor(colorinput) if countDistance == 0: return color minAlphaF = minOpacity / 100.0 distanceThreshold = int( math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)) if countDistance > distanceThreshold: color.setAlphaF(minAlphaF) else: alphaDiff = color.alphaF() - minAlphaF gradient = alphaDiff / float(distanceThreshold + 1) resultAlpha = color.alphaF() - gradient * countDistance # If alpha is out of bounds, clip it. resultAlpha = min(1.0, max(0.0, resultAlpha)) color.setAlphaF(resultAlpha) return color
class ScrollFlagArea(Panel): """Source code editor's scroll flag area""" WIDTH = 24 if sys.platform == 'darwin' and is_dark_interface() else 12 FLAGS_DX = 4 FLAGS_DY = 2 def __init__(self, editor): Panel.__init__(self, editor) self.setAttribute(Qt.WA_OpaquePaintEvent) self.scrollable = True self.setMouseTracking(True) # Define some attributes to be used for unit testing. self._unit_testing = False self._range_indicator_is_visible = False self._alt_key_is_down = False # Define permanent Qt colors that are needed for painting the flags # and the slider range. self._facecolors = { 'warning': QColor(editor.warning_color), 'error': QColor(editor.error_color), 'todo': QColor(editor.todo_color), 'breakpoint': QColor(editor.breakpoint_color), 'occurrence': QColor(editor.occurrence_color), 'found_results': QColor(editor.found_results_color) } self._edgecolors = { key: color.darker(120) for key, color in self._facecolors.items() } self._slider_range_color = QColor(Qt.gray) self._slider_range_color.setAlphaF(.85) self._slider_range_brush = QColor(Qt.gray) self._slider_range_brush.setAlphaF(.5) editor.sig_focus_changed.connect(self.update) editor.sig_key_pressed.connect(self.keyPressEvent) editor.sig_key_released.connect(self.keyReleaseEvent) editor.sig_alt_left_mouse_pressed.connect(self.mousePressEvent) editor.sig_alt_mouse_moved.connect(self.mouseMoveEvent) editor.sig_leave_out.connect(self.update) editor.sig_flags_changed.connect(self.delayed_update_flags) editor.sig_theme_colors_changed.connect(self.update_flag_colors) self._update_list_timer = QTimer(self) self._update_list_timer.setSingleShot(True) self._update_list_timer.timeout.connect(self.update_flags) # Dictionary with flag lists self._dict_flag_list = {} @property def slider(self): """This property holds whether the vertical scrollbar is visible.""" return self.editor.verticalScrollBar().isVisible() def sizeHint(self): """Override Qt method""" return QSize(self.WIDTH, 0) def update_flag_colors(self, color_dict): """ Update the permanent Qt colors that are used for painting the flags and the slider range with the new colors defined in the given dict. """ for name, color in color_dict.items(): self._facecolors[name] = QColor(color) self._edgecolors[name] = self._facecolors[name].darker(120) def delayed_update_flags(self): """ This function is called every time a flag is changed. There is no need of updating the flags thousands of time by second, as it is quite resources-heavy. This limits the calls to REFRESH_RATE. """ if self._update_list_timer.isActive(): return self._update_list_timer.start(REFRESH_RATE) def update_flags(self): """ Update flags list. This parses the entire file, which can take a lot of time for large files. Save all the flags in lists for painting during paint events. """ self._dict_flag_list = { 'error': [], 'warning': [], 'todo': [], 'breakpoint': [], } editor = self.editor block = editor.document().firstBlock() while block.isValid(): # Parse all lines in the file looking for something to flag. data = block.userData() if data: if data.code_analysis: # Paint the errors and warnings for _, _, severity, _ in data.code_analysis: if severity == DiagnosticSeverity.ERROR: flag_type = 'error' break else: flag_type = 'warning' elif data.todo: flag_type = 'todo' elif data.breakpoint: flag_type = 'breakpoint' else: flag_type = None if flag_type is not None: self._dict_flag_list[flag_type].append(block.blockNumber()) block = block.next() self.update() def paintEvent(self, event): """ Override Qt method. Painting the scroll flag area There is two cases: - The scroll bar is moving, in which case paint all flags. - The scroll bar is not moving, only paint flags corresponding to visible lines. """ # The area in which the slider handle of the scrollbar may move. groove_rect = self.get_scrollbar_groove_rect() # The scrollbar's scale factor ratio between pixel span height and # value span height scale_factor = groove_rect.height() / self.get_scrollbar_value_height() # The vertical offset of the scroll flag area relative to the # top of the text editor. offset = groove_rect.y() # Note that we calculate the pixel metrics required to draw the flags # here instead of using the convenience methods of the ScrollFlagArea # for performance reason. rect_x = ceil(self.FLAGS_DX / 2) rect_w = self.WIDTH - self.FLAGS_DX rect_h = self.FLAGS_DY # Fill the whole painting area painter = QPainter(self) painter.fillRect(event.rect(), self.editor.sideareas_color) editor = self.editor # Define compute_flag_ypos to position the flags: # Paint flags for the entire document last_line = editor.document().lastBlock().firstLineNumber() # The 0.5 offset is used to align the flags with the center of # their corresponding text edit block before scaling. first_y_pos = self.value_to_position(0.5, scale_factor, offset) - self.FLAGS_DY / 2 last_y_pos = self.value_to_position(last_line + 0.5, scale_factor, offset) - self.FLAGS_DY / 2 def compute_flag_ypos(block): line_number = block.firstLineNumber() if editor.verticalScrollBar().maximum() == 0: geometry = editor.blockBoundingGeometry(block) pos = geometry.y() + geometry.height() / 2 + self.FLAGS_DY / 2 elif last_line != 0: frac = line_number / last_line pos = first_y_pos + frac * (last_y_pos - first_y_pos) else: pos = first_y_pos return ceil(pos) # All the lists of block numbers for flags dict_flag_lists = { "occurrence": editor.occurrences, "found_results": editor.found_results } dict_flag_lists.update(self._dict_flag_list) for flag_type in dict_flag_lists: painter.setBrush(self._facecolors[flag_type]) painter.setPen(self._edgecolors[flag_type]) for block_number in dict_flag_lists[flag_type]: # Find the block block = editor.document().findBlockByNumber(block_number) if not block.isValid(): continue # paint if everything else is fine rect_y = compute_flag_ypos(block) painter.drawRect(rect_x, rect_y, rect_w, rect_h) # Paint the slider range if not self._unit_testing: alt = QApplication.queryKeyboardModifiers() & Qt.AltModifier else: alt = self._alt_key_is_down if self.slider: cursor_pos = self.mapFromGlobal(QCursor().pos()) is_over_self = self.rect().contains(cursor_pos) is_over_editor = editor.rect().contains( editor.mapFromGlobal(QCursor().pos())) # We use QRect.contains instead of QWidget.underMouse method to # determined if the cursor is over the editor or the flag scrollbar # because the later gives a wrong result when a mouse button # is pressed. if is_over_self or (alt and is_over_editor): painter.setPen(self._slider_range_color) painter.setBrush(self._slider_range_brush) x, y, width, height = self.make_slider_range( cursor_pos, scale_factor, offset, groove_rect) painter.drawRect(x, y, width, height) self._range_indicator_is_visible = True else: self._range_indicator_is_visible = False def enterEvent(self, event): """Override Qt method""" self.update() def leaveEvent(self, event): """Override Qt method""" self.update() def mouseMoveEvent(self, event): """Override Qt method""" self.update() def mousePressEvent(self, event): """Override Qt method""" if self.slider and event.button() == Qt.LeftButton: vsb = self.editor.verticalScrollBar() value = self.position_to_value(event.pos().y()) vsb.setValue(int(value - vsb.pageStep() / 2)) def keyReleaseEvent(self, event): """Override Qt method.""" if event.key() == Qt.Key_Alt: self._alt_key_is_down = False self.update() def keyPressEvent(self, event): """Override Qt method""" if event.key() == Qt.Key_Alt: self._alt_key_is_down = True self.update() def get_vertical_offset(self): """ Return the vertical offset of the scroll flag area relative to the top of the text editor. """ groove_rect = self.get_scrollbar_groove_rect() return groove_rect.y() def get_slider_min_height(self): """ Return the minimum height of the slider range based on that set for the scroll bar's slider. """ return QApplication.instance().style().pixelMetric( QStyle.PM_ScrollBarSliderMin) def get_scrollbar_groove_rect(self): """Return the area in which the slider handle may move.""" vsb = self.editor.verticalScrollBar() style = QApplication.instance().style() opt = QStyleOptionSlider() vsb.initStyleOption(opt) # Get the area in which the slider handle may move. groove_rect = style.subControlRect(QStyle.CC_ScrollBar, opt, QStyle.SC_ScrollBarGroove, self) return groove_rect def get_scrollbar_position_height(self): """Return the pixel span height of the scrollbar area in which the slider handle may move""" groove_rect = self.get_scrollbar_groove_rect() return float(groove_rect.height()) def get_scrollbar_value_height(self): """Return the value span height of the scrollbar""" vsb = self.editor.verticalScrollBar() return vsb.maximum() - vsb.minimum() + vsb.pageStep() def get_scale_factor(self): """Return scrollbar's scale factor: ratio between pixel span height and value span height""" return (self.get_scrollbar_position_height() / self.get_scrollbar_value_height()) def value_to_position(self, y, scale_factor, offset): """Convert value to position in pixels""" vsb = self.editor.verticalScrollBar() return int((y - vsb.minimum()) * scale_factor + offset) def position_to_value(self, y): """Convert position in pixels to value""" vsb = self.editor.verticalScrollBar() offset = self.get_vertical_offset() return vsb.minimum() + max([0, (y - offset) / self.get_scale_factor()]) def make_slider_range(self, cursor_pos, scale_factor, offset, groove_rect): """ Return the slider x and y positions and the slider width and height. """ # The slider range indicator position follows the mouse vertical # position while its height corresponds to the part of the file that # is currently visible on screen. vsb = self.editor.verticalScrollBar() slider_height = self.value_to_position(vsb.pageStep(), scale_factor, offset) - offset slider_height = max(slider_height, self.get_slider_min_height()) # Calcul the minimum and maximum y-value to constraint the slider # range indicator position to the height span of the scrollbar area # where the slider may move. min_ypos = offset max_ypos = groove_rect.height() + offset - slider_height # Determine the bounded y-position of the slider rect. slider_y = max(min_ypos, min(max_ypos, ceil(cursor_pos.y() - slider_height / 2))) return 1, slider_y, self.WIDTH - 2, slider_height def wheelEvent(self, event): """Override Qt method""" self.editor.wheelEvent(event) def set_enabled(self, state): """Toggle scroll flag area visibility""" self.enabled = state self.setVisible(state)
class MatplotlibDataViewer(MatplotlibViewerMixin, DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None, projection=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self.axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs, projection=projection) MatplotlibViewerMixin.setup_callbacks(self) self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop()
class MatplotlibDataViewer(DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self._axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs) for spine in self._axes.spines.values(): spine.set_zorder(ZORDER_MAX) self.loading_rectangle = Rectangle((0, 0), 1, 1, color='0.9', alpha=0.9, zorder=ZORDER_MAX - 1, transform=self.axes.transAxes) self.loading_rectangle.set_visible(False) self.axes.add_patch(self.loading_rectangle) self.loading_text = self.axes.text( 0.4, 0.5, 'Computing', color='k', zorder=self.loading_rectangle.get_zorder() + 1, ha='left', va='center', transform=self.axes.transAxes) self.loading_text.set_visible(False) self.state.add_callback('aspect', self.update_aspect) self.update_aspect() self.state.add_callback('x_min', self.limits_to_mpl) self.state.add_callback('x_max', self.limits_to_mpl) self.state.add_callback('y_min', self.limits_to_mpl) self.state.add_callback('y_max', self.limits_to_mpl) self.limits_to_mpl() self.state.add_callback('x_log', self.update_x_log, priority=1000) self.state.add_callback('y_log', self.update_y_log, priority=1000) self.update_x_log() self.axes.callbacks.connect('xlim_changed', self.limits_from_mpl) self.axes.callbacks.connect('ylim_changed', self.limits_from_mpl) self.axes.set_autoscale_on(False) self.state.add_callback('x_axislabel', self.update_x_axislabel) self.state.add_callback('x_axislabel_weight', self.update_x_axislabel) self.state.add_callback('x_axislabel_size', self.update_x_axislabel) self.state.add_callback('y_axislabel', self.update_y_axislabel) self.state.add_callback('y_axislabel_weight', self.update_y_axislabel) self.state.add_callback('y_axislabel_size', self.update_y_axislabel) self.state.add_callback('x_ticklabel_size', self.update_x_ticklabel) self.state.add_callback('y_ticklabel_size', self.update_y_ticklabel) self.update_x_axislabel() self.update_y_axislabel() self.update_x_ticklabel() self.update_y_ticklabel() self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop() def add_data(self, *args, **kwargs): return super(MatplotlibDataViewer, self).add_data(*args, **kwargs) def add_subset(self, *args, **kwargs): return super(MatplotlibDataViewer, self).add_subset(*args, **kwargs) def update_x_axislabel(self, *event): self.axes.set_xlabel(self.state.x_axislabel, weight=self.state.x_axislabel_weight, size=self.state.x_axislabel_size) self.redraw() def update_y_axislabel(self, *event): self.axes.set_ylabel(self.state.y_axislabel, weight=self.state.y_axislabel_weight, size=self.state.y_axislabel_size) self.redraw() def update_x_ticklabel(self, *event): self.axes.tick_params(axis='x', labelsize=self.state.x_ticklabel_size) self.axes.xaxis.get_offset_text().set_fontsize( self.state.x_ticklabel_size) self.redraw() def update_y_ticklabel(self, *event): self.axes.tick_params(axis='y', labelsize=self.state.y_ticklabel_size) self.axes.yaxis.get_offset_text().set_fontsize( self.state.y_ticklabel_size) self.redraw() def redraw(self): self.figure.canvas.draw() def update_x_log(self, *args): self.axes.set_xscale('log' if self.state.x_log else 'linear') self.redraw() def update_y_log(self, *args): self.axes.set_yscale('log' if self.state.y_log else 'linear') self.redraw() def update_aspect(self, aspect=None): self.axes.set_aspect(self.state.aspect, adjustable='datalim') @avoid_circular def limits_from_mpl(self, *args): with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): if isinstance(self.state.x_min, np.datetime64): x_min, x_max = [ mpl_to_datetime64(x) for x in self.axes.get_xlim() ] else: x_min, x_max = self.axes.get_xlim() self.state.x_min, self.state.x_max = x_min, x_max if isinstance(self.state.y_min, np.datetime64): y_min, y_max = [ mpl_to_datetime64(y) for y in self.axes.get_ylim() ] else: y_min, y_max = self.axes.get_ylim() self.state.y_min, self.state.y_max = y_min, y_max @avoid_circular def limits_to_mpl(self, *args): if self.state.x_min is not None and self.state.x_max is not None: x_min, x_max = self.state.x_min, self.state.x_max if self.state.x_log: if self.state.x_max <= 0: x_min, x_max = 0.1, 1 elif self.state.x_min <= 0: x_min = x_max / 10 self.axes.set_xlim(x_min, x_max) if self.state.y_min is not None and self.state.y_max is not None: y_min, y_max = self.state.y_min, self.state.y_max if self.state.y_log: if self.state.y_max <= 0: y_min, y_max = 0.1, 1 elif self.state.y_min <= 0: y_min = y_max / 10 self.axes.set_ylim(y_min, y_max) if self.state.aspect == 'equal': # FIXME: for a reason I don't quite understand, dataLim doesn't # get updated immediately here, which means that there are then # issues in the first draw of the image (the limits are such that # only part of the image is shown). We just set dataLim manually # to avoid this issue. self.axes.dataLim.intervalx = self.axes.get_xlim() self.axes.dataLim.intervaly = self.axes.get_ylim() # We then force the aspect to be computed straight away self.axes.apply_aspect() # And propagate any changes back to the state since we have the # @avoid_circular decorator with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): # TODO: fix case with datetime64 here self.state.x_min, self.state.x_max = self.axes.get_xlim() self.state.y_min, self.state.y_max = self.axes.get_ylim() self.axes.figure.canvas.draw() # TODO: shouldn't need this! @property def axes(self): return self._axes def _update_appearance_from_settings(self, message=None): update_appearance_from_settings(self.axes) self.redraw() def get_layer_artist(self, cls, layer=None, layer_state=None): return cls(self.axes, self.state, layer=layer, layer_state=layer_state) def apply_roi(self, roi, use_current=False): """ This method must be implemented by subclasses """ raise NotImplementedError def _script_header(self): state_dict = self.state.as_dict() return ['import matplotlib.pyplot as plt' ], SCRIPT_HEADER.format(**state_dict) def _script_footer(self): state_dict = self.state.as_dict() state_dict['x_log_str'] = 'log' if self.state.x_log else 'linear' state_dict['y_log_str'] = 'log' if self.state.y_log else 'linear' return [], SCRIPT_FOOTER.format(**state_dict)
class livestream(QWidget): i = 0 def __init__(self,qnd,images = None,annotations_on = True,annotate_coords = None,threshold_switch = False): QWidget.__init__(self) self.threshold_switch = threshold_switch self.video = images #frames buffer self.videobox = Label() if annotations_on and annotate_coords is not None: self.coords = annotate_coords self.videobox.switch = annotations_on self.videobox.activecoord = self.coords[0] if self.video is not None: self.videobox.activeframe = self.video[0] self.videobox.maxintens = self.video.shape[0] else: self.videobox.activeframe = np.loadtxt(os.getcwd() + '/defaultimage.txt') print(self.videobox.activeframe.shape) self.videobox.maxintens = np.max(self.videobox.activeframe) self.videobox.setGeometry(QtCore.QRect(70, 80, 310, 310)) self.videobox.h = 310 self.videobox.w = 310 self.lyt = QVBoxLayout() self.lyt.addWidget(self.videobox,5) self.setLayout(self.lyt) self.sl = QSlider(Qt.Horizontal) self.sl.setMinimum(0.0) if self.video is not None: self.sl.setMaximum(self.video.shape[0]) self.sl.valueChanged.connect(self.whenslidechanges) self.sl.setTickPosition(QSlider.TicksAbove) self.sl.setTracking(True) self.sl.setTickInterval(100) self.frame_counter = QDoubleSpinBox() if images is not None: self.frame = images[0] self.frame_counter.valueChanged.connect(self.video_time_update) self.frame_counter.setSingleStep(1) self.frame_counter.setRange(self.sl.minimum(),self.sl.maximum()) self.frame_counter.valueChanged.connect(self.sl.setValue) self.video_time = QDoubleSpinBox() self.video_time.setSingleStep(30) self.video_time.setRange(self.sl.minimum(),30*self.sl.maximum()) self.frameratetimer = QTimer() self.frameratetimer.setInterval(30) if self.video is not None: self.frameratetimer.timeout.connect(self.update_display) self.play_button = QPushButton('Play Video') self.play_button.clicked.connect(self.frameratetimer.start) self.stop_button = QPushButton('Stop Video') self.stop_button.clicked.connect(self.frameratetimer.stop) if self.video is not None: self.sl.valueChanged.connect(self.whenslidechanges) self.lyt.addWidget(self.play_button,0) self.lyt.addWidget(self.stop_button,1) self.lyt.addWidget(self.sl,2) self.lyt.addWidget(self.frame_counter,3) self.lyt.addWidget(self.video_time,4) self.show() def assign_images(self,images,centres = None): '''#first disconnect signals from eachother so nothing should change whilst video data is being updated self.sl.valueChanged.disconnect(self.video_time_update) self.frameratetimer.timeout.disconnect(self.update_display) self.frame_counter.valueChanged.disconnect(self.whenslidechanges) ''' self.video = images self.coords = centres self.videobox.activeframe = self.video[0] if self.coords is not None: self.videobox.activecoord = self.coords[0] #readjust slider and ticker values to dimensions of video self.sl.setMaximum(len(self.video)-1) self.frame_counter.setRange(self.sl.minimum(),self.sl.maximum()) self.video_time.setRange(self.sl.minimum(),30*self.sl.maximum()) #connect slider and timer etc. self.sl.valueChanged.connect(self.whenslidechanges) self.frameratetimer.timeout.connect(self.update_display) self.frame_counter.valueChanged.connect(self.video_time_update) self.videobox.maxintens = np.max(self.video) self.videobox.update() def update_display(self): if self.threshold_switch: frame = self.video[livestream.i] threshold = threshold_otsu(frame) mask = np.zeros_like(frame) mask[frame > threshold] = 1 self.videobox.maxintens = 1 self.videobox.activeframe = mask else: #if threshold switch is off display usual video, so change active frame source and reset maximum intensity for passing to qimage2ndarray self.videobox.activeframe = self.video[livestream.i] self.videobox.maxintens = np.max(self.video) try: self.videobox.activecoord = self.coords[livestream.i] if not self.videobox.switch: self.videobox.switch = True except: self.videobox.activecoord = None self.videobox.switch = False self.videobox.update() self.frame_counter.setValue(float(livestream.i)) livestream.i+=1 def whenslidechanges(self): if self.frameratetimer.isActive(): self.frameratetimer.stop() livestream.i = self.sl.value() self.update_display() livestream.i -=1 self.frameratetimer.start() else: livestream.i = self.sl.value() self.update_display() livestream.i -=1 def video_time_update(self): self.video_time.setValue(30*self.frame_counter.value()) def turn_on_threshold(self,threshold_switch): self.threshold_switch = threshold_switch self.update_display()
class QtPoll(QObject): """Polls anything once per frame via an event. QtPoll was first created for VispyTiledImageLayer. It polls the visual when the camera moves. However, we also want visuals to keep loading chunks even when the camera stops. We want the visual to finish up anything that was in progress. Before it goes fully idle. QtPoll will poll those visuals using a timer. If the visual says the event was "handled" it means the visual has more work to do. If that happens, QtPoll will continue to poll and draw the visual it until the visual is done with the in-progress work. An analogy is a snow globe. The user moving the camera shakes up the snow globe. We need to keep polling/drawing things until all the snow settles down. Then everything will stay completely still until the camera is moved again, shaking up the globe once more. Parameters ---------- parent : QObject Parent Qt object. camera : Camera The viewer's main camera. """ def __init__(self, parent: QObject, camera: Camera): super().__init__(parent) self.events = EmitterGroup(source=self, auto_connect=True, poll=None) camera.events.connect(self._on_camera) self.timer = QTimer() self.timer.setInterval(POLL_INTERVAL_MS) self.timer.timeout.connect(self._on_timer) def _on_camera(self, _event) -> None: """Called when camera view changes at all.""" # Poll right away. If the timer is running, it's generally starved # out by the mouse button being down. Why? If we end up "double # polling" it *should* be harmless. But if we don't poll then # everything is frozen. So better to poll. self._poll() # Start the timer so that we will keep polling even if the camera # doesn't move again. Although the mouse movement is starving out # the timer right now, we need the timer going so we keep polling # even if the mouse stops. self.timer.start() def _on_timer(self) -> None: """Called when the timer is running.""" # The timer is running which means someone we are polling still has # work to do. self._poll() def _poll(self) -> None: """Called on camera move or with the timer.""" # Poll everyone listening to our even. event = self.events.poll() # Listeners will "handle" the event if they need more polling. If # no one needs polling, then we can stop the timer. if not event.handled: self.timer.stop() return # Someone handled the event, so they want to be polled even if # the mouse doesn't move. So start the timer if needed. if not self.timer.isActive(): self.timer.start() def closeEvent(self, _event: QEvent) -> None: """Cleanup and close. Parameters ---------- event : QEvent The close event. """ self.timer.stop() self.deleteLater()
class QtLayerList(QScrollArea): """Widget storing a list of all the layers present in the current window. Parameters ---------- layers : napari.components.LayerList The layer list to track and display. Attributes ---------- centers : list List of layer widgets center coordinates. layers : napari.components.LayerList The layer list to track and display. vbox_layout : QVBoxLayout The layout instance in which the layouts appear. """ def __init__(self, layers: 'LayerList'): super().__init__() self.layers = layers self.setAttribute(Qt.WA_DeleteOnClose) self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.vbox_layout.setSpacing(2) self.centers = [] # Create a timer to be used for autoscrolling the layers list up and # down when dragging a layer near the end of the displayed area self._drag_timer = QTimer() self._drag_timer.setSingleShot(False) self._drag_timer.setInterval(20) self._drag_timer.timeout.connect(self._force_scroll) self._scroll_up = True self._min_scroll_region = 24 self.setAcceptDrops(True) self.setToolTip(trans._('Layer list')) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.layers.events.inserted.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) self.layers.selection.events.changed.connect(self._on_selection_change) self._drag_start_position = np.zeros(2) self._drag_name = None self.chunk_receiver = _create_chunk_receiver(self) def _on_selection_change(self, event): for layer in event.added: w = self._find_widget(layer) if w: w.setSelected(True) for layer in event.removed: w = self._find_widget(layer) if w: w.setSelected(False) if event.added: self._ensure_visible(list(event.added)[-1]) def _find_widget(self, layer): for i in range(self.vbox_layout.count()): w = self.vbox_layout.itemAt(i).widget() if getattr(w, 'layer', None) == layer: return w def _add(self, event): """Insert widget for layer `event.value` at index `event.index`. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer = event.value total = len(self.layers) index = 2 * (total - event.index) - 1 widget = QtLayerWidget(layer, selected=layer in self.layers.selection) self.vbox_layout.insertWidget(index, widget) self.vbox_layout.insertWidget(index + 1, QtDivider()) def _remove(self, event): """Remove widget for layer at index `event.index`. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer_index = event.index total = len(self.layers) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) + 1 widget = self.vbox_layout.itemAt(index).widget() divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(widget) disconnect_events(widget.layer.events, self) widget.close() self.vbox_layout.removeWidget(divider) divider.deleteLater() def _reorder(self, event=None): """Reorder list of layer widgets. Loops through all widgets in list, sequentially removing them and inserting them into the correct place in the final list. Parameters ---------- event : napari.utils.event.Event, optional The napari event that triggered this method. """ total = len(self.layers) # Create list of the current property and divider widgets widgets = [ self.vbox_layout.itemAt(i + 1).widget() for i in range(2 * total) ] # Take every other widget to ignore the dividers and get just the # property widgets indices = [ self.layers.index(w.layer) for i, w in enumerate(widgets) if i % 2 == 0 ] # Move through the layers in order for i in range(total): # Find index of property widget in list of the current layer index = 2 * indices.index(i) widget = widgets[index] divider = widgets[index + 1] # Check if current index does not match new index index_current = self.vbox_layout.indexOf(widget) index_new = 2 * (total - i) - 1 if index_current != index_new: # Remove that property widget and divider self.vbox_layout.removeWidget(widget) self.vbox_layout.removeWidget(divider) # Insert the property widget and divider into new location self.vbox_layout.insertWidget(index_new, widget) self.vbox_layout.insertWidget(index_new + 1, divider) def _force_scroll(self): """Force the scroll bar to automattically scroll either up or down.""" cur_value = self.verticalScrollBar().value() if self._scroll_up: new_value = cur_value - self.verticalScrollBar().singleStep() / 4 if new_value < 0: new_value = 0 self.verticalScrollBar().setValue(new_value) else: new_value = cur_value + self.verticalScrollBar().singleStep() / 4 if new_value > self.verticalScrollBar().maximum(): new_value = self.verticalScrollBar().maximum() self.verticalScrollBar().setValue(new_value) def _ensure_visible(self, layer): """Ensure layer widget for at particular layer is visible. Parameters ---------- layer : napari.layers.Layer An instance of a napari layer. """ total = len(self.layers) layer_index = self.layers.index(layer) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) - 1 widget = self.vbox_layout.itemAt(index).widget() self.ensureWidgetVisible(widget) def keyPressEvent(self, event): """Ignore a key press event. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() def keyReleaseEvent(self, event): """Ignore key release event. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() def mousePressEvent(self, event): """Register mouse click if it happens on a layer widget. Checks if mouse press happens on a layer properties widget or a child of such a widget. If not, the press has happened on the Layers Widget itself and should be ignored. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ widget = self.childAt(event.pos()) layer = ( getattr(widget, 'layer', None) or getattr(widget.parentWidget(), 'layer', None) or getattr(widget.parentWidget().parentWidget(), 'layer', None) ) if layer is not None: self._drag_start_position = np.array( [event.pos().x(), event.pos().y()] ) self._drag_name = layer.name else: self._drag_name = None def mouseReleaseEvent(self, event): """Select layer using mouse click. Key modifiers: Shift - If the Shift button is pressed, select all layers in between currently selected one and the clicked one. Control - If the Control button is pressed, mouse click will toggle selected state of the layer. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self._drag_name is None: # Unselect all the layers if not dragging a layer self.layers.selection.active = None return modifiers = event.modifiers() clicked_layer = self.layers[self._drag_name] if modifiers == Qt.ShiftModifier and self.layers.selection._current: # shift-click: select all layers between current and clicked clicked = self.layers.index(clicked_layer) current = self.layers.index(self.layers.selection._current) from_, to_ = sorted([clicked, current]) _to_select = self.layers[slice(from_, to_ + 1)] # inclusive range self.layers.selection.update(_to_select) self.layers.selection._current = clicked_layer elif modifiers == Qt.ControlModifier: # If control click toggle selected state of clicked layer self.layers.selection.toggle(clicked_layer) else: # If otherwise unselect all and leave clicked one selected self.layers.selection.active = clicked_layer def mouseMoveEvent(self, event): """Drag and drop layer with mouse movement. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ position = np.array([event.pos().x(), event.pos().y()]) distance = np.linalg.norm(position - self._drag_start_position) if ( distance < QApplication.startDragDistance() or self._drag_name is None ): return mimeData = QMimeData() mimeData.setText(self._drag_name) drag = QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) drag.exec_() # Check if dragged layer still exists or was deleted during drag names = [layer.name for layer in self.layers] dragged_layer_exists = self._drag_name in names if self._drag_name is not None and dragged_layer_exists: index = self.layers.index(self._drag_name) layer = self.layers[index] self._ensure_visible(layer) def dragLeaveEvent(self, event): """Unselects layer dividers. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() self._drag_timer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): """Update divider position before dragging layer widget to new position Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.source() == self: event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [ (divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1) ] else: event.ignore() def dragMoveEvent(self, event): """Highlight appriate divider when dragging layer to new position. Sets the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ max_height = self.frameGeometry().height() if ( event.pos().y() < self._min_scroll_region and not self._drag_timer.isActive() ): self._scroll_up = True self._drag_timer.start() elif ( event.pos().y() > max_height - self._min_scroll_region and not self._drag_timer.isActive() ): self._scroll_up = False self._drag_timer.start() elif ( self._drag_timer.isActive() and event.pos().y() >= self._min_scroll_region and event.pos().y() <= max_height - self._min_scroll_region ): self._drag_timer.stop() # Determine which widget center is the mouse currently closed to cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) # Determine the current location of the widget being dragged total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self._drag_name) # If the widget being dragged hasn't moved above or below any other # widgets then don't highlight any dividers selected = not (insert == index) and not (insert - 1 == index) # Set the selected state of all the dividers for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(selected) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): """Drop dragged layer widget into new position in the list of layers. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self._drag_timer.isActive(): self._drag_timer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self._drag_name) if index != insert and index + 1 != insert: if insert >= index: insert -= 1 self.layers.move_selected(index, insert) event.accept()
class ProgressView(QWidget): """ :type batch_manager: CalculationManager """ def __init__(self, parent, batch_manager): super().__init__(parent) self.task_count = 0 self.calculation_manager = batch_manager self.whole_progress = QProgressBar(self) self.whole_progress.setMinimum(0) self.whole_progress.setMaximum(1) self.whole_progress.setFormat("%v of %m") self.whole_progress.setTextVisible(True) self.part_progress = QProgressBar(self) self.part_progress.setMinimum(0) self.part_progress.setMaximum(1) self.part_progress.setFormat("%v of %m") self.whole_label = QLabel("All batch progress:", self) self.part_label = QLabel("Single batch progress:", self) self.cancel_remove_btn = QPushButton("Remove task") self.cancel_remove_btn.setDisabled(True) self.logs = ExceptionList(self) self.logs.setToolTip("Logs") self.task_view = QListView() self.task_que = QStandardItemModel(self) self.task_view.setModel(self.task_que) self.process_num_timer = QTimer() self.process_num_timer.setInterval(1000) self.process_num_timer.setSingleShot(True) self.process_num_timer.timeout.connect(self.change_number_of_workers) self.number_of_process = QSpinBox(self) self.number_of_process.setRange(1, multiprocessing.cpu_count()) self.number_of_process.setValue(1) self.number_of_process.setToolTip( "Number of process used in batch calculation") self.number_of_process.valueChanged.connect( self.process_num_timer_start) self.progress_item_dict = {} layout = QGridLayout() layout.addWidget(self.whole_label, 0, 0, Qt.AlignRight) layout.addWidget(self.whole_progress, 0, 1, 1, 2) layout.addWidget(self.part_label, 1, 0, Qt.AlignRight) layout.addWidget(self.part_progress, 1, 1, 1, 2) lab = QLabel("Number of process:") lab.setToolTip("Number of process used in batch calculation") layout.addWidget(lab, 2, 0) layout.addWidget(self.number_of_process, 2, 1) layout.addWidget(self.logs, 3, 0, 2, 3) layout.addWidget(self.task_view, 0, 4, 4, 1) layout.addWidget(self.cancel_remove_btn, 4, 4, 1, 1) layout.setColumnMinimumWidth(2, 10) layout.setColumnStretch(2, 1) self.setLayout(layout) self.preview_timer = QTimer() self.preview_timer.setInterval(1000) self.preview_timer.timeout.connect(self.update_info) self.task_view.selectionModel().currentChanged.connect( self.task_selection_change) self.cancel_remove_btn.clicked.connect(self.task_cancel_remove) def task_selection_change(self, new, old): task: CalculationProcessItem = self.task_que.item( new.row(), new.column()) if task is None: self.cancel_remove_btn.setDisabled(True) return self.cancel_remove_btn.setEnabled(True) if task.is_finished(): self.cancel_remove_btn.setText(f"Remove task {task.num}") else: self.cancel_remove_btn.setText(f"Cancel task {task.num}") def task_cancel_remove(self): index = self.task_view.selectionModel().currentIndex() task: CalculationProcessItem = self.task_que.item( index.row(), index.column()) if task.is_finished(): self.calculation_manager.remove_calculation(task.calculation) self.task_que.takeRow(index.row()) else: self.calculation_manager.cancel_calculation(task.calculation) print(task) def new_task(self): self.whole_progress.setMaximum( self.calculation_manager.calculation_size) if not self.preview_timer.isActive(): self.update_info() self.preview_timer.start() def update_info(self): res = self.calculation_manager.get_results() for el in res.errors: if el[0]: QListWidgetItem(el[0], self.logs) ExceptionListItem(el[1], self.logs) if (state_store.report_errors and parsed_version.is_devrelease and not isinstance(el[1][0], SegmentationLimitException) and isinstance(el[1][1], tuple)): with sentry_sdk.push_scope() as scope: scope.set_tag("auto_report", "true") sentry_sdk.capture_event(el[1][1][0]) self.whole_progress.setValue(res.global_counter) working_search = True for uuid, progress in res.jobs_status.items(): calculation = self.calculation_manager.calculation_dict[uuid] total = len(calculation.file_list) if uuid in self.progress_item_dict: item = self.progress_item_dict[uuid] item.update_count(progress) else: item = CalculationProcessItem(calculation, self.task_count, progress) self.task_count += 1 self.task_que.appendRow(item) self.progress_item_dict[uuid] = item if working_search and progress != total: self.part_progress.setMaximum(total) self.part_progress.setValue(progress) working_search = False if not self.calculation_manager.has_work: self.part_progress.setValue(self.part_progress.maximum()) self.preview_timer.stop() logging.info("Progress stop") def process_num_timer_start(self): self.process_num_timer.start() def update_progress(self, total_progress, part_progress): self.whole_progress.setValue(total_progress) self.part_progress.setValue(part_progress) def set_total_size(self, size): self.whole_progress.setMaximum(size) def set_part_size(self, size): self.part_progress.setMaximum(size) def change_number_of_workers(self): self.calculation_manager.set_number_of_workers( self.number_of_process.value())
class MatplotlibDataViewer(DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self._axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs) for spine in self._axes.spines.values(): spine.set_zorder(ZORDER_MAX) self.loading_rectangle = Rectangle((0, 0), 1, 1, color='0.9', alpha=0.9, zorder=ZORDER_MAX - 1, transform=self.axes.transAxes) self.loading_rectangle.set_visible(False) self.axes.add_patch(self.loading_rectangle) self.loading_text = self.axes.text(0.4, 0.5, 'Computing', color='k', zorder=self.loading_rectangle.get_zorder() + 1, ha='left', va='center', transform=self.axes.transAxes) self.loading_text.set_visible(False) self.state.add_callback('aspect', self.update_aspect) self.update_aspect() self.state.add_callback('x_min', self.limits_to_mpl) self.state.add_callback('x_max', self.limits_to_mpl) self.state.add_callback('y_min', self.limits_to_mpl) self.state.add_callback('y_max', self.limits_to_mpl) self.limits_to_mpl() self.state.add_callback('x_log', self.update_x_log, priority=1000) self.state.add_callback('y_log', self.update_y_log, priority=1000) self.update_x_log() self.axes.callbacks.connect('xlim_changed', self.limits_from_mpl) self.axes.callbacks.connect('ylim_changed', self.limits_from_mpl) self.axes.set_autoscale_on(False) self.state.add_callback('x_axislabel', self.update_x_axislabel) self.state.add_callback('x_axislabel_weight', self.update_x_axislabel) self.state.add_callback('x_axislabel_size', self.update_x_axislabel) self.state.add_callback('y_axislabel', self.update_y_axislabel) self.state.add_callback('y_axislabel_weight', self.update_y_axislabel) self.state.add_callback('y_axislabel_size', self.update_y_axislabel) self.state.add_callback('x_ticklabel_size', self.update_x_ticklabel) self.state.add_callback('y_ticklabel_size', self.update_y_ticklabel) self.update_x_axislabel() self.update_y_axislabel() self.update_x_ticklabel() self.update_y_ticklabel() self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop() def add_data(self, *args, **kwargs): return super(MatplotlibDataViewer, self).add_data(*args, **kwargs) def add_subset(self, *args, **kwargs): return super(MatplotlibDataViewer, self).add_subset(*args, **kwargs) def update_x_axislabel(self, *event): self.axes.set_xlabel(self.state.x_axislabel, weight=self.state.x_axislabel_weight, size=self.state.x_axislabel_size) self.redraw() def update_y_axislabel(self, *event): self.axes.set_ylabel(self.state.y_axislabel, weight=self.state.y_axislabel_weight, size=self.state.y_axislabel_size) self.redraw() def update_x_ticklabel(self, *event): self.axes.tick_params(axis='x', labelsize=self.state.x_ticklabel_size) self.axes.xaxis.get_offset_text().set_fontsize(self.state.x_ticklabel_size) self.redraw() def update_y_ticklabel(self, *event): self.axes.tick_params(axis='y', labelsize=self.state.y_ticklabel_size) self.axes.yaxis.get_offset_text().set_fontsize(self.state.y_ticklabel_size) self.redraw() def redraw(self): self.figure.canvas.draw() def update_x_log(self, *args): self.axes.set_xscale('log' if self.state.x_log else 'linear') self.redraw() def update_y_log(self, *args): self.axes.set_yscale('log' if self.state.y_log else 'linear') self.redraw() def update_aspect(self, aspect=None): self.axes.set_aspect(self.state.aspect, adjustable='datalim') @avoid_circular def limits_from_mpl(self, *args): with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): if isinstance(self.state.x_min, np.datetime64): x_min, x_max = [mpl_to_datetime64(x) for x in self.axes.get_xlim()] else: x_min, x_max = self.axes.get_xlim() self.state.x_min, self.state.x_max = x_min, x_max if isinstance(self.state.y_min, np.datetime64): y_min, y_max = [mpl_to_datetime64(y) for y in self.axes.get_ylim()] else: y_min, y_max = self.axes.get_ylim() self.state.y_min, self.state.y_max = y_min, y_max @avoid_circular def limits_to_mpl(self, *args): if self.state.x_min is not None and self.state.x_max is not None: x_min, x_max = self.state.x_min, self.state.x_max if self.state.x_log: if self.state.x_max <= 0: x_min, x_max = 0.1, 1 elif self.state.x_min <= 0: x_min = x_max / 10 self.axes.set_xlim(x_min, x_max) if self.state.y_min is not None and self.state.y_max is not None: y_min, y_max = self.state.y_min, self.state.y_max if self.state.y_log: if self.state.y_max <= 0: y_min, y_max = 0.1, 1 elif self.state.y_min <= 0: y_min = y_max / 10 self.axes.set_ylim(y_min, y_max) if self.state.aspect == 'equal': # FIXME: for a reason I don't quite understand, dataLim doesn't # get updated immediately here, which means that there are then # issues in the first draw of the image (the limits are such that # only part of the image is shown). We just set dataLim manually # to avoid this issue. self.axes.dataLim.intervalx = self.axes.get_xlim() self.axes.dataLim.intervaly = self.axes.get_ylim() # We then force the aspect to be computed straight away self.axes.apply_aspect() # And propagate any changes back to the state since we have the # @avoid_circular decorator with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): # TODO: fix case with datetime64 here self.state.x_min, self.state.x_max = self.axes.get_xlim() self.state.y_min, self.state.y_max = self.axes.get_ylim() self.axes.figure.canvas.draw() # TODO: shouldn't need this! @property def axes(self): return self._axes def _update_appearance_from_settings(self, message=None): update_appearance_from_settings(self.axes) self.redraw() def get_layer_artist(self, cls, layer=None, layer_state=None): return cls(self.axes, self.state, layer=layer, layer_state=layer_state) def apply_roi(self, roi, use_current=False): """ This method must be implemented by subclasses """ raise NotImplementedError def _script_header(self): state_dict = self.state.as_dict() return ['import matplotlib.pyplot as plt'], SCRIPT_HEADER.format(**state_dict) def _script_footer(self): state_dict = self.state.as_dict() state_dict['x_log_str'] = 'log' if self.state.x_log else 'linear' state_dict['y_log_str'] = 'log' if self.state.y_log else 'linear' return [], SCRIPT_FOOTER.format(**state_dict)
class InputsWidget(QWidget, Ui_Form): """There has following functions: + Function of mechanism variables settings. + Path recording. """ about_to_resolve = Signal() def __init__(self, parent: MainWindowBase) -> None: super(InputsWidget, self).__init__(parent) self.setupUi(self) # parent's function pointer self.free_move_button = parent.free_move_button self.entities_point = parent.entities_point self.entities_link = parent.entities_link self.vpoints = parent.vpoint_list self.vlinks = parent.vlink_list self.main_canvas = parent.main_canvas self.solve = parent.solve self.reload_canvas = parent.reload_canvas self.output_to = parent.output_to self.conflict = parent.conflict self.dof = parent.dof self.right_input = parent.right_input self.command_stack = parent.command_stack self.set_coords_as_current = parent.set_coords_as_current self.get_back_position = parent.get_back_position # Angle panel self.dial = QRotatableView(self) self.dial.setStatusTip("Input widget of rotatable joint.") self.dial.setEnabled(False) self.dial.value_changed.connect(self.__update_var) self.dial_spinbox.valueChanged.connect(self.__set_var) self.inputs_dial_layout.addWidget(self.dial) # Play button self.variable_stop.clicked.connect(self.variable_value_reset) # Timer for play button self.inputs_play_shaft = QTimer() self.inputs_play_shaft.setInterval(10) self.inputs_play_shaft.timeout.connect(self.__change_index) # Change the point coordinates with current position self.update_pos.clicked.connect(self.set_coords_as_current) # Inputs record context menu self.pop_menu_record_list = QMenu(self) self.record_list.customContextMenuRequested.connect( self.__record_list_context_menu) self.__path_data: Dict[str, _Paths] = {} def clear(self) -> None: """Clear function to reset widget status.""" self.__path_data.clear() for _ in range(self.record_list.count() - 1): self.record_list.takeItem(1) self.variable_list.clear() def __set_angle_mode(self) -> None: """Change to angle input.""" self.dial.set_minimum(0) self.dial.set_maximum(360) self.dial_spinbox.setMinimum(0) self.dial_spinbox.setMaximum(360) def __set_unit_mode(self) -> None: """Change to unit input.""" self.dial.set_minimum(-500) self.dial.set_maximum(500) self.dial_spinbox.setMinimum(-500) self.dial_spinbox.setMaximum(500) def path_data(self) -> Dict[str, _Paths]: """Return current path data.""" return self.__path_data @Slot(tuple) def set_selection(self, selections: Sequence[int]) -> None: """Set one selection from canvas.""" self.joint_list.setCurrentRow(selections[0]) @Slot() def clear_selection(self) -> None: """Clear the points selection.""" self.driver_list.clear() self.joint_list.setCurrentRow(-1) @Slot(int, name='on_joint_list_currentRowChanged') def __update_relate_points(self, _=None) -> None: """Change the point row from input widget.""" self.driver_list.clear() item: Optional[QListWidgetItem] = self.joint_list.currentItem() if item is None: return p0 = _variable_int(item.text()) base_point = self.vpoints[p0] type_int = base_point.type if type_int == VJoint.R: for i, vpoint in enumerate(self.vpoints): if i == p0: continue if base_point.same_link(vpoint): if base_point.grounded() and vpoint.grounded(): continue self.driver_list.addItem(f"[{vpoint.type_str}] Point{i}") elif type_int in {VJoint.P, VJoint.RP}: self.driver_list.addItem(f"[{base_point.type_str}] Point{p0}") @Slot(int, name='on_driver_list_currentRowChanged') def __set_add_var_enabled(self, _=None) -> None: """Set enable of 'add variable' button.""" driver = self.driver_list.currentIndex() self.variable_add.setEnabled(driver != -1) @Slot(name='on_variable_add_clicked') def __add_inputs_variable(self, p0: Optional[int] = None, p1: Optional[int] = None) -> None: """Add variable with '->' sign.""" if p0 is None: item: Optional[QListWidgetItem] = self.joint_list.currentItem() if item is None: return p0 = _variable_int(item.text()) if p1 is None: item = self.driver_list.currentItem() if item is None: return p1 = _variable_int(item.text()) # Check DOF if self.dof() <= self.input_count(): QMessageBox.warning( self, "Wrong DOF", "The number of variable must no more than degrees of freedom.") return # Check same link if not self.vpoints[p0].same_link(self.vpoints[p1]): QMessageBox.warning( self, "Wrong pair", "The base point and driver point should at the same link.") return # Check repeated pairs for p0_, p1_, a in self.input_pairs(): if {p0, p1} == {p0_, p1_} and self.vpoints[p0].type == VJoint.R: QMessageBox.warning(self, "Wrong pair", "There already have a same pair.") return if p0 == p1: # One joint by offset value = self.vpoints[p0].true_offset() else: # Two joints by angle value = self.vpoints[p0].slope_angle(self.vpoints[p1]) self.command_stack.push( AddInput( '->'.join(( f'Point{p0}', f"Point{p1}", f"{value:.02f}", )), self.variable_list)) def add_inputs_variables(self, variables: Sequence[Tuple[int, int]]) -> None: """Add from database.""" for p0, p1 in variables: self.__add_inputs_variable(p0, p1) @Slot(QListWidgetItem, name='on_variable_list_itemClicked') def __dial_ok(self, _=None) -> None: """Set the angle of base link and drive link.""" if self.inputs_play_shaft.isActive(): return row = self.variable_list.currentRow() enabled = row > -1 rotatable = (enabled and not self.free_move_button.isChecked() and self.right_input()) self.dial.setEnabled(rotatable) self.dial_spinbox.setEnabled(rotatable) self.oldVar = self.dial.value() self.variable_play.setEnabled(rotatable) self.variable_speed.setEnabled(rotatable) item: Optional[QListWidgetItem] = self.variable_list.currentItem() if item is None: return expr = item.text().split('->') p0 = int(expr[0].replace('Point', '')) p1 = int(expr[1].replace('Point', '')) value = float(expr[2]) if p0 == p1: self.__set_unit_mode() else: self.__set_angle_mode() self.dial.set_value(value if enabled else 0) def variable_excluding(self, row: Optional[int] = None) -> None: """Remove variable if the point was been deleted. Default: all.""" one_row: bool = row is not None for i, (b, d, a) in enumerate(self.input_pairs()): # If this is not origin point any more if one_row and row != b: continue self.command_stack.push(DeleteInput(i, self.variable_list)) @Slot(name='on_variable_remove_clicked') def remove_var(self, row: int = -1) -> None: """Remove and reset angle.""" if row == -1: row = self.variable_list.currentRow() if not row > -1: return self.variable_stop.click() self.command_stack.push(DeleteInput(row, self.variable_list)) self.get_back_position() self.solve() def interval(self) -> float: """Return interval value.""" return self.record_interval.value() def input_count(self) -> int: """Use to show input variable count.""" return self.variable_list.count() def input_pairs(self) -> Iterator[Tuple[int, int, float]]: """Back as point number code.""" for row in range(self.variable_list.count()): var = self.variable_list.item(row).text().split('->') p0 = int(var[0].replace('Point', '')) p1 = int(var[1].replace('Point', '')) angle = float(var[2]) yield p0, p1, angle def variable_reload(self) -> None: """Auto check the points and type.""" self.joint_list.clear() for i in range(self.entities_point.rowCount()): type_text = self.entities_point.item(i, 2).text() self.joint_list.addItem(f"[{type_text}] Point{i}") self.variable_value_reset() @Slot(float) def __set_var(self, value: float) -> None: self.dial.set_value(value) @Slot(float) def __update_var(self, value: float) -> None: """Update the value when rotating QDial.""" item = self.variable_list.currentItem() self.dial_spinbox.blockSignals(True) self.dial_spinbox.setValue(value) self.dial_spinbox.blockSignals(False) if item: item_text = item.text().split('->') item_text[-1] = f"{value:.02f}" item.setText('->'.join(item_text)) self.about_to_resolve.emit() if (self.record_start.isChecked() and abs(self.oldVar - value) > self.record_interval.value()): self.main_canvas.record_path() self.oldVar = value def variable_value_reset(self) -> None: """Reset the value of QDial.""" if self.inputs_play_shaft.isActive(): self.variable_play.setChecked(False) self.inputs_play_shaft.stop() self.get_back_position() for i, (p0, p1, a) in enumerate(self.input_pairs()): self.variable_list.item(i).setText('->'.join([ f'Point{p0}', f'Point{p1}', f"{self.vpoints[p0].slope_angle(self.vpoints[p1]):.02f}", ])) self.__dial_ok() self.solve() @Slot(bool, name='on_variable_play_toggled') def __play(self, toggled: bool) -> None: """Triggered when play button was changed.""" self.dial.setEnabled(not toggled) self.dial_spinbox.setEnabled(not toggled) if toggled: self.inputs_play_shaft.start() else: self.inputs_play_shaft.stop() if self.update_pos_option.isChecked(): self.set_coords_as_current() @Slot() def __change_index(self) -> None: """QTimer change index.""" index = self.dial.value() speed = self.variable_speed.value() extreme_rebound = (self.conflict.isVisible() and self.extremeRebound.isChecked()) if extreme_rebound: speed = -speed self.variable_speed.setValue(speed) index += speed * 0.06 * (3 if extreme_rebound else 1) self.dial.set_value(index) @Slot(bool, name='on_record_start_toggled') def __start_record(self, toggled: bool) -> None: """Save to file path data.""" if toggled: self.main_canvas.record_start( int(self.dial_spinbox.maximum() / self.record_interval.value())) return path = self.main_canvas.get_record_path() name, ok = QInputDialog.getText(self, "Recording completed!", "Please input name tag:") i = 0 name = name or f"Record_{i}" while name in self.__path_data: name = f"Record_{i}" i += 1 QMessageBox.information(self, "Record", "The name tag is being used or empty.") self.add_path(name, path) def add_path(self, name: str, path: _Paths) -> None: """Add path function.""" self.command_stack.push( AddPath(self.record_list, name, self.__path_data, path)) self.record_list.setCurrentRow(self.record_list.count() - 1) def load_paths(self, paths: Dict[str, _Paths]) -> None: """Add multiple path.""" for name, path in paths.items(): self.add_path(name, path) @Slot(name='on_record_remove_clicked') def __remove_path(self) -> None: """Remove path data.""" row = self.record_list.currentRow() if not row > 0: return self.command_stack.push( DeletePath(row, self.record_list, self.__path_data)) self.record_list.setCurrentRow(self.record_list.count() - 1) self.reload_canvas() @Slot(QListWidgetItem, name='on_record_list_itemDoubleClicked') def __path_dlg(self, item: QListWidgetItem) -> None: """View path data.""" name = item.text().split(":")[0] try: data = self.__path_data[name] except KeyError: return points_text = ", ".join(f"Point{i}" for i in range(len(data))) if QMessageBox.question(self, "Path data", f"This path data including {points_text}.", (QMessageBox.Save | QMessageBox.Close), QMessageBox.Close) != QMessageBox.Save: return file_name = self.output_to( "path data", ["Comma-Separated Values (*.csv)", "Text file (*.txt)"]) if not file_name: return with open(file_name, 'w', encoding='utf-8', newline='') as stream: writer = csv.writer(stream) for point in data: for coordinate in point: writer.writerow(coordinate) writer.writerow(()) logger.info(f"Output path data: {file_name}") @Slot(QPoint) def __record_list_context_menu(self, p: QPoint) -> None: """Show the context menu. Show path [0], [1], ... Or copy path coordinates. """ row = self.record_list.currentRow() if not row > -1: return showall_action = self.pop_menu_record_list.addAction("Show all") showall_action.index = -1 copy_action = self.pop_menu_record_list.addAction("Copy as new") name = self.record_list.item(row).text().split(':')[0] if name in self.__path_data: data = self.__path_data[name] else: # Auto preview path data = self.main_canvas.path_preview targets = 0 for text in ("Show", "Copy data from"): self.pop_menu_record_list.addSeparator() for i, path in enumerate(data): if len(set(path)) > 1: action = self.pop_menu_record_list.addAction( f"{text} Point{i}") action.index = i targets += 1 copy_action.setEnabled(targets > 0) action = self.pop_menu_record_list.exec_( self.record_list.mapToGlobal(p)) if action is None: self.pop_menu_record_list.clear() return text = action.text() if action == copy_action: # Copy path data num = 0 name_copy = f"{name}_{num}" while name_copy in self.__path_data: name_copy = f"{name}_{num}" num += 1 self.add_path(name_copy, copy(data)) elif text.startswith("Copy data from"): # Copy data to clipboard (csv) QApplication.clipboard().setText('\n'.join( f"[{x}, {y}]," for x, y in data[action.index])) elif text.startswith("Show"): # Switch points enabled status if action.index == -1: self.record_show.setChecked(True) self.main_canvas.set_path_show(action.index) self.pop_menu_record_list.clear() @Slot(bool, name='on_record_show_toggled') def __set_path_show(self, toggled: bool) -> None: """Show all paths or hide.""" self.main_canvas.set_path_show(-1 if toggled else -2) @Slot(int, name='on_record_list_currentRowChanged') def __set_path(self, _=None) -> None: """Reload the canvas when switch the path.""" if not self.record_show.isChecked(): self.record_show.setChecked(True) self.reload_canvas() def current_path(self) -> _Paths: """Return current path data to main canvas. + No path. + Show path data. + Auto preview. """ row = self.record_list.currentRow() if row in {0, -1}: return () path_name = self.record_list.item(row).text().split(':')[0] return self.__path_data.get(path_name, ()) @Slot(name='on_variable_up_clicked') @Slot(name='on_variable_down_clicked') def __set_variable_priority(self) -> None: row = self.variable_list.currentRow() if not row > -1: return item = self.variable_list.currentItem() self.variable_list.insertItem( row + (-1 if self.sender() == self.variable_up else 1), self.variable_list.takeItem(row)) self.variable_list.setCurrentItem(item)
class TrapViewer(QWidget): i = 0 def __init__(self, qnd, images, trap_positions=None, labels=None): QWidget.__init__(self) self.video = images # This is a file object buffer containing the images self.trap_positions = trap_positions self.labels = labels self.videobox = Label(trap_positions, labels) self.videobox.activeframe = images.asarray(key=TrapViewer.i) try: self.videobox.maxintens = int(images.imagej_metadata['max']) self.videobox.maxintens = 15265 print(images.imagej_metadata) except KeyError: self.videobox.maxintens = int(np.max(self.videobox.activeframe)) self.videobox.setGeometry(QtCore.QRect(70, 80, 200, 200)) self.lyt = QVBoxLayout() self.lyt.addWidget(self.videobox, 5) self.setLayout(self.lyt) self.sl = QSlider(Qt.Horizontal) self.sl.setMinimum(0.0) self.sl.setMaximum(self.video.imagej_metadata['frames'] - 1) self.sl.setTickPosition(QSlider.TicksAbove) self.sl.setTracking(True) self.sl.setTickInterval(100) self.sl.valueChanged.connect(self.whenslidechanges) self.frame_counter = QDoubleSpinBox() self.frame = self.videobox.activeframe self.frame_counter.setSingleStep(1) self.frame_counter.setRange(self.sl.minimum(), self.sl.maximum() - 1) self.frame_counter.valueChanged.connect(self.sl.setValue) self.frame_counter.valueChanged.connect(self.video_time_update) self.video_time = QDoubleSpinBox() self.video_time.setSingleStep(30) self.video_time.setRange(self.sl.minimum(), 30 * self.sl.maximum() - 1) self.frameratetimer = QTimer() self.frameratetimer.setInterval(50) self.frameratetimer.timeout.connect(self.update_display) self.play_button = QPushButton('Play Video') self.play_button.clicked.connect(self.frameratetimer.start) self.stop_button = QPushButton('Stop Video') self.stop_button.clicked.connect(self.frameratetimer.stop) self.sl.valueChanged.connect(self.whenslidechanges) self.lyt.addWidget(self.play_button, 0) self.lyt.addWidget(self.stop_button, 1) self.lyt.addWidget(self.sl, 2) self.lyt.addWidget(self.frame_counter, 3) self.lyt.addWidget(self.video_time, 4) self.show() def update_display(self): self.frame = self.video.asarray(key=TrapViewer.i) self.videobox.activeframe = self.frame self.videobox.update() self.frame_counter.setValue(float(TrapViewer.i)) if TrapViewer.i < self.video.imagej_metadata['frames']: TrapViewer.i += 1 def whenslidechanges(self): if self.frameratetimer.isActive(): self.frameratetimer.stop() TrapViewer.i = self.sl.value() self.update_display() TrapViewer.i -= 1 self.frameratetimer.start() else: TrapViewer.i = self.sl.value() self.update_display() TrapViewer.i -= 1 def video_time_update(self): self.video_time.setValue(30 * self.frame_counter.value())
class ScriptRunner(object): """ Runs a script that interacts with a widget (tests it). If the script is a python generator then after each iteration controls returns to the QApplication's event loop. Generator scripts can yield a positive number. It is treated as the number of seconds before the next iteration is called. During the wait time the event loop is running. """ def __init__(self, script, widget=None, close_on_finish=True, pause=0, is_cli=False): """ Initialise a runner. :param script: The script to run. :param widget: The widget to test. :param close_on_finish: If true close the widget after the script has finished. :param is_cli: If true the script is to be run from a command line tool. Exceptions are treated slightly differently in this case. """ app = get_application() self.script = script self.widget = widget self.close_on_finish = close_on_finish self.pause = pause self.is_cli = is_cli self.error = None self.script_iter = [None] self.pause_timer = QTimer(app) self.pause_timer.setSingleShot(True) self.script_timer = QTimer(app) def run(self): ret = run_script(self.script, self.widget) if isinstance(ret, Exception): raise ret self.script_iter = [iter(ret) if inspect.isgenerator(ret) else None] if self.pause != 0: self.script_timer.setInterval(self.pause * 1000) # Zero-timeout timer runs script_runner() between Qt events self.script_timer.timeout.connect(self, Qt.QueuedConnection) QMetaObject.invokeMethod(self.script_timer, 'start', Qt.QueuedConnection) def __call__(self): app = get_application() if not self.pause_timer.isActive(): try: script_iter = self.script_iter[-1] if script_iter is None: if self.close_on_finish: app.closeAllWindows() app.exit() return # Run test script until the next 'yield' try: ret = next(script_iter) except ValueError: return while ret is not None: if inspect.isgenerator(ret): self.script_iter.append(ret) ret = None elif isinstance(ret, six.integer_types) or isinstance(ret, float): # Start non-blocking pause in seconds self.pause_timer.start(int(ret * 1000)) ret = None else: ret = ret() except StopIteration: if len(self.script_iter) > 1: self.script_iter.pop() else: self.script_iter = [None] self.script_timer.stop() if self.close_on_finish: app.closeAllWindows() app.exit(0) except Exception as e: self.script_iter = [None] traceback.print_exc() if self.close_on_finish: app.exit(1) self.error = e
class QtLayerList(QScrollArea): def __init__(self, layers): super().__init__() self.layers = layers self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.vbox_layout.setSpacing(2) self.centers = [] # Create a timer to be used for autoscrolling the layers list up and # down when dragging a layer near the end of the displayed area self.dragTimer = QTimer() self.dragTimer.setSingleShot(False) self.dragTimer.setInterval(20) self.dragTimer.timeout.connect(self._force_scroll) self._scroll_up = True self._min_scroll_region = 24 self.setAcceptDrops(True) self.setToolTip('Layer list') self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.layers.events.added.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) self.drag_start_position = np.zeros(2) self.drag_name = None def _add(self, event): """Insert widget for layer `event.item` at index `event.index`.""" layer = event.item total = len(self.layers) index = 2 * (total - event.index) - 1 widget = QtLayerWidget(layer) self.vbox_layout.insertWidget(index, widget) self.vbox_layout.insertWidget(index + 1, QtDivider()) layer.events.select.connect(self._scroll_on_select) def _remove(self, event): """Remove widget for layer at index `event.index`.""" layer_index = event.index total = len(self.layers) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) + 1 widget = self.vbox_layout.itemAt(index).widget() divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(widget) widget.deleteLater() self.vbox_layout.removeWidget(divider) divider.deleteLater() def _reorder(self, event=None): """Reorders list of layer widgets by looping through all widgets in list sequentially removing them and inserting them into the correct place in final list. """ total = len(self.layers) # Create list of the current property and divider widgets widgets = [ self.vbox_layout.itemAt(i + 1).widget() for i in range(2 * total) ] # Take every other widget to ignore the dividers and get just the # property widgets indices = [ self.layers.index(w.layer) for i, w in enumerate(widgets) if i % 2 == 0 ] # Move through the layers in order for i in range(total): # Find index of property widget in list of the current layer index = 2 * indices.index(i) widget = widgets[index] divider = widgets[index + 1] # Check if current index does not match new index index_current = self.vbox_layout.indexOf(widget) index_new = 2 * (total - i) - 1 if index_current != index_new: # Remove that property widget and divider self.vbox_layout.removeWidget(widget) self.vbox_layout.removeWidget(divider) # Insert the property widget and divider into new location self.vbox_layout.insertWidget(index_new, widget) self.vbox_layout.insertWidget(index_new + 1, divider) def _force_scroll(self): """Force the scroll bar to automattically scroll either up or down.""" cur_value = self.verticalScrollBar().value() if self._scroll_up: new_value = cur_value - self.verticalScrollBar().singleStep() / 4 if new_value < 0: new_value = 0 self.verticalScrollBar().setValue(new_value) else: new_value = cur_value + self.verticalScrollBar().singleStep() / 4 if new_value > self.verticalScrollBar().maximum(): new_value = self.verticalScrollBar().maximum() self.verticalScrollBar().setValue(new_value) def _scroll_on_select(self, event): """Scroll to ensure that the currently selected layer is visible.""" layer = event.source self._ensure_visible(layer) def _ensure_visible(self, layer): """Ensure layer widget for at particular layer is visible.""" total = len(self.layers) layer_index = self.layers.index(layer) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) - 1 widget = self.vbox_layout.itemAt(index).widget() self.ensureWidgetVisible(widget) def keyPressEvent(self, event): event.ignore() def keyReleaseEvent(self, event): event.ignore() def mousePressEvent(self, event): # Check if mouse press happens on a layer properties widget or # a child of such a widget. If not, the press has happended on the # Layers Widget itself and should be ignored. widget = self.childAt(event.pos()) layer = ( getattr(widget, 'layer', None) or getattr(widget.parentWidget(), 'layer', None) or getattr(widget.parentWidget().parentWidget(), 'layer', None) ) if layer is not None: self.drag_start_position = np.array( [event.pos().x(), event.pos().y()] ) self.drag_name = layer.name else: self.drag_name = None def mouseReleaseEvent(self, event): if self.drag_name is None: # Unselect all the layers if not dragging a layer self.layers.unselect_all() return modifiers = event.modifiers() layer = self.layers[self.drag_name] if modifiers == Qt.ShiftModifier: # If shift select all layers in between currently selected one and # clicked one index = self.layers.index(layer) lastSelected = None for i in range(len(self.layers)): if self.layers[i].selected: lastSelected = i r = [index, lastSelected] r.sort() for i in range(r[0], r[1] + 1): self.layers[i].selected = True elif modifiers == Qt.ControlModifier: # If control click toggle selected state layer.selected = not layer.selected else: # If otherwise unselect all and leave clicked one selected self.layers.unselect_all(ignore=layer) layer.selected = True def mouseMoveEvent(self, event): position = np.array([event.pos().x(), event.pos().y()]) distance = np.linalg.norm(position - self.drag_start_position) if ( distance < QApplication.startDragDistance() or self.drag_name is None ): return mimeData = QMimeData() mimeData.setText(self.drag_name) drag = QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) drag.exec_() if self.drag_name is not None: index = self.layers.index(self.drag_name) layer = self.layers[index] self._ensure_visible(layer) def dragLeaveEvent(self, event): """Unselects layer dividers.""" event.ignore() self.dragTimer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): if event.source() == self: event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [ (divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1) ] else: event.ignore() def dragMoveEvent(self, event): """Set the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. """ max_height = self.frameGeometry().height() if ( event.pos().y() < self._min_scroll_region and not self.dragTimer.isActive() ): self._scroll_up = True self.dragTimer.start() elif ( event.pos().y() > max_height - self._min_scroll_region and not self.dragTimer.isActive() ): self._scroll_up = False self.dragTimer.start() elif ( self.dragTimer.isActive() and event.pos().y() >= self._min_scroll_region and event.pos().y() <= max_height - self._min_scroll_region ): self.dragTimer.stop() # Determine which widget center is the mouse currently closed to cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) # Determine the current location of the widget being dragged total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self.drag_name) # If the widget being dragged hasn't moved above or below any other # widgets then don't highlight any dividers selected = not (insert == index) and not (insert - 1 == index) # Set the selected state of all the dividers for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(selected) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): if self.dragTimer.isActive(): self.dragTimer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self.drag_name) if index != insert and index + 1 != insert: if insert >= index: insert -= 1 self.layers.move_selected(index, insert) event.accept()
class QtFrameRate(QLabel): """A frame rate label with green/yellow/red LEDs. The LED values are logarithmic, so a lot of detail between 60Hz and 10Hz but then the highest LED is many seconds long. """ def __init__(self): super().__init__() self.leds = LedState() # The per-LED config and state. # The last time we were updated, either from mouse move or our # timer. We update _last_time in both cases. self._last_time: Optional[float] = None # The bitmap image we draw into. self._image = np.zeros(BITMAP_SHAPE, dtype=np.uint8) # We animate on camera movements, but then we use a timer to # animate the display. When all the LEDs go off, we stop the timer # so that we use zero CPU until another camera movement. self._timer = QTimer() self._timer.setSingleShot(False) self._timer.setInterval(33) self._timer.timeout.connect(self._on_timer) # _print_calibration() # Debugging. def _on_timer(self) -> None: """Animate the LEDs.""" now = time.time() self._draw(now) # Just animate and draw, no new peak. # Stop timer if nothing more to animation, save CPU. if self.leds.all_off(): self._timer.stop() def _draw(self, now: float) -> None: """Animate the LEDs. Parameters ---------- now : float The current time in seconds. """ self.leds.update(now) # Animates the LEDs. self._update_image(now) # Draws our internal self._image self._update_bitmap() # Writes self._image into the QLabel bitmap. # We always update _last_time whether this was from a camera move # or the timer. This is the right thing to do since in either case # it means a frame was drawn. self._last_time = now def on_camera_move(self) -> None: """Update our display to show the new framerate.""" # Only count this frame if the timer is active. This avoids # displaying a potentially super long frame since it might have # been many seconds or minutes since the last camera movement. # # Ideally we should display the draw time of even that first frame, # but there's no easy/obvious way to do that today. And this will # show everthing except one frame. first_time = self._last_time is None use_delta = self._timer.isActive() and not first_time now = time.time() if use_delta: delta_seconds = now - self._last_time self.leds.set_peak(now, delta_seconds) self._draw(now) # Draw the whole meter. # Since there was activity, we need to start the timer so we can # animate the decay of the LEDs. The timer will be shut off when # all the LEDs go idle. self._timer.start() def _update_image(self, now: float) -> None: """Update our self._image with the latest meter display. Parameters ---------- now : float The current time in seconds. """ self._image.fill(0) # Start fresh each time. # Get colors with latest alpha values accord to decay. colors = self.leds.get_colors(now) # Draw each segment with the right color and alpha (due to decay). for index in range(NUM_SEGMENTS): x0 = int(index * SEGMENT_SPACING) x1 = int(x0 + SEGMENT_WIDTH) y0, y1 = 0, BITMAP_SHAPE[0] # The whole height of the bitmap. self._image[y0:y1, x0:x1] = colors[index] def _update_bitmap(self) -> None: """Update the bitmap with latest image data.""" height, width = BITMAP_SHAPE[:2] image = QImage(self._image, width, height, QImage.Format_RGBA8888) self.setPixmap(QPixmap.fromImage(image))
class InputsWidget(QWidget, Ui_Form): """There has following functions: + Function of mechanism variables settings. + Path recording. """ __paths: Dict[str, _Paths] __slider_paths: Dict[str, _SliderPaths] about_to_resolve = Signal() def __init__(self, parent: MainWindowBase): super(InputsWidget, self).__init__(parent) self.setupUi(self) # parent's function pointer self.free_move_button = parent.free_move_button self.entities_point = parent.entities_point self.entities_link = parent.entities_link self.vpoints = parent.vpoint_list self.vlinks = parent.vlink_list self.main_canvas = parent.main_canvas self.solve = parent.solve self.reload_canvas = parent.reload_canvas self.output_to = parent.output_to self.conflict = parent.conflict self.dof = parent.dof self.right_input = parent.right_input self.command_stack = parent.cmd_stack self.set_coords_as_current = parent.set_coords_as_current self.get_back_position = parent.get_back_position # Angle panel self.dial = QRotatableView(self) self.dial.setStatusTip("Input widget of rotatable joint.") self.dial.setEnabled(False) self.dial.value_changed.connect(self.__update_var) self.dial_spinbox.valueChanged.connect(self.__set_var) self.inputs_dial_layout.insertWidget(0, self.dial) # Play button self.variable_stop.clicked.connect(self.variable_value_reset) # Timer for play button self.inputs_play_shaft = QTimer() self.inputs_play_shaft.setInterval(10) self.inputs_play_shaft.timeout.connect(self.__change_index) # Change the point coordinates with current position self.update_pos.clicked.connect(self.set_coords_as_current) # Record list self.record_list.blockSignals(True) self.record_list.addItem(_AUTO_PATH) self.record_list.setCurrentRow(0) self.record_list.blockSignals(False) self.__paths = {_AUTO_PATH: self.main_canvas.path_preview} self.__slider_paths = {_AUTO_PATH: self.main_canvas.slider_path_preview} def slot(widget: QCheckBox) -> Callable[[int], None]: @Slot(int) def func(ind: int) -> None: widget.setEnabled(ind >= 0 and self.vpoints[ind].type != VJoint.R) return func # Slot option self.plot_joint.currentIndexChanged.connect(slot(self.plot_joint_slot)) self.wrt_joint.currentIndexChanged.connect(slot(self.wrt_joint_slot)) def clear(self) -> None: """Clear function to reset widget status.""" self.__paths = {_AUTO_PATH: self.__paths[_AUTO_PATH]} for _ in range(self.record_list.count() - 1): self.record_list.takeItem(1) self.variable_list.clear() def __set_angle_mode(self) -> None: """Change to angle input.""" self.dial.set_minimum(0) self.dial.set_maximum(360) self.dial_spinbox.setMinimum(0) self.dial_spinbox.setMaximum(360) def __set_unit_mode(self) -> None: """Change to unit input.""" self.dial.set_minimum(-500) self.dial.set_maximum(500) self.dial_spinbox.setMinimum(-500) self.dial_spinbox.setMaximum(500) def paths(self) -> Mapping[str, _Paths]: """Return current path data.""" return _no_auto_path(self.__paths) def slider_paths(self) -> Mapping[str, _SliderPaths]: """Return current path data.""" return _no_auto_path(self.__slider_paths) @Slot(tuple) def set_selection(self, selections: Sequence[int]) -> None: """Set one selection from canvas.""" self.joint_list.setCurrentRow(selections[0]) @Slot() def clear_selection(self) -> None: """Clear the points selection.""" self.driver_list.clear() self.joint_list.setCurrentRow(-1) @Slot(int, name='on_joint_list_currentRowChanged') def __update_relate_points(self, _=None) -> None: """Change the point row from input widget.""" self.driver_list.clear() item: Optional[QListWidgetItem] = self.joint_list.currentItem() if item is None: return p0 = _variable_int(item.text()) base_point = self.vpoints[p0] type_int = base_point.type if type_int == VJoint.R: for i, vpoint in enumerate(self.vpoints): if i == p0: continue if base_point.same_link(vpoint): if base_point.grounded() and vpoint.grounded(): continue self.driver_list.addItem(f"[{vpoint.type_str}] Point{i}") elif type_int in {VJoint.P, VJoint.RP}: self.driver_list.addItem(f"[{base_point.type_str}] Point{p0}") @Slot(int, name='on_driver_list_currentRowChanged') def __set_add_var_enabled(self, _=None) -> None: """Set enable of 'add variable' button.""" driver = self.driver_list.currentIndex() self.variable_add.setEnabled(driver != -1) @Slot(name='on_variable_add_clicked') def __add_inputs_variable( self, p0: Optional[int] = None, p1: Optional[int] = None ) -> None: """Add variable with '->' sign.""" if p0 is None: item: Optional[QListWidgetItem] = self.joint_list.currentItem() if item is None: return p0 = _variable_int(item.text()) if p1 is None: item = self.driver_list.currentItem() if item is None: return p1 = _variable_int(item.text()) # Check DOF if self.dof() <= self.input_count(): QMessageBox.warning( self, "Wrong DOF", "The number of variable must no more than degrees of freedom." ) return # Check same link if not self.vpoints[p0].same_link(self.vpoints[p1]): QMessageBox.warning( self, "Wrong pair", "The base point and driver point should at the same link." ) return # Check repeated pairs for p0_, p1_, _ in self.input_pairs(): if {p0, p1} == {p0_, p1_} and self.vpoints[p0].type == VJoint.R: QMessageBox.warning( self, "Wrong pair", "There already have a same pair." ) return if p0 == p1: # One joint by offset value = self.vpoints[p0].true_offset() else: # Two joints by angle value = self.vpoints[p0].slope_angle(self.vpoints[p1]) self.command_stack.push(AddInput('->'.join(( f'Point{p0}', f"Point{p1}", f"{value:.02f}", )), self.variable_list)) def add_inputs_variables(self, variables: _Vars) -> None: """Add from database.""" for p0, p1 in variables: self.__add_inputs_variable(p0, p1) @Slot(QListWidgetItem, name='on_variable_list_itemClicked') def __dial_ok(self, _=None) -> None: """Set the angle of base link and drive link.""" if self.inputs_play_shaft.isActive(): return row = self.variable_list.currentRow() enabled = row > -1 is_rotatable = ( enabled and not self.free_move_button.isChecked() and self.right_input() ) self.dial.setEnabled(is_rotatable) self.dial_spinbox.setEnabled(is_rotatable) self.oldVar = self.dial.value() self.variable_play.setEnabled(is_rotatable) self.variable_speed.setEnabled(is_rotatable) item: Optional[QListWidgetItem] = self.variable_list.currentItem() if item is None: return expr = item.text().split('->') p0 = int(expr[0].replace('Point', '')) p1 = int(expr[1].replace('Point', '')) value = float(expr[2]) if p0 == p1: self.__set_unit_mode() else: self.__set_angle_mode() self.dial.set_value(value if enabled else 0) def variable_excluding(self, row: Optional[int] = None) -> None: """Remove variable if the point was been deleted. Default: all.""" one_row: bool = row is not None for i, (b, d, _) in enumerate(self.input_pairs()): # If this is not origin point any more if one_row and row != b: continue self.command_stack.push(DeleteInput(i, self.variable_list)) @Slot(name='on_variable_remove_clicked') def remove_var(self, row: int = -1) -> None: """Remove and reset angle.""" if row == -1: row = self.variable_list.currentRow() if not row > -1: return self.variable_stop.click() self.command_stack.push(DeleteInput(row, self.variable_list)) self.get_back_position() self.solve() def interval(self) -> float: """Return interval value.""" return self.record_interval.value() def input_count(self) -> int: """Use to show input variable count.""" return self.variable_list.count() def input_pairs(self) -> Iterator[Tuple[int, int, float]]: """Back as point number code.""" for row in range(self.variable_list.count()): var = self.variable_list.item(row).text().split('->') p0 = int(var[0].replace('Point', '')) p1 = int(var[1].replace('Point', '')) angle = float(var[2]) yield p0, p1, angle def variable_reload(self) -> None: """Auto check the points and type.""" self.joint_list.clear() self.plot_joint.clear() self.wrt_joint.clear() for i in range(self.entities_point.rowCount()): type_text = self.entities_point.item(i, 2).text() for w in [self.joint_list, self.plot_joint, self.wrt_joint]: w.addItem(f"[{type_text}] Point{i}") self.variable_value_reset() @Slot(float) def __set_var(self, value: float) -> None: self.dial.set_value(value) @Slot(float) def __update_var(self, value: float) -> None: """Update the value when rotating QDial.""" item = self.variable_list.currentItem() self.dial_spinbox.blockSignals(True) self.dial_spinbox.setValue(value) self.dial_spinbox.blockSignals(False) if item: item_text = item.text().split('->') item_text[-1] = f"{value:.02f}" item.setText('->'.join(item_text)) self.about_to_resolve.emit() if ( self.record_start.isChecked() and abs(self.oldVar - value) > self.record_interval.value() ): self.main_canvas.record_path() self.oldVar = value def variable_value_reset(self) -> None: """Reset the value of QDial.""" if self.inputs_play_shaft.isActive(): self.variable_play.setChecked(False) self.inputs_play_shaft.stop() self.get_back_position() for i, (p0, p1, _) in enumerate(self.input_pairs()): self.variable_list.item(i).setText('->'.join([ f'Point{p0}', f'Point{p1}', f"{self.vpoints[p0].slope_angle(self.vpoints[p1]):.02f}", ])) self.__dial_ok() self.solve() @Slot(bool, name='on_variable_play_toggled') def __play(self, toggled: bool) -> None: """Triggered when play button was changed.""" self.dial.setEnabled(not toggled) self.dial_spinbox.setEnabled(not toggled) if toggled: self.inputs_play_shaft.start() else: self.inputs_play_shaft.stop() @Slot() def __change_index(self) -> None: """QTimer change index.""" index = self.dial.value() speed = self.variable_speed.value() extreme_rebound = ( self.conflict.isVisible() and self.extremeRebound.isChecked() ) if extreme_rebound: speed = -speed self.variable_speed.setValue(speed) index += speed * 0.06 * (3 if extreme_rebound else 1) self.dial.set_value(index) @Slot(bool, name='on_record_start_toggled') def __start_record(self, toggled: bool) -> None: """Save to file path data.""" if toggled: self.main_canvas.record_start(int( self.dial_spinbox.maximum() / self.record_interval.value() )) return path, path_slider = self.main_canvas.get_record_path() name, ok = QInputDialog.getText( self, "Recording completed!", "Please input name tag:" ) i = 0 name = name or f"Record_{i}" while name in self.__paths: name = f"Record_{i}" i += 1 QMessageBox.information(self, "Record", "The name tag is being used or empty.") self.add_path(name, path, path_slider) def add_path(self, name: str, path: _Paths, slider: _SliderPaths) -> None: """Add path function.""" self.command_stack.push(AddPath( self.record_list, name, self.__paths, self.__slider_paths, path, slider )) self.record_list.setCurrentRow(self.record_list.count() - 1) def load_paths(self, paths: Mapping[str, _Paths], slider_paths: Mapping[str, _SliderPaths]) -> None: """Add multiple paths.""" for name, path in paths.items(): self.add_path(name, path, slider_paths.get(name, {})) @Slot(name='on_record_remove_clicked') def __remove_path(self) -> None: """Remove path data.""" row = self.record_list.currentRow() if not row > 0: return self.command_stack.push(DeletePath( row, self.record_list, self.__paths, self.__slider_paths )) self.record_list.setCurrentRow(self.record_list.count() - 1) self.reload_canvas() @Slot(QListWidgetItem, name='on_record_list_itemDoubleClicked') def __path_dlg(self, item: QListWidgetItem) -> None: """View path data.""" name = item.text().split(":", maxsplit=1)[0] try: paths = self.__paths[name] except KeyError: return points_text = ", ".join(f"Point{i}" for i in range(len(paths))) if QMessageBox.question( self, "Path data", f"This path data including {points_text}.", (QMessageBox.Save | QMessageBox.Close), QMessageBox.Close ) != QMessageBox.Save: return file_name = self.output_to( "path data", ["Comma-Separated Values (*.csv)", "Text file (*.txt)"] ) if not file_name: return with open(file_name, 'w+', encoding='utf-8', newline='') as stream: w = writer(stream) for path in paths: for point in path: w.writerow(point) w.writerow(()) logger.info(f"Output path data: {file_name}") def __current_path_name(self) -> str: """Return the current path name.""" return self.record_list.currentItem().text().split(':', maxsplit=1)[0] @Slot(name='on_copy_path_clicked') def __copy_path(self): """Copy path from record list.""" name = self.__current_path_name() num = 0 name_copy = f"{name}_{num}" while name_copy in self.__paths: name_copy = f"{name}_{num}" num += 1 self.add_path(name_copy, copy(self.__paths[name]), {}) @Slot(name='on_cp_data_button_clicked') def __copy_path_data(self) -> None: """Copy current path data to clipboard.""" data = self.__paths[self.__current_path_name()] if not data: return QApplication.clipboard().setText('\n'.join( f"[{x}, {y}]," for x, y in data[self.plot_joint.currentIndex()] )) @Slot(name='on_show_button_clicked') def __show_path(self) -> None: """Show specified path.""" self.main_canvas.set_path_show(self.plot_joint.currentIndex()) @Slot(name='on_show_all_button_clicked') def __show_all_path(self) -> None: """Show all paths.""" self.record_show.setChecked(True) self.main_canvas.set_path_show(-1) @Slot(bool, name='on_record_show_toggled') def __set_path_show(self, toggled: bool) -> None: """Show all paths or hide.""" self.main_canvas.set_path_show(-1 if toggled else -2) @Slot(int, name='on_record_list_currentRowChanged') def __set_path(self, _=None) -> None: """Reload the canvas when switch the path.""" if not self.record_show.isChecked(): self.record_show.setChecked(True) self.reload_canvas() def current_path(self) -> Tuple[_Paths, _SliderPaths]: """Return current path data to main canvas. + No path. + Show path data. + Auto preview. """ row = self.record_list.currentRow() if row in {0, -1}: return (), {} name = self.record_list.item(row).text().split(':')[0] return self.__paths.get(name, ()), self.__slider_paths.get(name, {}) @Slot(name='on_variable_up_clicked') @Slot(name='on_variable_down_clicked') def __set_variable_priority(self) -> None: row = self.variable_list.currentRow() if not row > -1: return item = self.variable_list.currentItem() self.variable_list.insertItem( row + (-1 if self.sender() == self.variable_up else 1), self.variable_list.takeItem(row) ) self.variable_list.setCurrentItem(item) @Slot(name='on_animate_button_clicked') def __animate(self) -> None: """Make a motion animation.""" name = self.__current_path_name() data = self.__paths.get(name, []) if not data: return dlg = AnimateDialog(self.vpoints, self.vlinks, data, self.__slider_paths.get(name, {}), self.main_canvas.monochrome, self) dlg.show() dlg.exec_() dlg.deleteLater() @Slot(name='on_plot_button_clicked') def __plot(self) -> None: """Plot the data. Show the X and Y axes as two line.""" joint = self.plot_joint.currentIndex() name = self.__current_path_name() data = self.__paths.get(name, []) slider_data = self.__slider_paths.get(name, {}) if not data: return if self.plot_joint_slot.isChecked(): pos = array(slider_data.get(joint, [])) else: pos = array(data[joint]) if self.wrt_label.isChecked(): joint_wrt = self.wrt_joint.currentIndex() if self.wrt_joint_slot.isChecked(): pos[:] -= array(slider_data.get(joint_wrt, [])) else: pos[:] -= array(data[joint_wrt]) plot = {} row = 0 for button, value in [ (self.plot_pos, lambda: pos), (self.plot_vel, vel := lambda: derivative(pos)), (self.plot_acc, acc := lambda: derivative(vel())), (self.plot_jerk, lambda: derivative(acc())), (self.plot_curvature, cur := lambda: curvature(data[joint])), (self.plot_signature, lambda: path_signature(cur())), (self.plot_norm, lambda: norm_path(pos)), (self.plot_norm_pca, lambda: norm_pca(pos)), (self.plot_fourier, lambda: _fourier(pos)), ]: # type: QCheckBox, Callable[[], ndarray] if button.isChecked(): row += 1 plot[button.text()] = value() if row < 1: QMessageBox.warning(self, "No target", "No any plotting target.") return polar = self.p_coord_sys.isChecked() col = 1 if polar: row, col = col, row dlg = DataChartDialog(self, "Analysis", row, col, polar) dlg.setWindowIcon(QIcon(QPixmap("icons:formula.png"))) ax = dlg.ax() for p, (title, xy) in enumerate(plot.items()): ax_i = ax[p] ax_i.set_title(title) if title == "Path Signature": ax_i.plot(xy[:, 0], xy[:, 1]) ax_i.set_ylabel(r"$\kappa$") ax_i.set_xlabel(r"$\int|\kappa|dt$") elif xy.ndim == 2: x = xy[:, 0] y = xy[:, 1] if self.c_coord_sys.isChecked(): ax_i.plot(x, label='x') ax_i.plot(y, label='y') ax_i.legend() else: r = hypot(x, y) theta = arctan2(y, x) ax_i.plot(theta, r, linewidth=5) else: ax_i.plot(xy) dlg.set_margin(0.2) dlg.show() dlg.exec_() dlg.deleteLater()
class ProgressView(QWidget): """ :type batch_manager: CalculationManager """ def __init__(self, parent, batch_manager): QWidget.__init__(self, parent) self.calculation_manager = batch_manager self.whole_progress = QProgressBar(self) self.whole_progress.setMinimum(0) self.whole_progress.setMaximum(1) self.whole_progress.setFormat("%v of %m") self.whole_progress.setTextVisible(True) self.part_progress = QProgressBar(self) self.part_progress.setMinimum(0) self.part_progress.setMaximum(1) self.part_progress.setFormat("%v of %m") self.whole_label = QLabel("All batch progress:", self) self.part_label = QLabel("Single batch progress:", self) self.logs = ExceptionList(self) self.logs.setToolTip("Logs") self.task_que = QListWidget() self.process_num_timer = QTimer() self.process_num_timer.setInterval(1000) self.process_num_timer.setSingleShot(True) self.process_num_timer.timeout.connect(self.change_number_of_workers) self.number_of_process = QSpinBox(self) self.number_of_process.setRange(1, multiprocessing.cpu_count()) self.number_of_process.setValue(1) self.number_of_process.setToolTip( "Number of process used in batch calculation") self.number_of_process.valueChanged.connect( self.process_num_timer_start) layout = QGridLayout() layout.addWidget(self.whole_label, 0, 0, Qt.AlignRight) layout.addWidget(self.whole_progress, 0, 1, 1, 2) layout.addWidget(self.part_label, 1, 0, Qt.AlignRight) layout.addWidget(self.part_progress, 1, 1, 1, 2) lab = QLabel("Number of process:") lab.setToolTip("Number of process used in batch calculation") layout.addWidget(lab, 2, 0) layout.addWidget(self.number_of_process, 2, 1) layout.addWidget(self.logs, 3, 0, 1, 3) layout.addWidget(self.task_que, 0, 4, 0, 1) layout.setColumnMinimumWidth(2, 10) layout.setColumnStretch(2, 1) self.setLayout(layout) self.preview_timer = QTimer() self.preview_timer.setInterval(1000) self.preview_timer.timeout.connect(self.update_info) def new_task(self): self.whole_progress.setMaximum( self.calculation_manager.calculation_size) if not self.preview_timer.isActive(): self.update_info() self.preview_timer.start() def update_info(self): res = self.calculation_manager.get_results() for el in res.errors: if el[0]: QListWidgetItem(el[0], self.logs) ExceptionListItem(el[1], self.logs) if (state_store.report_errors and parsed_version.is_devrelease and not isinstance(el[1][0], SegmentationLimitException) and isinstance(el[1][1], tuple)): with sentry_sdk.push_scope() as scope: scope.set_tag("auto_report", "true") sentry_sdk.capture_event(el[1][1][0]) self.whole_progress.setValue(res.global_counter) working_search = True for i, (progress, total) in enumerate(res.jobs_status): if working_search and progress != total: self.part_progress.setMaximum(total) self.part_progress.setValue(progress) working_search = False if i < self.task_que.count(): item = self.task_que.item(i) item.setText("Task {} ({}/{})".format(i, progress, total)) else: self.task_que.addItem("Task {} ({}/{})".format( i, progress, total)) if not self.calculation_manager.has_work: print( "[ProgressView.update_info]", self.calculation_manager.has_work, self.calculation_manager.batch_manager.has_work, self.calculation_manager.writer.writing_finished(), ) self.part_progress.setValue(self.part_progress.maximum()) self.preview_timer.stop() logging.info("Progress stop") def process_num_timer_start(self): self.process_num_timer.start() def update_progress(self, total_progress, part_progress): self.whole_progress.setValue(total_progress) self.part_progress.setValue(part_progress) def set_total_size(self, size): self.whole_progress.setMaximum(size) def set_part_size(self, size): self.part_progress.setMaximum(size) def change_number_of_workers(self): self.calculation_manager.set_number_of_workers( self.number_of_process.value())
class PyDMTerminator(QLabel, PyDMPrimitiveWidget): """ A watchdog widget to close a window after X seconds of inactivity. """ designer_icon = QIcon(get_icon_file("terminator.png")) def __init__(self, parent=None, timeout=60, *args, **kwargs): super(PyDMTerminator, self).__init__(parent=parent, *args, **kwargs) self.setText("") self._hook_setup = False self._timeout = 60 self._time_rem_ms = 0 self._timer = QTimer() self._timer.timeout.connect(self.handle_timeout) self._timer.setSingleShot(True) self._window = None if timeout and timeout > 0: self.timeout = timeout else: self._reset() self._setup_activity_hook() self._update_label() def _find_window(self): """ Finds the first window available starting from this widget's parent Returns ------- QWidget """ # check buffer if self._window: return self._window # go fish w = self.parent() while w is not None: if w.isWindow(): return w w = w.parent() # we couldn't find it return None def _setup_activity_hook(self): if is_qt_designer(): return logger.debug('Setup Hook') if self._hook_setup: logger.debug('Setup Hook Already there') return self._window = self._find_window() logger.debug('Install event filter at window') # We must install the event filter in the application otherwise # it won't stop when typing or moving over other widgets or even # the PyDM main window if in use. QApplication.instance().installEventFilter(self) self._hook_setup = True def eventFilter(self, obj, ev): if ev.type() in (QEvent.MouseMove, QEvent.KeyPress, QEvent.KeyRelease): self.reset() return super(PyDMTerminator, self).eventFilter(obj, ev) def reset(self): if self._time_rem_ms != self._timeout * 1000: self._time_rem_ms = self._timeout * 1000 self._update_label() self.stop() self.start() def start(self): if is_qt_designer(): return interval = SLOW_TIMER_INTERVAL if self._time_rem_ms < 60 * 1000: interval = FAST_TIMER_INTERVAL self._timer.setInterval(interval) if not self._timer.isActive(): self._timer.start() def stop(self): if self._timer.isActive(): self._timer.stop() def _get_time_text(self, value): """ Converts value in seconds into a text for days, hours, minutes and seconds remaining. Parameters ---------- value : int The value in seconds to be converted Returns ------- str """ def time_msg(unit, val): return "{} {}{}".format(val, unit, "s" if val > 1 else "") units = ["day", "hour", "minute", "second"] scale = [86400, 3600, 60, 1] values = [0, 0, 0, 0] rem = value for idx, sc in enumerate(scale): val_scaled, rem = int(rem // sc), rem % sc if val_scaled >= 1: val_scaled = math.ceil(val_scaled + (rem / sc)) values[idx] = val_scaled break values[idx] = val_scaled time_items = [] for idx, un in enumerate(units): v = values[idx] if v > 0: time_items.append(time_msg(un, v)) return ", ".join(time_items) def _update_label(self): """Updates the label text with the remaining time.""" rem_time_s = self._time_rem_ms / 1000.0 text = self._get_time_text(rem_time_s) self.setText("This screen will close in {}.".format(text)) @Property(int) def timeout(self): """ Timeout in seconds. Returns ------- int """ return self._timeout @timeout.setter def timeout(self, seconds): self.stop() if seconds and seconds > 0: self._timeout = seconds self.reset() def handle_timeout(self): """ Handles the timeout event for the timer. Decreases the time remaining counter until 0 and when it is time, cleans up the event filter and closes the window. """ if is_qt_designer(): return # Decrease remaining time self._time_rem_ms -= self._timer.interval() # Update screen label with new remaining time self._update_label() if self._time_rem_ms > 0: self.start() return QApplication.instance().removeEventFilter(self) if self._window: logger.debug('Time to close the window') self._window.close() msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText( "Your window was closed due to inactivity for {}.".format( self._get_time_text(self.timeout))) msg.setStandardButtons(QMessageBox.Ok) msg.setDefaultButton(QMessageBox.Ok) msg.exec_()
class QtPoll(QObject): """Polls anything once per frame via an event. QtPoll was first created for VispyTiledImageLayer. It polls the visual when the camera moves. However, we also want visuals to keep loading chunks even when the camera stops. We want the visual to finish up anything that was in progress. Before it goes fully idle. QtPoll will poll those visuals using a timer. If the visual says the event was "handled" it means the visual has more work to do. If that happens, QtPoll will continue to poll and draw the visual it until the visual is done with the in-progress work. An analogy is a snow globe. The user moving the camera shakes up the snow globe. We need to keep polling/drawing things until all the snow settles down. Then everything will stay completely still until the camera is moved again, shaking up the globe once more. Parameters ---------- parent : QObject Parent Qt object. camera : Camera The viewer's main camera. """ def __init__(self, parent: QObject): super().__init__(parent) self.events = EmitterGroup(source=self, auto_connect=True, poll=None) self.timer = QTimer() self.timer.setInterval(POLL_INTERVAL_MS) self.timer.timeout.connect(self._on_timer) self._interval = IntervalTimer() def on_camera(self, _event) -> None: """Called when camera view changes at all.""" self.wake_up() def wake_up(self, _event=None) -> None: """Wake up QtPoll so it starts polling.""" # Start the timer so that we start polling. We used to poll once # right away here, but it led to crashes. Because we polled during # a paintGL event? if not self.timer.isActive(): self.timer.start() def _on_timer(self) -> None: """Called when the timer is running. The timer is running which means someone we are polling still has work to do. """ self._poll() def _poll(self) -> None: """Called on camera move or with the timer.""" # Listeners might include visuals and the monitor. event = self.events.poll() # Listeners will "handle" the event if they need more polling. If # no one needs polling, then we can stop the timer. if not event.handled: self.timer.stop() return # Someone handled the event, so they want to be polled even if # the mouse doesn't move. So start the timer if needed. if not self.timer.isActive(): self.timer.start() def closeEvent(self, _event: QEvent) -> None: """Cleanup and close. Parameters ---------- _event : QEvent The close event. """ self.timer.stop() self.deleteLater()
class Job(QObject): """Class for managing and monitoring an MFiX job. This class contains methods for issuing requests to and handling responses from the MFiX API. pidfile: Name of file containing MFiX API connection information. """ # Signals sig_job_exit = Signal() sig_response = Signal(str, dict) sig_error = Signal() sig_success = Signal() sig_handle_test = Signal(str, dict) sig_handle_test_error = Signal(str, QObject) sig_update_job_status = Signal() sig_change_run_state = Signal() def __init__(self, parent, pidfile): super(Job, self).__init__() self.parent = parent self.warning = parent.warning self.error = parent.error self.mfix_pid = None self.job_id = None self.mtime = time.time() # Job start time self.status = {} self.pidfile = pidfile self.api = None self.api_available = False self.pending = True self.sig_handle_test.connect(self.slot_handle_test) self.sig_handle_test_error.connect(self.slot_handle_test_error) self.api = PymfixAPI(self) self.mfix_pid = self.api.mfix_pid self.job_id = self.api.job_id self.run_cmd = self.api.run_cmd # test API before starting status loop # Timer will be stopped by slot_handle_test self.api_test_timer = QTimer() self.api_test_timer.setInterval(100) self.api_test_timer.timeout.connect(self.test_connection) self.api_test_timer.start() # don't start status timer here, it is started once API is responsive self.api_status_timer = QTimer() self.api_status_timer.setInterval(100) self.api_status_timer.timeout.connect(self.update_job_status) def update_job_status(self): self.api.get_status() def connect(self): self.test_connection() def stop_timers(self): if self.api_test_timer and self.api_test_timer.isActive(): self.api_test_timer.stop() if self.api_status_timer and self.api_status_timer.isActive(): self.api_status_timer.stop() def cleanup_and_exit(self): self.stop_timers() self.api_available = False self.sig_job_exit.emit() def test_connection(self): """Test API connection. Start API test timer if it is not already running. Check current test count and signal job exit if timeout has been exceeded.""" try: self.api.get_status() except Exception as e: # #print("SIG_ERROR", e) #increments error count self.sig_error.emit() def slot_handle_test_error(self, req_id, reply): # this can be hit multiple times: until JobManager # error limit is reached self.error('Network error: request id %s' % req_id) def slot_handle_test(self, req_id, response): """Parse response data from API test call. If response is in JSON format and does not contain an error message, then stop API test timer and start API status timer. If API response includes a permenant failure or is not well formed, emit :class:`Job.sig_error` signal. """ # this can be hit multiple times: until we hit JobManager error limit try: if response.get('internal_error'): self.error('Internal error %s' % response) self.sig_error.emit() return except Exception as e: # response was not parsable, API is not functional self.error('Response format error: %s, response=%s' % (e, response)) self.sig_error.emit() return # reset error count and mark API as available self.api_available = True # stop test timer and start status timer self.api_test_timer.stop() self.api_status_timer.start() self.sig_update_job_status.emit() self.sig_change_run_state.emit() def reinit(self, project_file_contents, autostart=False): """reinitialize job. Sanity checks (paused, gui.unsaved_flag, etc) must be handled by caller""" data = json.dumps({'mfix_dat': project_file_contents, 'autostart': autostart}) # TODO move unicode conversion into 'post' method data = data.encode('utf-8') headers = {'content-type': 'application/json'} self.api.post('reinit', data=data, headers=headers) # TODO: handlers for reinit success/failure (don't unpause job if reinit failed) # TODO restore GUI values to values matching running job if reinit failed (how?) def is_paused(self): return self.status.get('paused') def pause(self): self.api.get('pause') def unpause(self): self.api.get('unpause') def stop_mfix(self): self.api.get('stop') def exit_mfix(self): self.api.get('exit') def force_stop_mfix(self): """Send stop request""" if self.job_id: # Queued job; TODO qkill return if not self.mfix_pid: # Nothing to kill return try: p = psutil.Process(self.mfix_pid) except Exception as e: if isinstance(e, psutil.NoSuchProcess): return # Nothing to do else: raise is_mpi = False kill_pid = self.mfix_pid if self.run_cmd and 'mpirun' in self.run_cmd: while p: is_mpi = any('mpirun' in x for x in p.cmdline()) # cmdline is list of strings if is_mpi: kill_pid = p.pid break p = p.parent() else: return if is_mpi: sig = signal.SIGTERM else: sig = signal.SIGTERM try: os.kill(kill_pid, sig) except Exception as e: self.error("force_kill: %s" % e) def handle_stop_mfix(self, req_id, response): """Handler for responses to stop requests""" self.api_available = False self.api_status_timer.stop() self.handle_response(req_id, response) self.sig_change_run_state.emit() self.cleanup_and_exit() def is_output_pending(self): return not (self.api.stdout_collected and self.api.stderr_collected)
class WaitingSpinner(QWidget): """QtWaitingSpinner is a highly configurable, custom Qt widget for showing "waiting" or "loading" spinner icons in Qt applications, e.g. the spinners below are all QtWaitingSpinner widgets differing only in their configuration Args: parent (QWidget): Parent widget. centerOnParent (bool): Center on parent widget. disableParentWhenSpinning (bool): Disable parent widget when spinning. modality (Qt.WindowModality): Spinner modality. roundness (float): Lines roundness. fade (float): Spinner fade. lines (int): Lines count. line_length (int): Lines length. line_width (int): Lines width. radius (int): Spinner radius. speed (float): Spinner speed. """ def __init__(self, parent, centerOnParent=True, disableParentWhenSpinning=False, modality=Qt.NonModal, roundness=100., fade=80., lines=20, line_length=10, line_width=2, radius=10, speed=math.pi / 2): super().__init__(parent) self._centerOnParent = centerOnParent self._disableParentWhenSpinning = disableParentWhenSpinning self._color = QColor(0, 0, 0) self._roundness = roundness self._minimumTrailOpacity = math.pi self._trailFadePercentage = fade self._oldTrailFadePercentage = fade self._revolutionsPerSecond = speed self._numberOfLines = lines self._lineLength = line_length self._lineWidth = line_width self._innerRadius = radius self._currentCounter = 0 self._isSpinning = False self.fadeInTimer = QTimer() self.fadeOutTimer = QTimer() self._timer = QTimer(self) self._timer.timeout.connect(self.rotate) self.updateSize() self.updateTimer() self.hide() self.setWindowModality(modality) self.setAttribute(Qt.WA_TranslucentBackground) self.setColor(qrainbowstyle.getCurrentPalette().COLOR_ACCENT_4) def paintEvent(self, QPaintEvent): self.updatePosition() painter = QPainter(self) painter.fillRect(self.rect(), Qt.transparent) painter.setRenderHint(QPainter.Antialiasing, True) if self._currentCounter >= self._numberOfLines: self._currentCounter = 0 painter.setPen(Qt.NoPen) for i in range(self._numberOfLines): painter.save() painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength) rotateAngle = float(360 * i) / float(self._numberOfLines) painter.rotate(rotateAngle) painter.translate(self._innerRadius, 0) distance = self.lineCountDistanceFromPrimary( i, self._currentCounter, self._numberOfLines) color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage, self._minimumTrailOpacity, self._color) painter.setBrush(color) painter.drawRoundedRect( QRectF(0, -self._lineWidth / 2, self._lineLength, self._lineWidth), self._roundness, self._roundness, Qt.RelativeSize) painter.restore() def changeEvent(self, event: QEvent): """Change event handler. Args: event (QEvent): Event. """ if event.type() == QEvent.StyleChange: self.setColor(qrainbowstyle.getCurrentPalette().COLOR_ACCENT_4) def start(self): self.updatePosition() self._isSpinning = True self.show() if self.parentWidget and self._disableParentWhenSpinning: self.parentWidget().setEnabled(False) if not self._timer.isActive(): self._timer.start() self._currentCounter = 0 def stop(self): self._isSpinning = False self.hide() if self.parentWidget() and self._disableParentWhenSpinning: self.parentWidget().setEnabled(True) if self._timer.isActive(): self._timer.stop() self._currentCounter = 0 def setNumberOfLines(self, lines): self._numberOfLines = lines self._currentCounter = 0 self.updateTimer() def setLineLength(self, length): self._lineLength = length self.updateSize() def setLineWidth(self, width): self._lineWidth = width self.updateSize() def setInnerRadius(self, radius): self._innerRadius = radius self.updateSize() def fadeIn(self, time: int = 15): self.setTrailFadePercentage(0) self.stopFade() self.hide() self.fadeInTimer = QTimer() self.fadeInTimer.timeout.connect(self._on_fadeIn) self.fadeInTimer.start(time) def _on_fadeIn(self): if self.trailFadePercentage < self._oldTrailFadePercentage: if self.trailFadePercentage == 0: self.show() self.setTrailFadePercentage(self.trailFadePercentage + 1) else: self.fadeInTimer.stop() def fadeOut(self, time: int = 15): self.show() self.stopFade() self.fadeOutTimer = QTimer() self.fadeOutTimer.timeout.connect(self._on_fadeOut) self.fadeOutTimer.start(time) def _on_fadeOut(self): if self.trailFadePercentage > 0: self.setTrailFadePercentage(self.trailFadePercentage - 1) else: self.hide() self.fadeOutTimer.stop() self._isFading = False def stopFade(self): if self.fadeInTimer.isActive(): self.fadeInTimer.stop() if self.fadeOutTimer.isActive(): self.fadeOutTimer.stop() @property def color(self): return self._color @property def roundness(self): return self._roundness @property def minimumTrailOpacity(self): return self._minimumTrailOpacity @property def trailFadePercentage(self): return self._trailFadePercentage @property def revolutionsPersSecond(self): return self._revolutionsPerSecond @property def numberOfLines(self): return self._numberOfLines @property def lineLength(self): return self._lineLength @property def lineWidth(self): return self._lineWidth @property def innerRadius(self): return self._innerRadius @property def isSpinning(self): return self._isSpinning def setRoundness(self, roundness): self._roundness = max(0.0, min(100.0, roundness)) def setColor(self, color=Qt.black): self._color = QColor(color) def setRevolutionsPerSecond(self, revolutionsPerSecond): self._revolutionsPerSecond = revolutionsPerSecond self.updateTimer() def setTrailFadePercentage(self, trail): self._trailFadePercentage = trail def setMinimumTrailOpacity(self, minimumTrailOpacity): self._minimumTrailOpacity = minimumTrailOpacity def rotate(self): self._currentCounter += 1 if self._currentCounter >= self._numberOfLines: self._currentCounter = 0 self.update() def updateSize(self): size = int((self._innerRadius + self._lineLength) * 2) self.setFixedSize(size, size) def updateTimer(self): self._timer.setInterval( int(1000 / (self._numberOfLines * self._revolutionsPerSecond))) def updatePosition(self): if self.parentWidget() and self._centerOnParent: self.move( int(self.parentWidget().width() / 2 - self.width() / 2), int(self.parentWidget().height() / 2 - self.height() / 2)) def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines): distance = primary - current if distance < 0: distance += totalNrOfLines return distance def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput): color = QColor(colorinput) if countDistance == 0: return color minAlphaF = minOpacity / 100.0 distanceThreshold = int( math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)) if countDistance > distanceThreshold: color.setAlphaF(minAlphaF) else: alphaDiff = color.alphaF() - minAlphaF gradient = alphaDiff / float(distanceThreshold + 1) resultAlpha = color.alphaF() - gradient * countDistance # If alpha is out of bounds, clip it. resultAlpha = min(1.0, max(0.0, resultAlpha)) color.setAlphaF(resultAlpha) return color