示例#1
0
class GuiTopology(QObject):
    """
    Expose Topology attributes to QML
    """
    def __init__(self, preset, parent=None):
        super().__init__(parent)
        self.preset = preset
        self.load(preset)
        presets.topo_changed.connect(self.load)

    def load(self, preset):
        if self.preset != preset:
            return
        self.topology = Topology(preset)
        self._nodes_dict = {
            key: GuiNodes(key, nodes)
            for key, nodes in self.topology.nodes.items()
        }
        self._nodes = list(self._nodes_dict.values())

    def presetName(self):
        return self.topology.preset_name

    def nodes(self):
        return self._nodes

    def nodes_dict(self):
        return self._nodes_dict

    nodes_changed = Signal()
    nodes = Property('QVariant', nodes, notify=nodes_changed)
    nodes_dict_changed = Signal()
    nodes_dict = Property('QVariant', nodes_dict, notify=nodes_dict_changed)
    presetName_changed = Signal()
    presetName = Property(str, presetName, notify=presetName_changed)
示例#2
0
class GuiPositions(QObject):
    def __init__(self, position, parent=None):
        super().__init__(parent)
        self.position = position
        #self._x = [float(n) for n in self.position.x]
        #self._y = [float(n) for n in self.position.y]

    def x(self):
        return [float(n) for n in self.position.x]

    def y(self):
        return [float(n) for n in self.position.y]

    def angle(self):
        return self.position.angle

    def absolute_scale(self):
        return self.position.absolute_scale

    def translation(self):
        return self.position.translation

    def rotate(self, angle_deg):
        self.position.rotate(angle_deg)
        self.angle_changed.emit()
        self.x_changed.emit()
        self.y_changed.emit()

    def translate(self, x, y):
        self.position.translate(x, y)
        self.translation_changed.emit()
        self.x_changed.emit()
        self.y_changed.emit()

    def scale(self, scale_factor):
        self.position.scale(scale_factor)
        self.absolute_scale_changed.emit()
        self.x_changed.emit()
        self.y_changed.emit()

    x_changed = Signal()
    x = Property('QVariant', x, notify=x_changed)
    y_changed = Signal()
    y = Property('QVariant', y, notify=y_changed)
    angle_changed = Signal()
    angle = Property('QVariant', angle, notify=angle_changed)
    absolute_scale_changed = Signal()
    absolute_scale = Property('QVariant',
                              absolute_scale,
                              notify=absolute_scale_changed)
    translation_changed = Signal()
    translation = Property('QVariant', translation, notify=translation_changed)
示例#3
0
class ColorButton(QPushButton):
    """
    Color choosing push button
    """
    colorChanged = Signal(QColor)

    def __init__(self, parent=None):
        QPushButton.__init__(self, parent)
        self.setFixedSize(20, 20)
        self.setIconSize(QSize(12, 12))
        self.clicked.connect(self.choose_color)
        self._color = QColor()

    def choose_color(self):
        color = QColorDialog.getColor(self._color, self.parentWidget(),
                                      'Select Color',
                                      QColorDialog.ShowAlphaChannel)
        if color.isValid():
            self.set_color(color)

    def get_color(self):
        return self._color

    @Slot(QColor)
    def set_color(self, color):
        if color != self._color:
            self._color = color
            self.colorChanged.emit(self._color)
            pixmap = QPixmap(self.iconSize())
            pixmap.fill(color)
            self.setIcon(QIcon(pixmap))

    color = Property("QColor", get_color, set_color)
示例#4
0
class HalLedWidget(LEDWidget):

    # one to rule them all
    hal_status = HALStatus()

    def __init__(self, parent=None):
        super(HalLedWidget, self).__init__(parent)

        self._hal_pin = ''

    def getHalPin(self):
        return self._hal_pin

    @Slot(str)
    def setHalPin(self, hal_pin):
        self._hal_pin = hal_pin
        try:
            pin = self.hal_status.getHALPin(hal_pin)
        except ValueError as e:
            log.warning(e)
            return
        pin.valueChanged[bool].connect(self.setState)
        self.setState(pin.getValue())
        log.debug("HAL LED connected to yellow<{}>".format(hal_pin))

    hal_pin_name = Property(str, getHalPin, setHalPin)
示例#5
0
class GuiNodes(QObject):
    def __init__(self, key, nodes, parent=None):
        super().__init__(parent)
        self.nodes = nodes
        self._positions = [
            GuiPositions(positions) for positions in self.nodes.positions
        ]
        self._node_type = key

    def positions(self):
        return self._positions

    def nodeType(self):
        return self._node_type

    positions_changed = Signal()
    positions = Property('QVariant', positions, notify=positions_changed)
    node_type_changed = Signal()
    nodeType = Property(str, nodeType, notify=node_type_changed)
示例#6
0
class TyphosDesignerMixin(pydm.widgets.base.PyDMWidget):
    """
    A mixin class used to display Typhos widgets in the Qt designer.
    """

    # Unused properties that we don't want visible in designer
    alarmSensitiveBorder = Property(bool, designable=False)
    alarmSensitiveContent = Property(bool, designable=False)
    precisionFromPV = Property(bool, designable=False)
    precision = Property(int, designable=False)
    showUnits = Property(bool, designable=False)

    @Property(str)
    def channel(self):
        """The channel address to use for this widget"""
        if self._channel:
            return str(self._channel)
        return None

    @channel.setter
    def channel(self, value):
        if self._channel != value:
            # Remove old connection
            if self._channels:
                self._channels.clear()
                for channel in self._channels:
                    if hasattr(channel, 'disconnect'):
                        channel.disconnect()
            # Load new channel
            self._channel = str(value)
            channel = HappiChannel(address=self._channel, tx_slot=self._tx)
            self._channels = [channel]
            # Connect the channel to the HappiPlugin
            if hasattr(channel, 'connect'):
                channel.connect()

    @Slot(object)
    def _tx(self, value):
        """Receive information from happi channel"""
        self.add_device(value['obj'])
示例#7
0
class QDoubleScrollBar(QScrollBar):
    """A QScrollBar that handles float values."""

    rangeChanged = Signal(float, float)
    sliderMoved = Signal(float)
    valueChanged = Signal(float)

    def __init__(self, orientation=Qt.Horizontal, parent=None):
        """Init."""
        self._decimals = 0
        self._scale = 1
        super(QDoubleScrollBar, self).__init__(orientation, parent)
        super().rangeChanged.connect(self._intercept_rangeChanged)
        super().sliderMoved.connect(self._intercept_sliderMoved)
        super().valueChanged.connect(self._intercept_valueChanged)

        menu = QMenu(self)
        ac = menu.addAction('Set Single Step')
        ac.triggered.connect(self.dialogSingleStep)
        ac = menu.addAction('Set Page Step')
        ac.triggered.connect(self.dialogPageStep)
        menu.addSeparator()
        ac = menu.addAction("Left edge")
        ac.triggered.connect(lambda: self.triggerAction(self.SliderToMinimum))
        ac = menu.addAction("Right edge")
        ac.triggered.connect(lambda: self.triggerAction(self.SliderToMaximum))
        self.contextMenu = menu

    @Slot(bool)
    def dialogSingleStep(self, value):
        """Show dialog to set singleStep."""
        mini = 1/self._scale
        maxi = (self.maximum - self.minimum)/10
        d, okPressed = QInputDialog.getDouble(self, "Single Step",
                                              "Single Step:", self.singleStep,
                                              mini, maxi, self._decimals)
        if okPressed:
            self.setSingleStep(d)

    def dialogPageStep(self, value):
        """Show dialog to set pageStep."""
        mini = 10/self._scale
        maxi = (self.maximum - self.minimum)
        d, okPressed = QInputDialog.getDouble(self, "Page Step", "Page Step:",
                                              self.pageStep,
                                              mini, maxi, self._decimals)
        if okPressed:
            self.setPageStep(d)

    def contextMenuEvent(self, ev):
        """Show context menu."""
        self.contextMenu.exec_(ev.globalPos())

    def getDecimals(self):
        """Return decimals."""
        return self._decimals

    def setDecimals(self, value):
        """Set decimals."""
        mini = self.getMinimum()
        maxi = self.getMaximum()
        sgstep = self.getSingleStep()
        pgstep = self.getPageStep()
        val = self.getValue()
        slpos = self.getSliderPosition()

        self._decimals = value
        self._scale = 10**value
        self.setMinimum(mini)
        self.setMaximum(maxi)
        self.setSingleStep(sgstep)
        self.setPageStep(pgstep)
        self.setValue(val)
        self.setSliderPosition(slpos)

    decimals = Property(int, getDecimals, setDecimals)

    def getMinimum(self):
        """Return minimum value."""
        return super().minimum()/self._scale

    def setMinimum(self, value):
        """Set minimum value."""
        if _np.isnan(value):
            value = 0
        try:
            mini = round(value*self._scale)
            mini = min(mini, 2**31-1)
            mini = max(-2**31, mini)
            super().setMinimum(mini)
        except (OverflowError, ValueError) as err:
            logging.warning(str(err), '(value=' + str(value) + ')')

    minimum = Property(float, getMinimum, setMinimum)

    def getMaximum(self):
        """Return maximum value."""
        return super().maximum()/self._scale

    def setMaximum(self, value):
        """Set maximum value."""
        if _np.isnan(value):
            value = 0
        try:
            maxi = round(value*self._scale)
            maxi = min(maxi, 2**31-1)
            maxi = max(-2**31, maxi)
            super().setMaximum(maxi)
        except (OverflowError, ValueError) as err:
            logging.warning(str(err), '(value=' + str(value) + ')')

    maximum = Property(float, getMaximum, setMaximum)

    def getSingleStep(self):
        """Get single step."""
        return super().singleStep()/self._scale

    def setSingleStep(self, value):
        """Set single step."""
        val = round(value*self._scale)
        rang = super().maximum() - super().minimum()
        if not val:
            super().setSingleStep(1)
        elif val > round(rang/10):
            super().setSingleStep(round(rang/10))
        else:
            super().setSingleStep(val)

    singleStep = Property(float, getSingleStep, setSingleStep)

    def getPageStep(self):
        """Get page step."""
        return super().pageStep()/self._scale

    def setPageStep(self, value):
        """Set page step."""
        val = round(value*self._scale)
        rang = super().maximum() - super().minimum()
        if val < 10:
            super().setPageStep(10)
        elif val > round(rang):
            super().setPageStep(round(rang))
        else:
            super().setPageStep(val)

    pageStep = Property(float, getPageStep, setPageStep)

    def getValue(self):
        """Get value."""
        return super().value()/self._scale

    @Slot(float)
    def setValue(self, value):
        """Set value."""
        if value is None:
            return
        try:
            val = round(value*self._scale)
            val = min(val, 2**31-1)
            val = max(-2**31, val)
            super().setValue(val)
        except (OverflowError, ValueError) as err:
            logging.warning(str(err), '(value=' + str(value) + ')')

    value = Property(float, getValue, setValue)

    def getSliderPosition(self):
        """Get slider position."""
        return super().sliderPosition()/self._scale

    def setSliderPosition(self, value):
        """Set slider position."""
        pos = round(value*self._scale)
        pos = min(pos, 2**31-1)
        pos = max(-2**31, pos)
        super().setSliderPosition(pos)

    sliderPosition = Property(float, getSliderPosition, setSliderPosition)

    def keyPressEvent(self, event):
        """Reimplement keyPressEvent."""
        singlestep = self.getSingleStep()
        pagestep = self.getPageStep()
        ctrl_hold = self.app.queryKeyboardModifiers() == Qt.ControlModifier
        if ctrl_hold and (event.key() == Qt.Key_Left):
            self.setSingleStep(10*singlestep)
            self.setPageStep(10*pagestep)
            self._show_step_tooltip()
        elif ctrl_hold and (event.key() == Qt.Key_Right):
            self.setSingleStep(0.1*singlestep)
            self.setPageStep(0.1*pagestep)
            self._show_step_tooltip()
        else:
            super().keyPressEvent(event)

    def _show_step_tooltip(self):
        QToolTip.showText(
            self.mapToGlobal(
                QPoint(self.x()+self.width()/2, self.y()-2*self.height())),
            'Single step: '+str(self.singleStep) +
            '\nPage step: '+str(self.pageStep),
            self, self.rect(), 1000)

    @Slot(float, float)
    def setRange(self, mini, maxi):
        """Set range."""
        mini = max(-2**31, round(mini/self._scale))
        maxi = min(round(maxi*self._scale), 2**31-1)
        super().setRange(mini, maxi)

    @Slot(int, int)
    def _intercept_rangeChanged(self, mini, maxi):
        self.rangeChanged.emit(mini/self._scale, maxi/self._scale)

    @Slot(int)
    def _intercept_sliderMoved(self, value):
        self.sliderMoved.emit(value/self._scale)

    @Slot(int)
    def _intercept_valueChanged(self, value):
        self.valueChanged.emit(value/self._scale)
示例#8
0
class PyDMDrawingPolyline(PyDMDrawing):
    """
    A widget with a multi-segment, piecewise-linear line drawn in it.
    This class inherits from PyDMDrawing.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """
    def __init__(self, parent=None, init_channel=None):
        super(PyDMDrawingPolyline, self).__init__(parent, init_channel)
        self.penStyle = Qt.SolidLine
        self.penWidth = 1
        self._points = []

    def draw_item(self, painter):
        """
        Draws the segmented line after setting up the canvas with a call to
        ```PyDMDrawing.draw_item```.
        """
        super(PyDMDrawingPolyline, self).draw_item(painter)
        x, y, w, h = self.get_bounds()

        def p2d(pt):
            "convert point to drawing coordinates"
            # drawing coordinates are centered: (0,0) is in center
            # our points are absolute: (0,0) is upper-left corner
            u, v = map(int, pt.split(","))
            return QPointF(u+x, v+y)

        if len(self._points) > 1:
            for i, p1 in enumerate(self._points[:-1]):
                painter.drawLine(p2d(p1), p2d(self._points[i+1]))

    def getPoints(self):
        return self._points

    def _validator_(self, value):
        """
        ensure that `value` has correct form

        Parameters
        ----------
        value : [str]
            List of strings representing ordered pairs
            of integer coordinates.  Each ordered pair
            is comma-separated (although white-space
            separated is acceptable as input).

        Returns
        ----------
        verified : [str]
            List of strings in standard format

        """
        def isinteger(value):
            value = value.strip()
            try:
                float(value)
                return True
            except:
                return False

        verified = []
        for i, pt in enumerate(value):
            point = pt.split(",")
            if len(point) != 2:
                point = pt.split()    # tolerant of space-separated
            if len(point) != 2:
                emsg = "polyline point %d must be two values, comma-separated, received '%s'" % (i+1, pt)
                logger.exception(emsg)
                return
            if not isinteger(point[0]):
                emsg = "polyline point %d content must be integer, received '%s'" % (i+1, point[0])
                logger.exception(emsg)
                return
            if not isinteger(point[1]):
                emsg = "polyline point %d content must be integer, received '%s'" % (i+1, point[1])
                logger.exception(emsg)
                return
            verified.append(", ".join(point))

        return verified

    def setPoints(self, value):
        if len(value) < 2:
            emsg = "Must have two or more points"
            logger.exception(emsg)
            return

        verified = self._validator_(value)
        if verified is not None:
            self._points = verified
            self.update()

    def resetPoints(self):
        self._points = []
        self.update()

    points = Property("QStringList", getPoints, setPoints, resetPoints)
示例#9
0
class GcodeEditor(EditorBase, QObject):
    ARROW_MARKER_NUM = 8

    def __init__(self, parent=None):
        super(GcodeEditor, self).__init__(parent)

        self.filename = ""
        self._last_filename = None
        self.auto_show_mdi = True
        self.last_line = None
        # self.setEolVisibility(True)

        self.is_editor = False

        self.dialog = FindReplaceDialog(parent=self)

        # QSS Hack

        self.backgroundcolor = ''
        self.marginbackgroundcolor = ''

    @Slot(bool)
    def setEditable(self, state):
        if state:
            self.setReadOnly(False)
        else:
            self.setReadOnly(True)

    @Slot(str)
    def setFilename(self, path):
        self.filename = path

    @Slot()
    def save(self):
        save_file = QFile(self.filename)

        result = save_file.open(QFile.WriteOnly)
        if result:
            save_stream = QTextStream(save_file)
            save_stream << self.text()

            save_file.close()

    @Slot()
    def saveAs(self):
        file_name = self.save_as_dialog(self.filename)

        if file_name is False:
            return

        original_file = QFileInfo(self.filename)
        path = original_file.path()

        new_absolute_path = os.path.join(path, file_name)
        new_file = QFile(new_absolute_path)

        result = new_file.open(QFile.WriteOnly)
        if result:
            save_stream = QTextStream(new_file)
            save_stream << self.text()

            new_file.close()

    @Slot()
    def find_replace(self):
        self.dialog.show()

    def search_text(self, find_text, highlight_all):
        from_start = False
        if find_text != "":
            self.text_search(find_text, from_start, highlight_all)

    def replace_text(self, find_text, replace_text):
        from_start = False
        if find_text != "" and replace_text != "":
            self.text_replace(find_text, replace_text, from_start)

    def replace_all_text(self, find_text, replace_text):
        from_start = True
        if find_text != "" and replace_text != "":
            self.text_replace_all(find_text, find_text, from_start)

    @Property(bool)
    def is_editor(self):
        return self._is_editor

    @is_editor.setter
    def is_editor(self, enabled):
        self._is_editor = enabled
        if not self._is_editor:
            STATUS.file.notify(self.load_program)
            STATUS.motion_line.onValueChanged(self.highlight_line)

            # STATUS.connect('line-changed', self.highlight_line)
            # if self.idle_line_reset:
            #     STATUS.connect('interp_idle', lambda w: self.set_line_number(None, 0))

    @Property(str)
    def backgroundcolor(self):
        """Property to set the background color of the GCodeEditor (str).

        sets the background color of the GCodeEditor
        """
        return self._backgroundcolor

    @backgroundcolor.setter
    def backgroundcolor(self, color):
        self._backgroundcolor = color
        self.set_background_color(color)

    @Property(str)
    def marginbackgroundcolor(self):
        """Property to set the background color of the GCodeEditor margin (str).

        sets the background color of the GCodeEditor margin
        """
        return self._marginbackgroundcolor

    @marginbackgroundcolor.setter
    def marginbackgroundcolor(self, color):
        self._marginbackgroundcolor = color
        self.set_margin_background_color(color)

    def load_program(self, fname=None):
        if fname is None:
            fname = self._last_filename
        else:
            self._last_filename = fname
        self.load_text(fname)
        # self.zoomTo(6)
        self.setCursorPosition(0, 0)

    # when switching from MDI to AUTO we need to reload the
    # last (linuxcnc loaded) program.
    def reload_last(self):
        self.load_text(STATUS.old['file'])
        self.setCursorPosition(0, 0)

    # With the auto_show__mdi option, MDI history is shown
    def load_mdi(self):
        self.load_text(INFO.MDI_HISTORY_PATH)
        self._last_filename = INFO.MDI_HISTORY_PATH
        # print 'font point size', self.font().pointSize()
        # self.zoomTo(10)
        # print 'font point size', self.font().pointSize()
        self.setCursorPosition(self.lines(), 0)

    # With the auto_show__mdi option, MDI history is shown
    def load_manual(self):
        if STATUS.is_man_mode():
            self.load_text(INFO.MACHINE_LOG_HISTORY_PATH)
            self.setCursorPosition(self.lines(), 0)

    def load_text(self, fname):
        try:
            fp = os.path.expanduser(fname)
            self.setText(open(fp).read())
        except:
            LOG.error('File path is not valid: {}'.format(fname))
            self.setText('')
            return

        self.last_line = None
        self.ensureCursorVisible()
        self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET)

    def highlight_line(self, line):
        # if STATUS.is_auto_running():
        #     if not STATUS.old['file'] == self._last_filename:
        #         LOG.debug('should reload the display')
        #         self.load_text(STATUS.old['file'])
        #         self._last_filename = STATUS.old['file']
        self.markerAdd(line, self.ARROW_MARKER_NUM)
        if self.last_line:
            self.markerDelete(self.last_line, self.ARROW_MARKER_NUM)
        self.setCursorPosition(line, 0)
        self.ensureCursorVisible()
        self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET)
        self.last_line = line

    def set_line_number(self, line):
        pass

    def line_changed(self, line, index):
        # LOG.debug('Line changed: {}'.format(STATUS.is_auto_mode()))
        self.line_text = str(self.text(line)).strip()
        self.line = line
        if STATUS.is_mdi_mode() and STATUS.is_auto_running() is False:
            STATUS.emit('mdi-line-selected', self.line_text,
                        self._last_filename)

    def select_lineup(self):
        line, col = self.getCursorPosition()
        LOG.debug(line)
        self.setCursorPosition(line - 1, 0)
        self.highlight_line(line - 1)

    def select_linedown(self):
        line, col = self.getCursorPosition()
        LOG.debug(line)
        self.setCursorPosition(line + 1, 0)
        self.highlight_line(line + 1)

    # designer recognized getter/setters
    # auto_show_mdi status
    def set_auto_show_mdi(self, data):
        self.auto_show_mdi = data

    def get_auto_show_mdi(self):
        return self.auto_show_mdi

    def reset_auto_show_mdi(self):
        self.auto_show_mdi = True

    auto_show_mdi_status = Property(bool, get_auto_show_mdi, set_auto_show_mdi,
                                    reset_auto_show_mdi)

    # simple input dialog for save as
    def save_as_dialog(self, filename):
        text, ok_pressed = QInputDialog.getText(self, "Save as", "New name:",
                                                QLineEdit.Normal, filename)

        if ok_pressed and text != '':
            return text
        else:
            return False
示例#10
0
class GrabCutInstance(QObject):

    GRAB_CUT_NUM_ITER = 5

    COLOR_OBJ_SURE = bgr(40, 250, 10, 100)
    COLOR_OBJ_GUESS = bgr(200, 200, 20, 50)
    COLOR_OBJ_CONTOUR = bgr(0, 255, 0, 200)

    COLOR_BGD_GUESS = bgr(120, 40, 20, 0)
    COLOR_BGD_SURE = bgr(250, 40, 10, 100)

    COLOR_TABLE = np.array(
        [COLOR_BGD_SURE, COLOR_OBJ_SURE, COLOR_BGD_GUESS, COLOR_OBJ_GUESS])

    ALPHA_CONTOUR = 255
    ALPHA_CLASS_COLOR = 150

    MORPH_KERNEL = np.ones((3, 3), np.uint8)

    def __init__(self,
                 backend,
                 instance_id,
                 semantic_class,
                 photo,
                 crop_rect,
                 roi_rect,
                 use_grab_cut=True,
                 depth_index=None):
        super().__init__()

        self.backend = backend
        self.id = instance_id
        self.semantic_class = semantic_class
        self.photo = photo

        self.crop_rect = crop_rect
        self.roi_rect = roi_rect

        self.crop_tl = crop_rect[0]
        self.crop_br = crop_rect[1]

        self.roi_tl = roi_rect[0] - self.crop_tl
        self.roi_br = roi_rect[1] - self.crop_tl

        self.photo_crop = self.photo[self.crop_tl[1]:self.crop_br[1],
                                     self.crop_tl[0]:self.crop_br[0]]

        self.depth_index = depth_index or backend.depth_index_new()

        self.use_grab_cut = use_grab_cut

        self.update_qt_info()

    def grab_cut_init(self, existing_instance_mask_global=None):

        self.grab_cut_state = np.zeros((2, 65), np.float64)
        self.grab_cut_mask = np.full(self.photo_crop.shape[:2],
                                     cv2.GC_PR_BGD,
                                     dtype=np.uint8)

        # sometimes grab cut throws an exception because it finds no foreground in the whole roi
        # we help it then by marking the central pixel as foreground
        def set_center_pixel_to_foreground():
            sh_c = np.array(self.grab_cut_mask.shape) // 2
            sh_l = sh_c - 2
            sh_r = sh_c + 2
            self.grab_cut_mask[sh_l[0]:sh_r[0], sh_l[1]:sh_r[1]] = cv2.GC_FGD
            #self.grab_cut_mask[sh[0]//2, sh[1]//2] = cv2.GC_FGD
            # self.grab_cut_mask[0, 0] = cv2.GC_BGD
            #print('gc mask bincount', np.bincount(self.grab_cut_mask.reshape(-1)))

        def gc_init(mode=cv2.GC_INIT_WITH_RECT):
            cv2.grabCut(
                self.photo_crop,
                self.grab_cut_mask,
                tuple(
                    np.concatenate([self.roi_tl, self.roi_br - self.roi_tl],
                                   axis=0)),
                self.grab_cut_state[0:1],
                self.grab_cut_state[1:2],
                self.GRAB_CUT_NUM_ITER,
                mode,
            )

        if self.use_grab_cut:
            try:
                gc_init()
            except cv2.error:
                log.warning(
                    'GrabCut failed on initialization - retrying with center pixel marked'
                )
                set_center_pixel_to_foreground()
                gc_init(mode=cv2.GC_INIT_WITH_RECT | cv2.GC_INIT_WITH_MASK)

            # exclude previously existing instances
            if existing_instance_mask_global is not None:
                # we do not do it in the single init step, because if we use the "init with mask" mode
                # grab-cut expects to have BOTH negative and positive samples and crashes on an assert
                #  - but we only have negative samples
                # therefore, we will now perform another step but with the negative samples
                existing_instance_mask_crop = existing_instance_mask_global[
                    self.crop_tl[1]:self.crop_br[1],
                    self.crop_tl[0]:self.crop_br[0]]

                if np.any(existing_instance_mask_crop):
                    self.grab_cut_mask[np.where(
                        existing_instance_mask_crop)] = cv2.GC_BGD
                    log.debug(
                        'Applying mask of existing objects to the new instance, label counts: {nonzero} {bc}'
                        .format(
                            nonzero=np.count_nonzero(
                                existing_instance_mask_crop),
                            bc=np.bincount(self.grab_cut_mask.reshape(-1)),
                        ))

                    try:
                        self.grab_cut_update()
                    except cv2.error:
                        log.warning(
                            'GrabCut failed after applying existing object mask - retrying with center pixel marked'
                        )
                        set_center_pixel_to_foreground()
                        self.grab_cut_update()

        self.update_mask()

    def grab_cut_update(self):
        if self.use_grab_cut:
            cv2.grabCut(
                self.photo_crop,
                self.grab_cut_mask,
                None,
                self.grab_cut_state[0:1],
                self.grab_cut_state[1:2],
                self.GRAB_CUT_NUM_ITER,
                cv2.GC_INIT_WITH_MASK,
            )

        self.update_mask()

    def paint_circle(self, label, center_pt):
        label_value = [cv2.GC_BGD, cv2.GC_FGD][label]

        center_pt = center_pt - self.crop_tl
        cv2.circle(self.grab_cut_mask, tuple(center_pt), 5, label_value, -1)

        self.update_mask()

    def paint_polygon(self, label, points):
        label_value = [cv2.GC_BGD, cv2.GC_FGD][label]

        points_in_crop = points - self.crop_tl
        points_in_crop_int = np.rint(points_in_crop).astype(np.int32)

        cv2.drawContours(self.grab_cut_mask, [points_in_crop_int], 0,
                         label_value, -1)

        self.update_mask()

    def update_mask(self):
        self.mask = (self.grab_cut_mask == cv2.GC_FGD) | (self.grab_cut_mask
                                                          == cv2.GC_PR_FGD)
        erosion = cv2.erode(self.mask.astype(np.uint8),
                            self.MORPH_KERNEL,
                            iterations=1).astype(np.bool)
        self.contour_mask = self.mask & ~erosion
        self.contour_where = np.where(self.contour_mask)

    def draw_overlay_edit_interface(self, overlay):
        overlay_crop = overlay[self.crop_tl[1]:self.crop_br[1],
                               self.crop_tl[0]:self.crop_br[0]]
        overlay_crop[:] = self.COLOR_TABLE[self.grab_cut_mask.reshape(
            -1)].reshape(overlay_crop.shape)
        overlay_crop[self.contour_where] = self.COLOR_OBJ_CONTOUR

    def draw_overlay_contour(self, overlay):
        overlay_crop = overlay[self.crop_tl[1]:self.crop_br[1],
                               self.crop_tl[0]:self.crop_br[0]]

        class_color_bgr = self.semantic_class.color[::-1]
        overlay_crop[self.mask] = np.concatenate(
            [class_color_bgr, [self.ALPHA_CLASS_COLOR]], axis=0)
        overlay_crop[self.contour_where] = np.concatenate(
            [class_color_bgr, [self.ALPHA_CONTOUR]], axis=0)

    def draw_mask(self, global_mask, label=None):

        if label is None:
            label = self.semantic_class.id

        mask_crop = global_mask[self.crop_tl[1]:self.crop_br[1],
                                self.crop_tl[0]:self.crop_br[0]]
        mask_crop[self.mask] = label

        # def assign_reshape():
        # 	overlay_crop[:] = self.COLOR_TABLE[self.grab_cut_mask.reshape(-1)].reshape(overlay_crop.shape)
        #
        # def assign_equal():
        # 	overlay_crop[self.grab_cut_mask == cv2.GC_FGD] = self.COLOR_OBJ_SURE
        # 	overlay_crop[self.grab_cut_mask == cv2.GC_PR_FGD] = self.COLOR_OBJ_GUESS
        # 	overlay_crop[self.grab_cut_mask == cv2.GC_PR_BGD] = self.COLOR_BGD_GUESS
        # 	overlay_crop[self.grab_cut_mask == cv2.GC_BGD] = self.COLOR_BGD_SURE
        #
        # import timeit
        #
        # gl = dict(
        # 	assign_reshape = assign_reshape,
        # 	assign_equal=assign_equal,
        # )
        # n = int(1e4)
        # print('tm(reshape)  ', timeit.timeit('assign_reshape()', globals=gl, number=n))
        # print('tm(equal)    ', timeit.timeit('assign_equal()', globals=gl, number=n))
        # #tm(reshape) 10.847654940000211
        # #tm(equal) 18.054724517001887

    def contains_point(self, pt) -> bool:
        pt_int = np.rint(pt).astype(np.int32)

        if np.all(pt_int >= self.crop_tl) and np.all(pt_int < self.crop_br):
            #log.debug(f'point {pt_int} is inside of bbox {self.crop_tl} {self.crop_br} - check mask')
            pt_in_crop = pt_int - self.crop_tl
            return self.mask[pt_in_crop[1], pt_in_crop[0]]
        else:
            #log.debug(f'point {pt_int} is outside of bbox {self.crop_tl} {self.crop_br}')
            return False

    def to_dict(self):
        return dict(
            id=self.id,
            cls=self.semantic_class.id,
            crop_rect=self.crop_rect.tolist(),
            roi_rect=self.roi_rect.tolist(),
            depth_index=self.depth_index,
            use_grab_cut=self.use_grab_cut,
        )

    def save_to_dir(self, dir_path):
        imwrite(dir_path / f'instance_{self.id:03d}_gc_mask.png',
                self.grab_cut_mask)
        np.save(dir_path / f'instance_{self.id:03d}_gc_state.npy',
                self.grab_cut_state)

    def load_from_dir(self, dir_path):
        self.grab_cut_mask = imread(dir_path /
                                    f'instance_{self.id:03d}_gc_mask.png')
        self.grab_cut_state = np.load(dir_path /
                                      f'instance_{self.id:03d}_gc_state.npy')
        self.update_mask()

    @staticmethod
    def from_dict(backend, saved_info, config, photo):

        inst = GrabCutInstance(
            backend,
            saved_info['id'],
            config.classes_by_id[saved_info['cls']],
            photo,
            np.array(saved_info['crop_rect']),
            np.array(saved_info['roi_rect']),
            depth_index=saved_info.get('depth_index'),
            use_grab_cut=saved_info.get('use_grab_cut', True),
        )
        return inst

        # self.depth_index += change
        # log.debug(f'Depth index +{change} is now {self.depth_index}')
        # self.backend.reindex(self, new_index)
        # self.update_qt_info()

    # Expose to Qt
    infoChanged = Signal()
    info = Property("QVariant", notify=infoChanged)

    @info.getter
    def getInfo(self):
        return self.qt_info

    def update_qt_info(self):
        self.qt_info = dict(
            id=self.id,
            name=f'{self.semantic_class.name} {self.depth_index}',
            cls=self.semantic_class.to_dict(),
            x=float(self.crop_tl[0] + self.roi_tl[0]),
            y=float(self.crop_tl[1] + self.roi_tl[1]),
            width=float(self.roi_br[0] - self.roi_tl[0]),
            height=float(self.roi_br[1] - self.roi_tl[1]),
            depth_index=self.depth_index,
        )
        self.infoChanged.emit()

    deleted = Signal()
示例#11
0
class LabelBackend(QObject):
    @staticmethod
    def qml_point_to_np(qpoint: QPointF):
        return np.array(qpoint.toTuple())

    @staticmethod
    def qml_rect_to_np(qrect: QRectF):
        return np.array([
            qrect.topLeft().toTuple(),
            qrect.bottomRight().toTuple(),
        ])

    def __init__(self):
        super().__init__()

        self.instances = []
        self.instances_by_id = {}

        self.image_provider = LabelOverlayImageProvider()
        self.config = LabelConfig()

        self.dir_start = ''

    # Semantic classes
    def load_config(self, cfg_path):
        if cfg_path.is_file():
            self.config.load_from_path(cfg_path)
        else:
            log.error(f'Config path {cfg_path} is not a file')

    @staticmethod
    def load_photo(img_path):
        """ Load image and ensure it is 8bit RGB """
        img_data = imread(img_path)

        # 8 bit
        img_data = img_data.astype(np.uint8)

        # ensure that is has 3 channels
        if img_data.shape.__len__() == 2:  # 2D grayscale
            img_data = np.broadcast_to(img_data[:, :, None],
                                       img_data.shape + (3, ))

        if img_data.shape[2] == 4:  #RGBA
            img_data = img_data[:, :, :3]

        return img_data

    @Slot(result=str)
    def get_image_path(self):
        return str(self.img_path)

    def set_image_path(self, img_path):
        log.info(f'Loading image {img_path}')

        # Load new image

        # this may throw if its not an image, in that case we don't apply any changes to variables
        img_path = Path(img_path)
        img_data = self.load_photo(img_path)

        self.img_path = img_path
        self.photo = img_data
        self.resolution = np.array(self.photo.shape[:2][::-1])
        self.image_provider.init_image(self.resolution)
        self.overlay_data = self.image_provider.image_view

        # Clear instances
        for old_inst in self.instances:
            old_inst.deleted.emit()
        self.instances = []
        self.instances_by_id = {}

        # Load state
        data_dir = self.img_path.with_suffix('.labels')
        if data_dir.is_dir():
            log.info(f'Loading saved state from {data_dir}')
            self.load(data_dir)

        self.next_instance_id = int(
            np.max([0] + [inst.id for inst in self.instances]) + 1)
        self.instances_by_id = {inst.id: inst for inst in self.instances}
        self.instance_selected = None
        self.overlay_refresh_after_selection_change()

    @Slot(QUrl, result=bool)
    def set_image(self, img_url: QUrl):
        try:  # this has to finish, we don't want to break UI interaction
            self.set_image_path(img_url.toLocalFile())
            return True
        except Exception as e:
            log.exception('Exception in set_image')
            return False

    @Slot(int, QPointF)
    def paint_circle(self, label_to_paint: int, center: QPointF):
        try:  # this has to finish, we don't want to break UI interaction

            if self.instance_selected:
                center_pt = np.rint(center.toTuple()).astype(dtype=np.int)

                self.instance_selected.paint_circle(label_to_paint, center_pt)
                self.instance_selected.grab_cut_update()
                self.overlay_refresh_after_edit()
            else:
                log.info('paint_circle: no instance is selected')

        except Exception as e:
            log.exception('Exception in paint_circle')

    @Slot(int, QJSValue)
    def paint_polygon(self, label_to_paint: int, points: QJSValue):
        try:  # this has to finish, we don't want to break UI interaction

            if self.instance_selected:
                points = np.array([p.toTuple() for p in points.toVariant()])

                self.instance_selected.paint_polygon(label_to_paint, points)
                self.instance_selected.grab_cut_update()
                self.overlay_refresh_after_edit()
            else:
                log.info('paint_polygon: no instance is selected')

        except Exception as e:
            log.exception('Exception in paint_polygon')

    @Slot(QPointF, result=int)
    def instance_at_point(self, pt: QPointF):
        """
		Instance id at point
		-1 means no instance 
		"""
        try:
            pt = np.rint(self.qml_point_to_np(pt)).astype(np.int32)

            for inst in self.instances_by_depthindex():
                if inst.contains_point(pt):
                    return inst.id

            return -1

        except Exception as e:
            log.exception('Exception in instance_at_point')
            return -1

    def overlay_refresh_after_selection_change(self):
        if self.instance_selected:

            self.overlay_data[:] = (0, 0, 0, 128)
            self.instance_selected.draw_overlay_edit_interface(
                self.overlay_data)

        else:
            self.overlay_data[:] = 0

            # draw with depth in mind
            for inst in reversed(self.instances_by_depthindex()):
                inst.draw_overlay_contour(self.overlay_data)

        self.overlayUpdated.emit()
        self.selectedUpdate.emit()

    def overlay_refresh_after_edit(self):
        if self.instance_selected:
            self.instance_selected.draw_overlay_edit_interface(
                self.overlay_data)
            self.overlayUpdated.emit()
        else:
            log.info(
                'overlay_refresh_after_edit but instance_selected is null')

    def depth_index_new(self):
        return max(
            (inst.depth_index for inst in self.instances), default=0) + 1

    def instances_by_depthindex(self):
        instances_by_depthindex = self.instances.copy()
        instances_by_depthindex.sort(key=attrgetter('depth_index'))
        return instances_by_depthindex

    @Slot(int)
    def select_instance(self, instance_id: int):
        self.instance_selected = self.instances_by_id.get(instance_id, None)
        self.overlay_refresh_after_selection_change()

    @Slot(QRectF, int)
    def new_instance(self, roi_rect_qt: QRectF, sem_class_id: int):
        try:  # this has to finish, we don't want to break UI interaction
            roi_rect = np.rint(self.qml_rect_to_np(roi_rect_qt)).astype(np.int)
            sem_class = self.config.classes_by_id.get(sem_class_id,
                                                      self.config.classes[0])

            margin = 32
            crop_rect = np.array([
                np.maximum(roi_rect[0] - margin, 0),
                np.minimum(roi_rect[1] + margin, self.resolution),
            ])

            # calculate area
            crop_size = crop_rect[1] - crop_rect[0]
            area = np.prod(crop_size)
            # bool(...) because the bool_ object returned by np is not json-serializable
            use_grab_cut = bool(area < MAX_AREA_FOR_GRAB_CUT)

            if not use_grab_cut:
                log.info(
                    f'GrabCut is not used for instance, because patch area ({area}) is above {MAX_AREA_FOR_GRAB_CUT}'
                )

            # automatically mark existing instances as excluded from the new instance
            existing_instance_mask = np.zeros(tuple(self.resolution[::-1]),
                                              dtype=np.uint8)
            for inst in self.instances:
                inst.draw_mask(existing_instance_mask, 1)

            instance = GrabCutInstance(
                self,
                self.next_instance_id,
                sem_class,
                self.photo,
                crop_rect,
                roi_rect,
                use_grab_cut=use_grab_cut,
            )
            self.next_instance_id += 1

            instance.grab_cut_init(existing_instance_mask)

            self.instances.append(instance)
            self.instances_by_id[instance.id] = instance
            self.select_instance(instance.id)

            self.instanceAdded.emit(instance)

        except Exception as e:
            log.exception('Exception in new_instance')

    @Slot(int, int)
    def set_instance_class(self, instance_id: int, class_id: int):
        try:  # this has to finish, we don't want to break UI interaction
            inst = self.instances_by_id[instance_id]
            cls = self.config.classes_by_id[class_id]

            inst.semantic_class = cls
            inst.update_qt_info()
            self.overlay_refresh_after_selection_change()

        except Exception as e:
            log.exception('Exception in set_instance_class')

    @Slot(int)
    def delete_instance(self, instance_id: int):
        try:  # this has to finish, we don't want to break UI interaction
            inst = self.instances_by_id[instance_id]

            if self.instance_selected == inst:
                self.select_instance(0)

            del self.instances_by_id[instance_id]
            self.instances.remove(inst)

            inst.deleted.emit()
            self.overlay_refresh_after_selection_change()

        except Exception as e:
            log.exception('Exception in delete_instance')

    @Slot(int, int)
    def change_instance_depth(self, instance_id: int, change: int):
        try:  # this has to finish, we don't want to break UI interaction

            instance = self.instances_by_id.get(instance_id)

            if instance is not None:
                # -1 because array is 0 indexed but depth is 1-indexed
                requested_index = max(0, instance.depth_index - 1 + change)

                instances_by_depthindex = self.instances_by_depthindex()
                instances_by_depthindex.remove(instance)
                instances_by_depthindex.insert(requested_index, instance)

                #log.debug(f'Depth: moving inst {instance.id} to {requested_index}')

                for i, inst in enumerate(instances_by_depthindex):
                    new_depth_index = i + 1

                    if new_depth_index != inst.depth_index:
                        #log.debug(f'Inst {inst.id} depth change: {inst.depth_index} -> {new_depth_index}')
                        inst.depth_index = new_depth_index
                        inst.update_qt_info()
            else:
                log.warning(
                    'change_instance_depth with nonexistent instance id {instance_id}'
                )

        except Exception as e:
            log.exception(
                f'Exception in change_instance_depth, args: {instance_id}, {change}'
            )

    @Slot(result=bool)
    def save(self):
        try:  # this has to finish, we don't want to break UI interaction
            log.info(f'save {self.img_path}')

            # outputs
            sem_map = np.zeros(tuple(self.resolution[::-1]), dtype=np.uint8)
            sem_colorimg = np.zeros(tuple(self.resolution[::-1]) + (3, ),
                                    dtype=np.uint8)
            inst_map = np.zeros(tuple(self.resolution[::-1]), dtype=np.uint8)

            # draw the instance list, using depth_index as label
            for inst in reversed(self.instances_by_depthindex()):
                inst.draw_mask(sem_map)
                inst.draw_mask(sem_colorimg, inst.semantic_class.color)
                inst.draw_mask(inst_map, inst.depth_index)

            out_dir = self.img_path.with_suffix('.labels')
            out_dir.mkdir(exist_ok=True)

            imwrite(out_dir / 'labels_semantic.png', sem_map)
            imwrite(out_dir / 'labels_semantic_color.png', sem_colorimg)
            imwrite(out_dir / 'labels_instance.png', inst_map)

            # internal state

            json_data = dict(
                instances=[inst.to_dict() for inst in self.instances])

            with (out_dir / 'index.json').open('w') as f_out:
                json.dump(json_data, f_out, indent='	')

            for inst in self.instances:
                inst.save_to_dir(out_dir)

            return True
        except Exception as e:
            log.exception('Exception in save')
            return False

    def load(self, in_dir):
        with (in_dir / 'index.json').open('r') as f_in:
            json_data = json.load(f_in)

        self.instances = [
            GrabCutInstance.from_dict(self, inst_data, self.config, self.photo)
            for inst_data in json_data['instances']
        ]

        for inst in self.instances:
            inst.load_from_dir(in_dir)
            self.instanceAdded.emit(inst)

    # Expose to Qt
    overlayUpdated = Signal()
    instanceAdded = Signal(QObject)

    classesUpdated = Signal()
    classes = Property('QVariant', notify=classesUpdated)

    @classes.getter
    def get_classes(self):
        return self.config.to_simple_objects()

    @Slot(result='QVariant')
    def get_instances(self):
        return self.instances

    selectedUpdate = Signal()
    selected = Property(QObject,
                        attrgetter('instance_selected'),
                        notify=selectedUpdate)

    def set_starting_directory(self, dir_start):
        self.dir_start = dir_start

    @Slot(result=str)
    def get_starting_directory(self):
        log.info(f'dir start {self.dir_start}')
        return str(self.dir_start)
示例#12
0
class PyDMTimePlot(BasePlot):
    """
    PyDMWaveformPlot is a widget to plot one or more waveforms.

    Each curve can plot either a Y-axis waveform vs. its indices,
    or a Y-axis waveform against an X-axis waveform.

    Parameters
    ----------
    parent : optional
        The parent of this widget.
    init_y_channels : list
        A list of scalar channels to plot vs time.
    plot_by_timestamps : bool
        If True, the x-axis shows timestamps as ticks, and those timestamps
        scroll to the left as time progresses.  If False, the x-axis tick marks
        show time relative to the current time.
    background: optional
        The background color for the plot.  Accepts any arguments that
        pyqtgraph.mkColor will accept.
    """
    SynchronousMode = 1
    AsynchronousMode = 2

    plot_redrawn_signal = Signal(TimePlotCurveItem)

    def __init__(self,
                 parent=None,
                 init_y_channels=[],
                 plot_by_timestamps=True,
                 background='default'):
        """
        Parameters
        ----------

        parent : Widget
            The parent widget of the chart.
        init_y_channels : list
            A list of scalar channels to plot vs time.
        plot_by_timestamps : bool
            If True, the x-axis shows timestamps as ticks, and those timestamps
            scroll to the left as time progresses.  If False, the x-axis tick
            marks show time relative to the current time.
        background : str, optional
            The background color for the plot.  Accepts any arguments that
            pyqtgraph.mkColor will accept.
        """
        self._plot_by_timestamps = plot_by_timestamps

        self._left_axis = AxisItem("left")
        if plot_by_timestamps:
            self._bottom_axis = TimeAxisItem('bottom')
        else:
            self.starting_epoch_time = time.time()
            self._bottom_axis = AxisItem('bottom')

        super(PyDMTimePlot, self).__init__(parent=parent,
                                           background=background,
                                           axisItems={
                                               "bottom": self._bottom_axis,
                                               "left": self._left_axis
                                           })

        # Removing the downsampling while PR 763 is not merged at pyqtgraph
        # Reference: https://github.com/pyqtgraph/pyqtgraph/pull/763
        # self.setDownsampling(ds=True, auto=True, mode="mean")

        if self._plot_by_timestamps:
            self.plotItem.disableAutoRange(ViewBox.XAxis)
            self.getViewBox().setMouseEnabled(x=False)
        else:
            self.plotItem.setRange(xRange=[DEFAULT_X_MIN, 0], padding=0)
            self.plotItem.setLimits(xMax=0)

        self._bufferSize = DEFAULT_BUFFER_SIZE

        self._time_span = DEFAULT_TIME_SPAN  # This is in seconds
        self._update_interval = DEFAULT_UPDATE_INTERVAL

        self.update_timer = QTimer(self)
        self.update_timer.setInterval(self._update_interval)
        self._update_mode = PyDMTimePlot.SynchronousMode
        self._needs_redraw = True

        self.labels = {"left": None, "right": None, "bottom": None}

        self.units = {"left": None, "right": None, "bottom": None}

        for channel in init_y_channels:
            self.addYChannel(channel)

    def initialize_for_designer(self):
        # If we are in Qt Designer, don't update the plot continuously.
        # This function gets called by PyDMTimePlot's designer plugin.
        self.redraw_timer.setSingleShot(True)

    def addYChannel(self,
                    y_channel=None,
                    name=None,
                    color=None,
                    lineStyle=None,
                    lineWidth=None,
                    symbol=None,
                    symbolSize=None):
        """
        Adds a new curve to the current plot

        Parameters
        ----------
        y_channel : str
            The PV address
        name : str
            The name of the curve (usually made the same as the PV address)
        color : QColor
            The color for the curve
        lineStyle : str
            The line style of the curve, i.e. solid, dash, dot, etc.
        lineWidth : int
            How thick the curve line should be
        symbol : str
            The symbols as markers along the curve, i.e. circle, square,
            triangle, star, etc.
        symbolSize : int
            How big the symbols should be

        Returns
        -------
        new_curve : TimePlotCurveItem
            The newly created curve.
        """
        plot_opts = dict()
        plot_opts['symbol'] = symbol
        if symbolSize is not None:
            plot_opts['symbolSize'] = symbolSize
        if lineStyle is not None:
            plot_opts['lineStyle'] = lineStyle
        if lineWidth is not None:
            plot_opts['lineWidth'] = lineWidth

        # Add curve
        new_curve = TimePlotCurveItem(
            y_channel,
            plot_by_timestamps=self._plot_by_timestamps,
            name=name,
            color=color,
            **plot_opts)
        new_curve.setUpdatesAsynchronously(self.updatesAsynchronously)
        new_curve.setBufferSize(self._bufferSize)

        self.update_timer.timeout.connect(new_curve.asyncUpdate)
        self.addCurve(new_curve, curve_color=color)

        new_curve.data_changed.connect(self.set_needs_redraw)
        self.redraw_timer.start()

        return new_curve

    def removeYChannel(self, curve):
        """
        Remove a curve from the graph. This also stops update the timer
        associated with the curve.

        Parameters
        ----------
        curve : TimePlotCurveItem
            The curve to be removed.
        """
        self.update_timer.timeout.disconnect(curve.asyncUpdate)
        self.removeCurve(curve)
        if len(self._curves) < 1:
            self.redraw_timer.stop()

    def removeYChannelAtIndex(self, index):
        """
        Remove a curve from the graph, given its index in the graph's curve
        list.

        Parameters
        ----------
        index : int
            The curve's index from the graph's curve list.
        """
        curve = self._curves[index]
        self.removeYChannel(curve)

    @Slot()
    def set_needs_redraw(self):
        self._needs_redraw = True

    @Slot()
    def redrawPlot(self):
        """
        Redraw the graph
        """
        if not self._needs_redraw:
            return

        self.updateXAxis()

        for curve in self._curves:
            curve.redrawCurve()
            self.plot_redrawn_signal.emit(curve)
        self._needs_redraw = False

    def updateXAxis(self, update_immediately=False):
        """
        Update the x-axis for every graph redraw.

        Parameters
        ----------
        update_immediately : bool
            Update the axis range(s) immediately if True, or defer until the
            next rendering.
        """
        if len(self._curves) == 0:
            return

        if self._plot_by_timestamps:
            if self._update_mode == PyDMTimePlot.SynchronousMode:
                maxrange = max([curve.max_x() for curve in self._curves])
            else:
                maxrange = time.time()
            minrange = maxrange - self._time_span
            self.plotItem.setXRange(minrange,
                                    maxrange,
                                    padding=0.0,
                                    update=update_immediately)
        else:
            diff_time = self.starting_epoch_time - max(
                [curve.max_x() for curve in self._curves])
            if diff_time > DEFAULT_X_MIN:
                diff_time = DEFAULT_X_MIN
            self.getViewBox().setLimits(minXRange=diff_time)

    def clearCurves(self):
        """
        Remove all curves from the graph.
        """
        super(PyDMTimePlot, self).clear()

    def getCurves(self):
        """
        Dump the current list of curves and each curve's settings into a list
        of JSON-formatted strings.

        Returns
        -------
        settings : list
            A list of JSON-formatted strings, each containing a curve's
            settings
        """
        return [json.dumps(curve.to_dict()) for curve in self._curves]

    def setCurves(self, new_list):
        """
        Add a list of curves into the graph.

        Parameters
        ----------
        new_list : list
            A list of JSON-formatted strings, each contains a curve and its
            settings
        """
        try:
            new_list = [json.loads(str(i)) for i in new_list]
        except ValueError as e:
            logger.exception("Error parsing curve json data: {}".format(e))
            return
        self.clearCurves()
        for d in new_list:
            color = d.get('color')
            if color:
                color = QColor(color)
            self.addYChannel(d['channel'],
                             name=d.get('name'),
                             color=color,
                             lineStyle=d.get('lineStyle'),
                             lineWidth=d.get('lineWidth'),
                             symbol=d.get('symbol'),
                             symbolSize=d.get('symbolSize'))

    curves = Property("QStringList", getCurves, setCurves)

    def findCurve(self, pv_name):
        """
        Find a curve from a graph's curve list.

        Parameters
        ----------
        pv_name : str
            The curve's PV address.

        Returns
        -------
        curve : TimePlotCurveItem
            The found curve, or None.
        """
        for curve in self._curves:
            if curve.address == pv_name:
                return curve

    def refreshCurve(self, curve):
        """
        Remove a curve currently being plotted on the timeplot, then redraw
        that curve, which could have been updated with a new symbol, line
        style, line width, etc.

        Parameters
        ----------
        curve : TimePlotCurveItem
            The curve to be re-added.
        """
        curve = self.findCurve(curve.channel)
        if curve:
            self.removeYChannel(curve)
            self.addYChannel(y_channel=curve.address,
                             color=curve.color,
                             name=curve.address,
                             lineStyle=curve.lineStyle,
                             lineWidth=curve.lineWidth,
                             symbol=curve.symbol,
                             symbolSize=curve.symbolSize)

    def addLegendItem(self, item, pv_name, force_show_legend=False):
        """
        Add an item into the graph's legend.

        Parameters
        ----------
        item : TimePlotCurveItem
            A curve being plotted in the graph
        pv_name : str
            The PV channel
        force_show_legend : bool
            True to make the legend to be displayed; False to just add the
            item, but do not display the legend.
        """
        self._legend.addItem(item, pv_name)
        self.setShowLegend(force_show_legend)

    def removeLegendItem(self, pv_name):
        """
        Remove an item from the legend.

        Parameters
        ----------
        pv_name : str
            The PV channel, used to search for the legend item to remove.
        """
        self._legend.removeItem(pv_name)
        if len(self._legend.items) == 0:
            self.setShowLegend(False)

    def getBufferSize(self):
        """
        Get the size of the data buffer for the entire chart.

        Returns
        -------
        size : int
            The chart's data buffer size.
        """
        return int(self._bufferSize)

    def setBufferSize(self, value):
        """
        Set the size of the data buffer of the entire chart. This will also
        update the same value for each of the data buffer of each chart's
        curve.

        Parameters
        ----------
        value : int
            The new buffer size for the chart.
        """
        if self._bufferSize != int(value):
            # Originally, the bufferSize is the max between the user's input and 1, and 1 doesn't make sense.
            # So, I'm comparing the user's input with the minimum buffer size, and pick the max between the two
            self._bufferSize = max(int(value), MINIMUM_BUFFER_SIZE)
            for curve in self._curves:
                curve.setBufferSize(value)

    def resetBufferSize(self):
        """
        Reset the data buffer size of the chart, and each of the chart's
        curve's data buffer, to the minimum
        """
        if self._bufferSize != DEFAULT_BUFFER_SIZE:
            self._bufferSize = DEFAULT_BUFFER_SIZE
            for curve in self._curves:
                curve.resetBufferSize()

    bufferSize = Property("int", getBufferSize, setBufferSize, resetBufferSize)

    def getUpdatesAsynchronously(self):
        return self._update_mode == PyDMTimePlot.AsynchronousMode

    def setUpdatesAsynchronously(self, value):
        for curve in self._curves:
            curve.setUpdatesAsynchronously(value)
        if value is True:
            self._update_mode = PyDMTimePlot.AsynchronousMode
            self.update_timer.start()
        else:
            self._update_mode = PyDMTimePlot.SynchronousMode
            self.update_timer.stop()

    def resetUpdatesAsynchronously(self):
        self._update_mode = PyDMTimePlot.SynchronousMode
        self.update_timer.stop()
        for curve in self._curves:
            curve.resetUpdatesAsynchronously()

    updatesAsynchronously = Property("bool", getUpdatesAsynchronously,
                                     setUpdatesAsynchronously,
                                     resetUpdatesAsynchronously)

    def getTimeSpan(self):
        """
        The extent of the x-axis of the chart, in seconds.  In other words,
        how long a data point stays on the plot before falling off the left
        edge.

        Returns
        -------
        time_span : float
            The extent of the x-axis of the chart, in seconds.
        """
        return float(self._time_span)

    def setTimeSpan(self, value):
        """
        Set the extent of the x-axis of the chart, in seconds.
        In aynchronous mode, the chart will allocate enough buffer for the new time span duration.
        Data arriving after each duration will be recorded into the buffer
        having been rotated.

        Parameters
        ----------
        value : float
            The time span duration, in seconds, to allocate enough buffer to
            collect data for, before rotating the buffer.
        """
        value = float(value)
        if self._time_span != value:
            self._time_span = value

            if self.getUpdatesAsynchronously():
                self.setBufferSize(
                    int((self._time_span * 1000.0) / self._update_interval))

            self.updateXAxis(update_immediately=True)

    def resetTimeSpan(self):
        """
        Reset the timespan to the default value.
        """
        if self._time_span != DEFAULT_TIME_SPAN:
            self._time_span = DEFAULT_TIME_SPAN
            if self.getUpdatesAsynchronously():
                self.setBufferSize(
                    int((self._time_span * 1000.0) / self._update_interval))
            self.updateXAxis(update_immediately=True)

    timeSpan = Property(float, getTimeSpan, setTimeSpan, resetTimeSpan)

    def getUpdateInterval(self):
        """
        Get the update interval for the chart.

        Returns
        -------
        interval : float
            The update interval of the chart.
        """
        return float(self._update_interval) / 1000.0

    def setUpdateInterval(self, value):
        """
        Set a new update interval for the chart and update its data buffer size.

        Parameters
        ----------
        value : float
            The new update interval value.
        """
        value = abs(int(1000.0 * value))
        if self._update_interval != value:
            self._update_interval = value
            self.update_timer.setInterval(self._update_interval)
            if self.getUpdatesAsynchronously():
                self.setBufferSize(
                    int((self._time_span * 1000.0) / self._update_interval))

    def resetUpdateInterval(self):
        """
        Reset the chart's update interval to the default.
        """
        if self._update_interval != DEFAULT_UPDATE_INTERVAL:
            self._update_interval = DEFAULT_UPDATE_INTERVAL
            self.update_timer.setInterval(self._update_interval)
            if self.getUpdatesAsynchronously():
                self.setBufferSize(
                    int((self._time_span * 1000.0) / self._update_interval))

    updateInterval = Property(float, getUpdateInterval, setUpdateInterval,
                              resetUpdateInterval)

    def getAutoRangeX(self):
        if self._plot_by_timestamps:
            return False
        else:
            super(PyDMTimePlot, self).getAutoRangeX()

    def setAutoRangeX(self, value):
        if self._plot_by_timestamps:
            self._auto_range_x = False
            self.plotItem.enableAutoRange(ViewBox.XAxis,
                                          enable=self._auto_range_x)
        else:
            super(PyDMTimePlot, self).setAutoRangeX(value)

    def channels(self):
        return [curve.channel for curve in self._curves]

    # The methods for autoRangeY, minYRange, and maxYRange are
    # all defined in BasePlot, but we don't expose them as properties there, because not all plot
    # subclasses necessarily want them to be user-configurable in Designer.
    autoRangeY = Property(bool,
                          BasePlot.getAutoRangeY,
                          BasePlot.setAutoRangeY,
                          BasePlot.resetAutoRangeY,
                          doc="""
    Whether or not the Y-axis automatically rescales to fit the data.
    If true, the values in minYRange and maxYRange are ignored.
    """)

    minYRange = Property(float,
                         BasePlot.getMinYRange,
                         BasePlot.setMinYRange,
                         doc="""
    Minimum Y-axis value visible on the plot.""")

    maxYRange = Property(float,
                         BasePlot.getMaxYRange,
                         BasePlot.setMaxYRange,
                         doc="""
    Maximum Y-axis value visible on the plot.""")

    def enableCrosshair(self,
                        is_enabled,
                        starting_x_pos=DEFAULT_X_MIN,
                        starting_y_pos=DEFAULT_Y_MIN,
                        vertical_angle=90,
                        horizontal_angle=0,
                        vertical_movable=False,
                        horizontal_movable=False):
        """
        Display a crosshair on the graph.

        Parameters
        ----------
        is_enabled : bool
            True is to display the crosshair; False is to hide it.
        starting_x_pos : float
            The x position where the vertical line will cross
        starting_y_pos : float
            The y position where the horizontal line will cross
        vertical_angle : int
            The angle of the vertical line
        horizontal_angle : int
            The angle of the horizontal line
        vertical_movable : bool
            True if the user can move the vertical line; False if not
        horizontal_movable : bool
            True if the user can move the horizontal line; False if not
        """
        super(PyDMTimePlot,
              self).enableCrosshair(is_enabled, starting_x_pos, starting_y_pos,
                                    vertical_angle, horizontal_angle,
                                    vertical_movable, horizontal_movable)
示例#13
0
class LEDButton(ActionButton):

    RULE_PROPERTIES = ActionButton.RULE_PROPERTIES.copy()
    RULE_PROPERTIES.update({
        'LED On': ['setLedState', bool],
        'LED Flashing': ['setLedFlashing', bool]
    })

    def __init__(self, parent=None):
        super(LEDButton, self).__init__(parent)

        self._alignment = Qt.AlignRight | Qt.AlignTop
        self.led = LEDWidget(self)
        self.led.setDiameter(14)
        self.placeLed()

    def placeLed(self):
        x = 0
        y = 0
        alignment = self._alignment
        ledDiameter = self.led.getDiameter()
        halfLed = ledDiameter / 2
        quarterLed = ledDiameter / 4 # cheap hueristic to avoid borders

        if alignment & Qt.AlignLeft:
            x = quarterLed
        elif alignment & Qt.AlignRight:
            x = self.width() - ledDiameter - quarterLed
        elif alignment & Qt.AlignHCenter:
            x = (self.width()/2) - halfLed
        elif alignment & Qt.AlignJustify:
            x = 0

        if alignment & Qt.AlignTop:
            y = quarterLed
        elif alignment & Qt.AlignBottom:
            y = self.height() - ledDiameter - quarterLed
        elif alignment & Qt.AlignVCenter:
            y = self.height()/2 - halfLed
        # print x, y
        self.led.move(x, y)
        self.updateGeometry()

    def resizeEvent(self, event):
        self.placeLed()

    def update(self):
        # self.placeLed()
        # super(LEDButton, self).update()
        pass

    def sizeHint( self ):
        return QSize(80, 30)

    @Slot(bool)
    def setLedState(self, state):
        self.led.setState(state)

    @Slot(bool)
    def setLedFlashing(self, flashing):
        self.led.setFlashing(flashing)

    def getLedDiameter(self):
        return self.led.getDiameter()

    @Slot(int)
    def setLedDiameter(self, value):
        self.led.setDiameter(value)
        self.placeLed()

    def getLedColor(self):
        return self.led.getColor()

    @Slot(QColor)
    def setLedColor(self, value):
        self.led.setColor(value)

    def getAlignment(self):
        return self._alignment

    @Slot(Qt.Alignment)
    def setAlignment(self, value):
        self._alignment = Qt.Alignment(value)
        self.update()

    diameter = Property(int, getLedDiameter, setLedDiameter)
    color = Property(QColor, getLedColor, setLedColor)
    alignment = Property(Qt.Alignment, getAlignment, setAlignment)
示例#14
0
class LEDWidget(QWidget):
    def __init__(self, parent=None):

        super(LEDWidget, self).__init__(parent)

        self._diamX = 0
        self._diamY = 0
        self._diameter = 30
        self._color = QColor("red")
        self._alignment = Qt.AlignCenter
        self._state = True
        self._flashing = False
        self._flashRate = 200

        self._timer = QTimer()
        self._timer.timeout.connect(self.toggleState)

        self.setDiameter(self._diameter)

    def paintEvent(self, event):
        painter = QPainter()
        x = 0
        y = 0
        if self._alignment & Qt.AlignLeft:
            x = 0
        elif self._alignment & Qt.AlignRight:
            x = self.width() - self._diameter
        elif self._alignment & Qt.AlignHCenter:
            x = (self.width() - self._diameter) / 2
        elif self._alignment & Qt.AlignJustify:
            x = 0

        if self._alignment & Qt.AlignTop:
            y = 0
        elif self._alignment & Qt.AlignBottom:
            y = self.height() - self._diameter
        elif self._alignment & Qt.AlignVCenter:
            y = (self.height() - self._diameter) / 2

        gradient = QRadialGradient(x + self._diameter / 2,
                                   y + self._diameter / 2,
                                   self._diameter * 0.3, self._diameter * 0.1,
                                   self._diameter * 0.1)
        gradient.setColorAt(0, Qt.white)

        # ensure the border/halo is same color as gradient
        draw_color = QColor(self._color)

        if not self._state:
            # cut to black @ 70% for darker effect
            draw_color = QColor(Qt.black)

        if not self.isEnabled():
            draw_color.setAlpha(30)

        pen_color = draw_color
        gradient.setColorAt(0.7, draw_color)

        painter.begin(self)
        brush = QBrush(gradient)
        painter.setPen(pen_color)
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setBrush(brush)
        painter.drawEllipse(x + 1, y + 1, self._diameter - 2,
                            self._diameter - 2)

        if self._flashRate > 0 and self._flashing:
            self._timer.start(self._flashRate)
        else:
            self._timer.stop()

        painter.end()

    def minimumSizeHint(self):
        return QSize(self._diameter, self._diameter)

    def sizeHint(self):
        return QSize(self._diameter, self._diameter)

    def getDiameter(self):
        return self._diameter

    @Slot(int)
    def setDiameter(self, value):
        self._diameter = value
        self.adjustSize()
        self.update()

    def getColor(self):
        return self._color

    @Slot(QColor)
    def setColor(self, value):
        self._color = value
        self._disabledColor = QColor(self._color)
        self._disabledColor.setAlpha(30)
        self.update()

    def getAlignment(self):
        return self._alignment

    @Slot(Qt.Alignment)
    def setAlignment(self, value):
        self._alignment = value
        self.update()

    def getState(self):
        return self._state

    @Slot(bool)
    def setState(self, value):
        self._state = value
        self.update()

    @Slot()
    def toggleState(self):
        self._state = not self._state
        self.update()

    def isFlashing(self):
        return self._flashing

    @Slot(bool)
    def setFlashing(self, value):
        self._flashing = value
        self.update()

    def getFlashRate(self):
        return self._flashRate

    @Slot(int)
    def setFlashRate(self, value):
        self._flashRate = value
        self.update()

    @Slot()
    def startFlashing(self):
        self.setFlashing(True)

    @Slot()
    def stopFlashing(self):
        self.setFlashing(False)

    diameter = Property(int, getDiameter, setDiameter)
    color = Property(QColor, getColor, setColor)
    alignment = Property(Qt.Alignment, getAlignment, setAlignment)
    state = Property(bool, getState, setState)
    flashing = Property(bool, isFlashing, setFlashing)
    flashRate = Property(int, getFlashRate, setFlashRate)
示例#15
0
class GcodeBackplot(QBackPlot):
    line_selected = Signal(int)
    gcode_error = Signal(str)

    def __init__(self, parent=None, standalone=False):
        super(GcodeBackplot, self).__init__(parent)

        # This prevents doing unneeded initialization
        # when QtDesginer loads the plugin.
        if parent is None and not standalone:
            return

        self.show_overlay = False  # no DRO or DRO overlay
        self.program_alpha = True
        self.grid_size = 1
        self._reload_filename = None

        # Add loading progress bar and abort button
        self.progressBar = QProgressBar(visible=False)
        self.progressBar.setFormat("Loading backplot: %p%")
        self.abortButton = QPushButton('Abort', visible=False)

        hBox = QHBoxLayout()
        hBox.addWidget(self.progressBar)
        hBox.addWidget(self.abortButton)

        vBox = QVBoxLayout(self)
        vBox.addStretch()
        vBox.addLayout(hBox)

        self.abortButton.clicked.connect(self.abort)

        STATUS.actual_position.onValueChanged(self.update)
        STATUS.joint_actual_position.onValueChanged(self.update)
        STATUS.homed.onValueChanged(self.update)
        STATUS.limit.onValueChanged(self.update)
        STATUS.tool_in_spindle.onValueChanged(self.update)
        STATUS.motion_mode.onValueChanged(self.update)
        STATUS.current_vel.onValueChanged(self.update)

        STATUS.g5x_offset.onValueChanged(self.reloadBackplot)
        STATUS.g92_offset.onValueChanged(self.reloadBackplot)

        # Connect status signals
        STATUS.file.notify(self.loadBackplot)
        # STATUS.reload_backplot.notify(self.reloadBackplot)
        STATUS.program_units.notify(lambda v: self.setMetricUnits(v == 2))

    def loadBackplot(self, fname):
        LOG.debug('load the display: {}'.format(fname.encode('utf-8')))
        self._reload_filename = fname
        self.load(fname)

    @Slot()
    def reloadBackplot(self):
        QTimer.singleShot(100, lambda: self._reloadBackplot())

    def _reloadBackplot(self):
        LOG.debug('reload the display: {}'.format(self._reload_filename))
        dist = self.get_zoom_distance()
        try:
            self.load(self._reload_filename)
            self.set_zoom_distance(dist)
        except:
            LOG.warning("Problem reloading backplot file: {}".format(self._reload_filename), exc_info=True)

    # ==========================================================================
    #  Override QBackPlot methods
    # ==========================================================================

    def report_loading_started(self):
        self.progressBar.show()
        self.abortButton.show()
        self.start = time.time()

    def report_progress_percentage(self, percentage):
        QApplication.processEvents()
        self.progressBar.setValue(percentage)

    def report_loading_finished(self):
        print((time.time() - self.start))
        self.progressBar.hide()
        self.abortButton.hide()

    # overriding functions
    def report_gcode_error(self, result, seq, filename):
        error = gcode.strerror(result)
        file = os.path.basename(filename)
        line = seq - 1
        msg = "G-code error in '{}' near line {}: {}".format(file, line, error)
        LOG.error(msg)
        STATUS.backplot_gcode_error.emit(msg)

    # Override gremlin's / glcannon.py function so we can emit a GObject signal
    def update_highlight_variable(self, line):
        self.highlight_line = line
        if line is None:
            line = -1
        STATUS.backplot_line_selected.emit(line)

    # ==============================================================================
    #  QtDesigner property setters/getters
    # ==============================================================================

    @Slot(str)
    def setView(self, view):
        view = view.lower()
        if self.is_lathe:
            if view not in ['p', 'y', 'y2']:
                return False
        elif view not in ['p', 'x', 'y', 'z', 'z2']:
            return False
        self.current_view = view
        if self.initialised:
            self.set_current_view()

    def getView(self):
        return self.current_view

    defaultView = Property(str, getView, setView)

    @Slot()
    def setViewP(self):
        self.setView('p')

    @Slot()
    def setViewX(self):
        self.setView('x')

    @Slot()
    def setViewY(self):
        self.setView('y')

    @Slot()
    def setViewZ(self):
        self.setView('z')

    @Slot()
    def setViewZ2(self):
        self.setView('z2')

    @Slot()
    def clearLivePlot(self):
        self.clear_live_plotter()

    @Slot()
    def zoomIn(self):
        self.zoomin()

    @Slot()
    def zoomOut(self):
        self.zoomout()

    @Slot(bool)
    def alphaBlend(self, alpha):
        self.program_alpha = alpha
        self.update()

    @Slot(bool)
    def showGrid(self, grid):
        self.grid_size = int(grid)  # ugly hack for now
        self.update()

    # @Slot(str) Fixme check for the correct data type
    def setdro(self, state):
        self.enable_dro = state
        self.updateGL()

    def getdro(self):
        return self.enable_dro

    _dro = Property(bool, getdro, setdro)

    # DTG

    # @Slot(str) Fixme check for the correct data type
    def setdtg(self, state):
        self.show_dtg = state
        self.updateGL()

    def getdtg(self):
        return self.show_dtg

    _dtg = Property(bool, getdtg, setdtg)

    # METRIC

    # @Slot(str) Fixme check for the correct data type
    def setMetricUnits(self, metric):
        self.metric_units = metric
        self.updateGL()

    def getMetricUnits(self):
        return self.metric_units

    metricUnits = Property(bool, getMetricUnits, setMetricUnits)

    # @Slot(str) Fixme check for the correct data type
    def setProgramAlpha(self, alpha):
        self.program_alpha = alpha
        self.updateGL()

    def getProgramAlpha(self):
        return self.program_alpha

    renderProgramAlpha = Property(bool, getProgramAlpha, setProgramAlpha)

    # @Slot(str) Fixme check for the correct data type
    def setBackgroundColor(self, color):
        self.colors['back'] = color.getRgbF()[:3]
        self.updateGL()

    def getBackgroundColor(self):
        r, g, b = self.colors['back']
        color = QColor()
        color.setRgbF(r, g, b, 1.0)
        return color

    backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor)
示例#16
0
class JogIncrementWidget(QWidget):
    def __init__(self, parent=None, standalone=False):
        super(JogIncrementWidget, self).__init__(parent)

        self._container = hBox = QBoxLayout(QBoxLayout.LeftToRight, self)

        hBox.setContentsMargins(0, 0, 0, 0)
        self._ledDiameter = 15
        self._ledColor = QColor('green')
        self._alignment = Qt.AlignTop | Qt.AlignRight
        # This prevents doing unneeded initialization
        # when QtDesginer loads the plugin.
        if parent is None and not standalone:
            return

        increments = INFO.getIncrements()
        for increment in increments:
            button = LEDButton()
            button.setCheckable(True)
            button.setAutoExclusive(True)
            button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
            button.setMinimumSize(50, 42)

            if increment != 0:
                raw_increment = increment.strip()
                # print '[', raw_increment, ']'
                button.setText(raw_increment)
                button.clicked.connect(self.setJogIncrement)
                hBox.addWidget(button)

        self.placeLed()

    def setJogIncrement(self):
        setSetting('machine.jog.increment', self.sender().text())

    def layoutWidgets(self, layout):
        return (layout.itemAt(i) for i in range(layout.count()))

    def placeLed(self):
        for w in self.layoutWidgets(self._container):
            w.widget().setLedDiameter(self._ledDiameter)
            w.widget().setLedColor(self._ledColor)
            w.widget().setAlignment(self._alignment)

    def getLedDiameter(self):
        return self._ledDiameter

    @Slot(int)
    def setLedDiameter(self, value):
        self._ledDiameter = value
        self.placeLed()

    def getLedColor(self):
        return self._ledColor

    @Slot(QColor)
    def setLedColor(self, value):
        self._ledColor = value
        self.placeLed()

    def getAlignment(self):
        return self._alignment

    @Slot(Qt.Alignment)
    def setAlignment(self, value):
        self._alignment = Qt.Alignment(value)
        self.placeLed()

    def getOrientation(self):
        if self._container.direction() == QBoxLayout.LeftToRight:
            return Qt.Horizontal
        else:
            return Qt.Vertical

    @Slot(Qt.Orientation)
    def setOrientation(self, value):
        if value == Qt.Horizontal:
            self._container.setDirection(QBoxLayout.LeftToRight)
        else:
            self._container.setDirection(QBoxLayout.TopToBottom)
        self.adjustSize()

    def getLayoutSpacing(self):
        return self._container.spacing()

    @Slot(int)
    def setLayoutSpacing(self, value):
        self._container.setSpacing(value)

    diameter = Property(int, getLedDiameter, setLedDiameter)
    color = Property(QColor, getLedColor, setLedColor)
    alignment = Property(Qt.Alignment, getAlignment, setAlignment)
    orientation = Property(Qt.Orientation, getOrientation, setOrientation)
    layoutSpacing = Property(int, getLayoutSpacing, setLayoutSpacing)
示例#17
0
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()
示例#18
0
class GcodeEditor(EditorBase, QObject):
    ARROW_MARKER_NUM = 8

    def __init__(self, parent=None):
        super(GcodeEditor, self).__init__(parent)

        self._last_filename = None
        self.auto_show_mdi = True
        self.last_line = None
        # self.setEolVisibility(True)

        STATUS.file.notify(self.load_program)
        STATUS.motion_line.onValueChanged(self.highlight_line)
        # STATUS.connect('line-changed', self.highlight_line)
        # if self.idle_line_reset:
        #     STATUS.connect('interp_idle', lambda w: self.set_line_number(None, 0))

        # QSS Hack

        self._backgroundcolor = ''
        self.backgroundcolor = self._backgroundcolor

        self._marginbackgroundcolor = ''
        self.marginbackgroundcolor = self._marginbackgroundcolor

    @Property(str)
    def backgroundcolor(self):
        """Property to set the background color of the GCodeEditor (str).

        sets the background color of the GCodeEditor
        """
        return self._backgroundcolor

    @backgroundcolor.setter
    def backgroundcolor(self, color):
        self._backgroundcolor = color
        self.set_background_color(color)

    @Property(str)
    def marginbackgroundcolor(self):
        """Property to set the background color of the GCodeEditor margin (str).

        sets the background color of the GCodeEditor margin
        """
        return self._marginbackgroundcolor

    @marginbackgroundcolor.setter
    def marginbackgroundcolor(self, color):
        self._marginbackgroundcolor = color
        self.set_margin_background_color(color)

    def load_program(self, fname=None):
        if fname is None:
            fname = self._last_filename
        else:
            self._last_filename = fname
        self.load_text(fname)
        # self.zoomTo(6)
        self.setCursorPosition(0, 0)

    # when switching from MDI to AUTO we need to reload the
    # last (linuxcnc loaded) program.
    def reload_last(self):
        self.load_text(STATUS.old['file'])
        self.setCursorPosition(0, 0)

    # With the auto_show__mdi option, MDI history is shown
    def load_mdi(self):
        self.load_text(INFO.MDI_HISTORY_PATH)
        self._last_filename = INFO.MDI_HISTORY_PATH
        # print 'font point size', self.font().pointSize()
        # self.zoomTo(10)
        # print 'font point size', self.font().pointSize()
        self.setCursorPosition(self.lines(), 0)

    # With the auto_show__mdi option, MDI history is shown
    def load_manual(self):
        if STATUS.is_man_mode():
            self.load_text(INFO.MACHINE_LOG_HISTORY_PATH)
            self.setCursorPosition(self.lines(), 0)

    def load_text(self, fname):
        try:
            fp = os.path.expanduser(fname)
            self.setText(open(fp).read())
        except:
            LOG.error('File path is not valid: {}'.format(fname))
            self.setText('')
            return

        self.last_line = None
        self.ensureCursorVisible()
        self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET)

    def highlight_line(self, line):
        # if STATUS.is_auto_running():
        #     if not STATUS.old['file'] == self._last_filename:
        #         LOG.debug('should reload the display')
        #         self.load_text(STATUS.old['file'])
        #         self._last_filename = STATUS.old['file']
        self.markerAdd(line, self.ARROW_MARKER_NUM)
        if self.last_line:
            self.markerDelete(self.last_line, self.ARROW_MARKER_NUM)
        self.setCursorPosition(line, 0)
        self.ensureCursorVisible()
        self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET)
        self.last_line = line

    def set_line_number(self, line):
        pass

    def line_changed(self, line, index):
        # LOG.debug('Line changed: {}'.format(STATUS.is_auto_mode()))
        self.line_text = str(self.text(line)).strip()
        self.line = line
        if STATUS.is_mdi_mode() and STATUS.is_auto_running() is False:
            STATUS.emit('mdi-line-selected', self.line_text,
                        self._last_filename)

    def select_lineup(self):
        line, col = self.getCursorPosition()
        LOG.debug(line)
        self.setCursorPosition(line - 1, 0)
        self.highlight_line(line - 1)

    def select_linedown(self):
        line, col = self.getCursorPosition()
        LOG.debug(line)
        self.setCursorPosition(line + 1, 0)
        self.highlight_line(line + 1)

    # designer recognized getter/setters
    # auto_show_mdi status
    def set_auto_show_mdi(self, data):
        self.auto_show_mdi = data

    def get_auto_show_mdi(self):
        return self.auto_show_mdi

    def reset_auto_show_mdi(self):
        self.auto_show_mdi = True

    auto_show_mdi_status = Property(bool, get_auto_show_mdi, set_auto_show_mdi,
                                    reset_auto_show_mdi)
示例#19
0
class HALLEDButton(QPushButton):
    """HAL LED Button"""
    def __init__(self, parent=None):
        super(HALLEDButton, self).__init__(parent)

        self._alignment = Qt.AlignRight | Qt.AlignTop
        self._pin_name = ''
        self._flash_pin_name = ''
        # self.setCheckable(True)
        self.led = LEDWidget(self)
        self.led.setDiameter(14)
        # self.toggled.connect(self.updateState)
        # self.updateState()
        self.placeLed()

    def placeLed(self):
        x = 0
        y = 0
        alignment = self._alignment
        ledDiameter = self.led.getDiameter()
        halfLed = ledDiameter / 2
        quarterLed = ledDiameter /4 # cheap hueristic to avoid borders

        if alignment & Qt.AlignLeft:
            x = quarterLed
        elif alignment & Qt.AlignRight:
            x = self.width() - ledDiameter - quarterLed
        elif alignment & Qt.AlignHCenter:
            x = (self.width()/2) - halfLed
        elif alignment & Qt.AlignJustify:
            x = 0

        if alignment & Qt.AlignTop:
            y = quarterLed
        elif alignment & Qt.AlignBottom:
            y = self.height() - ledDiameter - quarterLed
        elif alignment & Qt.AlignVCenter:
            y = self.height()/2 - halfLed
        # print(x, y)
        self.led.move(x, y)
        self.updateGeometry()

    def resizeEvent(self, event):
        self.placeLed()

    def update(self):
        self.placeLed()
        super(LEDButton, self).update()

    def updateState(self, state):
        self.led.setState(state)

    def updateFlashing(self, flashing):
        self.led.setFlashing(flashing)

    def sizeHint( self ):
        return QSize(80, 30)

    def getLedDiameter(self):
        return self.led.getDiameter()

    @Slot(int)
    def setLedDiameter(self, value):
        self.led.setDiameter(value)
        self.placeLed()

    def getLedColor(self):
        return self.led.getColor()

    @Slot(QColor)
    def setLedColor(self, value):
        self.led.setColor(value)

    def getAlignment(self):
        return self._alignment

    @Slot(Qt.Alignment)
    def setAlignment(self, value):
        self._alignment = Qt.Alignment(value)
        self.update()

    diameter = Property(int, getLedDiameter, setLedDiameter)
    color = Property(QColor, getLedColor, setLedColor)
    alignment = Property(Qt.Alignment, getAlignment, setAlignment)

    @Property(str)
    def flashPinName(self):
        """The `actionName` property for setting the action the button
            should trigger from within QtDesigner.

        Returns:
            str : The action name.
        """
        return self._flash_pin_name

    @flashPinName.setter
    def flashPinName(self, flash_pin_name):
        """Sets the name of the action the button should trigger and
            binds the widget to that action.

        Args:
            action_name (str) : A fully qualified action name.
        """
        self._flash_pin_name = flash_pin_name
        try:
            hal_pin = hal_status.getHALPin(flash_pin_name)
        except ValueError:
            return
        hal_pin.connect(self.updateState)


    @Property(str)
    def pinName(self):
        """The `actionName` property for setting the action the button
            should trigger from within QtDesigner.

        Returns:
            str : The action name.
        """
        return self._pin_name

    @pinName.setter
    def pinName(self, pin_name):
        """Sets the name of the action the button should trigger and
            binds the widget to that action.

        Args:
            action_name (str) : A fully qualified action name.
        """
        self._pin_name = pin_name
        try:
            hal_pin = hal_status.getHALPin(pin_name)
        except ValueError:
            return
        hal_pin.connect(self.updateState)
示例#20
0
class QRangeSlider(QWidget):
    """
    QRangeSlider class, super class for QVRangeSlider and QHRangeSlider.
    """

    valuesChanged = Signal(tuple)
    rangeChanged = Signal(tuple)
    collapsedChanged = Signal(bool)
    focused = Signal()
    resized = Signal()

    def __init__(
        self,
        initial_values=None,
        data_range=None,
        step_size=None,
        collapsible=True,
        collapsed=False,
        parent=None,
    ):
        """A range slider with two handles for min/max values.

        Values should be provided in the range of the underlying data.
        (normalization to 0-1 happens internally in the slider.sliderValues())

        Parameters
        ----------
        initial_values : 2-tuple, optional
            Initial min & max values of the slider, defaults to (0.2, 0.8)
        data_range : 2-tuple, optional
            Min and max of the slider range, defaults to (0, 1)
        step_size : float, optional
            Single step size for the slider, defaults to 1
        collapsible : bool
            Whether the slider is collapsible, defaults to True.
        collapsed : bool
            Whether the slider begins collapsed, defaults to False.
        parent : qtpy.QtWidgets.QWidget
            Parent widget.
        """
        super().__init__(parent)
        self.handle_radius = 8
        self.slider_width = 8
        self.moving = "none"
        self.collapsible = collapsible
        self.collapsed = collapsed
        self.prev_moving = None
        self.bc_min = None
        self.bc_max = None

        # Variables initialized in methods
        self.value_min = 0
        self.value_max = 1
        self.start_display_min = None
        self.start_display_max = None
        self.start_pos = None
        self.display_min = None
        self.display_max = None

        self.setBarColor(QColor(200, 200, 200))
        self.setBackgroundColor(QColor(100, 100, 100))
        self.setHandleColor(QColor(200, 200, 200))
        self.setHandleBorderColor(QColor(200, 200, 200))

        self.setRange((0, 100) if data_range is None else data_range)
        self.setValues((20, 80) if initial_values is None else initial_values)
        if step_size is None:
            # pick an appropriate slider step size based on the data range
            if data_range is not None:
                step_size = (data_range[1] - data_range[0]) / 1000
            else:
                step_size = 0.001
        self.setStep(step_size)
        if not parent:
            if 'HRange' in self.__class__.__name__:
                self.setGeometry(200, 200, 200, 20)
            else:
                self.setGeometry(200, 200, 20, 200)

    def range(self):
        """Min and max possible values for the slider range. In data units"""
        return self.data_range_min, self.data_range_max

    def setRange(self, values):
        """Min and max possible values for the slider range. In data units."""
        validate_2_tuple(values)
        self.data_range_min, self.data_range_max = values
        self.rangeChanged.emit(self.range())
        self.updateDisplayPositions()

    def values(self):
        """Current slider values.

        Returns
        -------
        tuple
            Current minimum and maximum values of the range slider
        """
        return tuple(
            [self._slider_to_data_value(v) for v in self.sliderValues()])

    def setValues(self, values):
        self.setSliderValues([self._data_to_slider_value(v) for v in values])

    def sliderValues(self):
        """Current slider values, as a fraction of slider width.

        Returns
        -------
        values : 2-tuple of int
            Start and end of the range.
        """
        return self.value_min, self.value_max

    def setSliderValues(self, values):
        """Set current slider values, as a fraction of slider width.

        Parameters
        ----------
        values : 2-tuple of float or int
            Start and end of the range.
        """
        validate_2_tuple(values)
        self.value_min, self.value_max = values
        self.valuesChanged.emit(self.values())
        self.updateDisplayPositions()

    def setStep(self, step):
        self._step = step

    @property
    def single_step(self):
        return self._step / self.scale

    def mouseMoveEvent(self, event):
        if not self.isEnabled():
            return

        size = self.rangeSliderSize()
        pos = self.getPos(event)
        if self.moving == "min":
            if pos <= self.handle_radius:
                self.display_min = self.handle_radius
            elif pos > self.display_max - self.handle_radius / 2:
                self.display_min = self.display_max - self.handle_radius / 2
            else:
                self.display_min = pos
        elif self.moving == "max":
            if pos >= size + self.handle_radius:
                self.display_max = size + self.handle_radius
            elif pos < self.display_min + self.handle_radius / 2:
                self.display_max = self.display_min + self.handle_radius / 2
            else:
                self.display_max = pos
        elif self.moving == "bar":
            width = self.start_display_max - self.start_display_min
            lower_part = self.start_pos - self.start_display_min
            upper_part = self.start_display_max - self.start_pos
            if pos + upper_part >= size + self.handle_radius:
                self.display_max = size + self.handle_radius
                self.display_min = self.display_max - width
            elif pos - lower_part <= self.handle_radius:
                self.display_min = self.handle_radius
                self.display_max = self.display_min + width
            else:
                self.display_min = pos - lower_part
                self.display_max = self.display_min + width

        self.updateValuesFromDisplay()

    def mousePressEvent(self, event):
        if not self.isEnabled():
            return

        pos = self.getPos(event)
        top = self.rangeSliderSize() + self.handle_radius
        if event.button() == Qt.LeftButton:
            if not self.collapsed:
                if abs(self.display_min - pos) <= (self.handle_radius):
                    self.moving = "min"
                elif abs(self.display_max - pos) <= (self.handle_radius):
                    self.moving = "max"
                elif pos > self.display_min and pos < self.display_max:
                    self.moving = "bar"
                elif pos > self.display_max and pos < top:
                    self.display_max = pos
                    self.moving = "max"
                    self.updateValuesFromDisplay()
                elif pos < self.display_min and pos > self.handle_radius:
                    self.display_min = pos
                    self.moving = "min"
                    self.updateValuesFromDisplay()
            else:
                self.moving = "bar"
                if pos > self.handle_radius and pos < top:
                    self.display_max = pos
                    self.display_min = pos
        else:
            if self.collapsible:
                if self.collapsed:
                    self.expand()
                else:
                    self.collapse()
                self.collapsedChanged.emit(self.collapsed)

        self.start_display_min = self.display_min
        self.start_display_max = self.display_max
        self.start_pos = pos
        self.focused.emit()

    def mouseReleaseEvent(self, event):
        if self.isEnabled():
            if not (self.moving == "none"):
                self.valuesChanged.emit(self.values())
            self.moving = "none"

    def collapse(self):
        self.bc_min, self.bc_max = self.value_min, self.value_max
        midpoint = (self.value_max + self.value_min) / 2
        min_value = midpoint
        max_value = midpoint
        self.setSliderValues((min_value, max_value))
        self.collapsed = True

    def expand(self):
        _mid = (self.bc_max - self.bc_min) / 2
        min_value = self.value_min - _mid
        max_value = self.value_min + _mid
        if min_value < 0:
            min_value = 0
            max_value = self.bc_max - self.bc_min
        elif max_value > 1:
            max_value = 1
            min_value = max_value - (self.bc_max - self.bc_min)
        self.setSliderValues((min_value, max_value))
        self.collapsed = False

    def resizeEvent(self, event):
        self.updateDisplayPositions()
        self.resized.emit()

    def updateDisplayPositions(self):
        size = self.rangeSliderSize()
        range_min = int(size * self.value_min)
        range_max = int(size * self.value_max)
        self.display_min = range_min + self.handle_radius
        self.display_max = range_max + self.handle_radius
        self.update()

    def _data_to_slider_value(self, value):
        rmin, rmax = self.range()
        return (value - rmin) / self.scale

    def _slider_to_data_value(self, value):
        rmin, rmax = self.range()
        return rmin + value * self.scale

    @property
    def scale(self):
        return self.data_range_max - self.data_range_min

    def updateValuesFromDisplay(self):
        size = self.rangeSliderSize()
        val_min, val_max = self.sliderValues()
        if (self.moving == "min") or (self.moving == "bar"):
            scale_min = (self.display_min - self.handle_radius) / size
            ratio = round(scale_min / self.single_step)
            val_min = ratio * self.single_step
        if (self.moving == "max") or (self.moving == "bar"):
            scale_max = (self.display_max - self.handle_radius) / size
            ratio = round(scale_max / self.single_step)
            val_max = ratio * self.single_step
        self.setSliderValues((val_min, val_max))

    def getBarColor(self):
        return self.bar_color

    def setBarColor(self, barColor):
        self.bar_color = barColor

    barColor = Property(QColor, getBarColor, setBarColor)

    def getBackgroundColor(self):
        return self.background_color

    def setBackgroundColor(self, backgroundColor):
        self.background_color = backgroundColor

    backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor)

    @property
    def handle_width(self):
        return self.handle_radius * 2

    def getHandleColor(self):
        return self.handle_color

    def setHandleColor(self, handleColor):
        self.handle_color = handleColor

    handleColor = Property(QColor, getHandleColor, setHandleColor)

    def getHandleBorderColor(self):
        return self.handle_border_color

    def setHandleBorderColor(self, handleBorderColor):
        self.handle_border_color = handleBorderColor

    handleBorderColor = Property(QColor, getHandleBorderColor,
                                 setHandleBorderColor)

    def setEnabled(self, bool):
        super().setEnabled(bool)
        self.update()
示例#21
0
class PyDMWaveformPlot(BasePlot):
    """
    PyDMWaveformPlot is a widget to plot one or more waveforms.

    Each curve can plot either a Y-axis waveform vs. its indices,
    or a Y-axis waveform against an X-axis waveform.

    Parameters
    ----------
    parent : optional
        The parent of this widget.
    init_x_channels: optional
        init_x_channels can be a string with the address for a channel,
        or a list of strings, each containing an address for a channel.
        If not specified, y-axis waveforms will be plotted against their
        indices. If a list is specified for both init_x_channels and
        init_y_channels, they both must have the same length.
        If a single x channel was specified, and a list of y channels are
        specified, all y channels will be plotted against the same x channel.
    init_y_channels: optional
        init_y_channels can be a string with the address for a channel,
        or a list of strings, each containing an address for a channel.
        If a list is specified for both init_x_channels and init_y_channels,
        they both must have the same length.
        If a single x channel was specified, and a list of y channels are
        specified, all y channels will be plotted against the same x channel.
    background: optional
        The background color for the plot.  Accepts any arguments that
        pyqtgraph.mkColor will accept.
    """
    def __init__(self,
                 parent=None,
                 init_x_channels=[],
                 init_y_channels=[],
                 background='default'):
        super(PyDMWaveformPlot, self).__init__(parent, background)
        # If the user supplies a single string instead of a list,
        # wrap it in a list.
        if isinstance(init_x_channels, str):
            init_x_channels = [init_x_channels]
        if isinstance(init_y_channels, str):
            init_y_channels = [init_y_channels]
        if len(init_x_channels) == 0:
            init_x_channels = list(itertools.repeat(None,
                                                    len(init_y_channels)))
        if len(init_x_channels) != len(init_y_channels):
            raise ValueError("If lists are provided for both X and Y " +
                             "channels, they must be the same length.")
        # self.channel_pairs is an ordered dictionary that is keyed on a
        # (x_channel, y_channel) tuple, with WaveformCurveItem values.
        # It gets populated in self.addChannel().
        self.channel_pairs = OrderedDict()
        init_channel_pairs = zip(init_x_channels, init_y_channels)
        for (x_chan, y_chan) in init_channel_pairs:
            self.addChannel(y_chan, x_channel=x_chan)

    def initialize_for_designer(self):
        # If we are in Qt Designer, don't update the plot continuously.
        # This function gets called by PyDMTimePlot's designer plugin.
        pass

    def addChannel(self,
                   y_channel=None,
                   x_channel=None,
                   name=None,
                   color=None,
                   lineStyle=None,
                   lineWidth=None,
                   symbol=None,
                   symbolSize=None,
                   redraw_mode=None):
        """
        Add a new curve to the plot.  In addition to the arguments below,
        all other keyword arguments are passed to the underlying
        pyqtgraph.PlotDataItem used to draw the curve.

        Parameters
        ----------
        y_channel: str
            The address for the y channel for the curve.
        x_channel: str, optional
            The address for the x channel for the curve.
        name: str, optional
            A name for this curve.  The name will be used in the plot legend.
        color: str or QColor, optional
            A color for the line of the curve.  If not specified, the plot will
            automatically assign a unique color from a set of default colors.
        lineStyle: int, optional
            Style of the line connecting the data points.
            0 means no line (scatter plot).
        lineWidth: int, optional
            Width of the line connecting the data points.
        redraw_mode: int, optional
            WaveformCurveItem.REDRAW_ON_EITHER: (Default)
                Redraw after either X or Y receives new data.
            WaveformCurveItem.REDRAW_ON_X:
                Redraw after X receives new data.
            WaveformCurveItem.REDRAW_ON_Y:
                Redraw after Y receives new data.
            WaveformCurveItem.REDRAW_ON_BOTH:
                Redraw after both X and Y receive new data.
        symbol: str or None, optional
            Which symbol to use to represent the data.
        symbol: int, optional
            Size of the symbol.
        """
        plot_opts = {}
        plot_opts['symbol'] = symbol
        if symbolSize is not None:
            plot_opts['symbolSize'] = symbolSize
        if lineStyle is not None:
            plot_opts['lineStyle'] = lineStyle
        if lineWidth is not None:
            plot_opts['lineWidth'] = lineWidth
        if redraw_mode is not None:
            plot_opts['redraw_mode'] = redraw_mode
        self._needs_redraw = False
        curve = WaveformCurveItem(y_addr=y_channel,
                                  x_addr=x_channel,
                                  name=name,
                                  color=color,
                                  **plot_opts)
        self.channel_pairs[(y_channel, x_channel)] = curve
        self.addCurve(curve, curve_color=color)
        curve.data_changed.connect(self.set_needs_redraw)

    def removeChannel(self, curve):
        """
        Remove a curve from the plot.

        Parameters
        ----------
        curve: WaveformCurveItem
            The curve to remove.
        """
        self.removeCurve(curve)

    def removeChannelAtIndex(self, index):
        """
        Remove a curve from the plot, given an index
        for a curve.

        Parameters
        ----------
        index: int
            Index for the curve to remove.
        """
        curve = self._curves[index]
        self.removeChannel(curve)

    @Slot()
    def set_needs_redraw(self):
        self._needs_redraw = True

    @Slot()
    def redrawPlot(self):
        """
        Request a redraw from each curve in the plot.
        Called by curves when they get new data.
        """
        if not self._needs_redraw:
            return
        for curve in self._curves:
            curve.redrawCurve()
        self._needs_redraw = False

    def clearCurves(self):
        """
        Remove all curves from the plot.
        """
        super(PyDMWaveformPlot, self).clear()

    def getCurves(self):
        """
        Get a list of json representations for each curve.
        """
        return [json.dumps(curve.to_dict()) for curve in self._curves]

    def setCurves(self, new_list):
        """
        Replace all existing curves with new ones.  This function
        is mostly used as a way to load curves from a .ui file, and
        almost all users will want to add curves through addChannel,
        not this method.

        Parameters
        ----------
        new_list: list
            A list of json strings representing each curve in the plot.
        """
        try:
            new_list = [json.loads(str(i)) for i in new_list]
        except ValueError as e:
            print("Error parsing curve json data: {}".format(e))
            return
        self.clearCurves()
        for d in new_list:
            color = d.get('color')
            if color:
                color = QColor(color)
            self.addChannel(d['y_channel'],
                            d['x_channel'],
                            name=d.get('name'),
                            color=color,
                            lineStyle=d.get('lineStyle'),
                            lineWidth=d.get('lineWidth'),
                            symbol=d.get('symbol'),
                            symbolSize=d.get('symbolSize'),
                            redraw_mode=d.get('redraw_mode'))

    curves = Property("QStringList", getCurves, setCurves, designable=False)

    def channels(self):
        """
        Returns the list of channels used by all curves in the plot.

        Returns
        -------
        list
        """
        chans = []
        chans.extend([curve.y_channel for curve in self._curves])
        chans.extend([
            curve.x_channel for curve in self._curves
            if curve.x_channel is not None
        ])
        return chans

    # The methods for autoRangeX, minXRange, maxXRange, autoRangeY, minYRange,
    # and maxYRange are all defined in BasePlot, but we don't expose them as
    # properties there, because not all plot subclasses necessarily want them
    # to be user-configurable in Designer.
    autoRangeX = Property(bool,
                          BasePlot.getAutoRangeX,
                          BasePlot.setAutoRangeX,
                          BasePlot.resetAutoRangeX,
                          doc="""
Whether or not the X-axis automatically rescales to fit the data.
If true, the values in minXRange and maxXRange are ignored.""")

    minXRange = Property(float,
                         BasePlot.getMinXRange,
                         BasePlot.setMinXRange,
                         doc="""
Minimum X-axis value visible on the plot.""")

    maxXRange = Property(float,
                         BasePlot.getMaxXRange,
                         BasePlot.setMaxXRange,
                         doc="""
Maximum X-axis value visible on the plot.""")

    autoRangeY = Property(bool,
                          BasePlot.getAutoRangeY,
                          BasePlot.setAutoRangeY,
                          BasePlot.resetAutoRangeY,
                          doc="""
Whether or not the Y-axis automatically rescales to fit the data.
If true, the values in minYRange and maxYRange are ignored.""")

    minYRange = Property(float,
                         BasePlot.getMinYRange,
                         BasePlot.setMinYRange,
                         doc="""
Minimum Y-axis value visible on the plot.""")

    maxYRange = Property(float,
                         BasePlot.getMaxYRange,
                         BasePlot.setMaxYRange,
                         doc="""
Maximum Y-axis value visible on the plot.""")
示例#22
0
class SpinBox(QtGui.QAbstractSpinBox):
    """
    **Bases:** QtGui.QAbstractSpinBox

    QSpinBox widget on steroids. Allows selection of numerical value, with extra features:

    - SI prefix notation (eg, automatically display "300 mV" instead of "0.003 V")
    - Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.)
    - Option for unbounded values
    - Delayed signals (allows multiple rapid changes with only one change signal)

    =============================  ==============================================
    **Signals:**
    valueChanged(value)            Same as QSpinBox; emitted every time the value
                                   has changed.
    sigValueChanged(self)          Emitted when value has changed, but also combines
                                   multiple rapid changes into one signal (eg,
                                   when rolling the mouse wheel).
    sigValueChanging(self, value)  Emitted immediately for all value changes.
    =============================  ==============================================
    """

    ## There's a PyQt bug that leaks a reference to the
    ## QLineEdit returned from QAbstractSpinBox.lineEdit()
    ## This makes it possible to crash the entire program
    ## by making accesses to the LineEdit after the spinBox has been deleted.
    ## I have no idea how to get around this..

    valueChanged = QtCore.Signal(
        object)  # (value)  for compatibility with QSpinBox
    sigValueChanged = QtCore.Signal(object)  # (self)
    sigValueChanging = QtCore.Signal(
        object, object)  # (self, value)  sent immediately; no delay.

    def __init__(self, parent=None, value=0.0, **kwargs):
        """
        ============== ========================================================================
        **Arguments:**
        parent         Sets the parent widget for this SpinBox (optional). Default is None.
        value          (float/int) initial value. Default is 0.0.
        bounds         (min,max) Minimum and maximum values allowed in the SpinBox.
                       Either may be None to leave the value unbounded. By default, values are unbounded.
        suffix         (str) suffix (units) to display after the numerical value. By default, suffix is an empty str.
        siPrefix       (bool) If True, then an SI prefix is automatically prepended
                       to the units and the value is scaled accordingly. For example,
                       if value=0.003 and suffix='V', then the SpinBox will display
                       "300 mV" (but a call to SpinBox.value will still return 0.003). Default is False.
        step           (float) The size of a single step. This is used when clicking the up/
                       down arrows, when rolling the mouse wheel, or when pressing
                       keyboard arrows while the widget has keyboard focus. Note that
                       the interpretation of this value is different when specifying
                       the 'dec' argument. Default is 0.01.
        dec            (bool) If True, then the step value will be adjusted to match
                       the current size of the variable (for example, a value of 15
                       might step in increments of 1 whereas a value of 1500 would
                       step in increments of 100). In this case, the 'step' argument
                       is interpreted *relative* to the current value. The most common
                       'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is False.
        minStep        (float) When dec=True, this specifies the minimum allowable step size.
        int            (bool) if True, the value is forced to integer type. Default is False
        decimals       (int) Number of decimal values to display. Default is 2.
        readonly       (bool) If True, then mouse and keyboard interactions are caught and
                       will not produce a valueChanged signal, but the value can be still
                       changed programmatic via the setValue method. Default is False.
        ============== ========================================================================
        """
        QtGui.QAbstractSpinBox.__init__(self, parent)
        self.lastValEmitted = None
        self.lastText = ''
        self.textValid = True  ## If false, we draw a red border
        self.setMinimumWidth(0)
        self.setMaximumHeight(20)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding,
                           QtGui.QSizePolicy.Preferred)
        self.opts = {
            'bounds': [None, None],

            ## Log scaling options   #### Log mode is no longer supported.
            #'step': 0.1,
            #'minStep': 0.001,
            #'log': True,
            #'dec': False,

            ## decimal scaling option - example
            #'step': 0.1,
            #'minStep': .001,
            #'log': False,
            #'dec': True,

            ## normal arithmetic step
            'step':
            D('0.01'
              ),  ## if 'dec' is false, the spinBox steps by 'step' every time
            ## if 'dec' is True, the step size is relative to the value
            ## 'step' needs to be an integral divisor of ten, ie 'step'*n=10 for some integer value of n (but only if dec is True)
            'log': False,
            'dec':
            False,  ## if true, does decimal stepping. ie from 1-10 it steps by 'step', from 10 to 100 it steps by 10*'step', etc.
            ## if true, minStep must be set in order to cross zero.
            'int': False,  ## Set True to force value to be integer
            'suffix': '',
            'siPrefix':
            False,  ## Set to True to display numbers with SI prefix (ie, 100pA instead of 1e-10A)
            'delay': 0.3,  ## delay sending wheel update signals for 300ms
            'delayUntilEditFinished':
            True,  ## do not send signals until text editing has finished

            ## for compatibility with QDoubleSpinBox and QSpinBox
            'decimals': 2,
            'readonly': False,
        }

        self.decOpts = ['step', 'minStep']
        self.val = D(asUnicode(
            value))  ## Value is precise decimal. Ordinary math not allowed.
        self.updateText()
        self.skipValidate = False
        self.setCorrectionMode(self.CorrectToPreviousValue)
        self.setKeyboardTracking(False)
        self.setOpts(**kwargs)

        self.editingFinished.connect(self.editingFinishedEvent)
        self.proxy = SignalProxy(self.sigValueChanging,
                                 slot=self.delayedChange,
                                 delay=self.opts['delay'])

    def event(self, ev):
        ret = QtGui.QAbstractSpinBox.event(self, ev)
        if ev.type() == QtCore.QEvent.KeyPress and ev.key(
        ) == QtCore.Qt.Key_Return:
            ret = True  ## For some reason, spinbox pretends to ignore return key press

        #Fix: introduce the Escape event, which is restoring the previous display.
        if ev.type() == QtCore.QEvent.KeyPress and ev.key(
        ) == QtCore.Qt.Key_Escape:
            self.updateText()
            ret = True
        return ret

    ##lots of config options, just gonna stuff 'em all in here rather than do the get/set crap.
    def setOpts(self, **opts):
        """
        Changes the behavior of the SpinBox. Accepts most of the arguments
        allowed in :func:`__init__ <pyqtgraph.SpinBox.__init__>`.

        """
        #print opts
        for k in opts:
            if k == 'bounds':
                #print opts[k]
                self.setMinimum(opts[k][0], update=False)
                self.setMaximum(opts[k][1], update=False)
                #for i in [0,1]:
                #if opts[k][i] is None:
                #self.opts[k][i] = None
                #else:
                #self.opts[k][i] = D(unicode(opts[k][i]))
            elif k in ['step', 'minStep']:
                self.opts[k] = D(asUnicode(opts[k]))
            elif k == 'value':
                pass  ## don't set value until bounds have been set
            else:
                self.opts[k] = opts[k]
        if 'value' in opts:
            self.setValue(opts['value'])

        ## If bounds have changed, update value to match
        if 'bounds' in opts and 'value' not in opts:
            self.setValue()

        ## sanity checks:
        if self.opts['int']:
            if 'step' in opts:
                step = opts['step']
                ## not necessary..
                #if int(step) != step:
                #raise Exception('Integer SpinBox must have integer step size.')
            else:
                self.opts['step'] = int(self.opts['step'])

            if 'minStep' in opts:
                step = opts['minStep']
                if int(step) != step:
                    raise Exception(
                        'Integer SpinBox must have integer minStep size.')
            else:
                ms = int(self.opts.get('minStep', 1))
                if ms < 1:
                    ms = 1
                self.opts['minStep'] = ms

        if 'delay' in opts:
            self.proxy.setDelay(opts['delay'])

        if 'readonly' in opts:
            self.opts['readonly'] = opts['readonly']

        self.updateText()

    #Fix: reimplement the methods for compatibility reasons to QSpinBox and QDoubleSpinBox methods
    def maximum(self):
        """ Reimplement the maximum functionality."""
        max_val = self.opts['bounds'][1]

        if self.opts['int'] and max_val is not None:
            return int(max_val)
        elif max_val is not None:
            return float(max_val)
        else:
            return max_val

    def setMaximum(self, m, update=True):
        """Set the maximum allowed value (or None for no limit)"""
        if m is not None:

            #FIX: insert the integer functionality:
            if self.opts['int']:
                m = int(m)

            m = D(asUnicode(m))
        self.opts['bounds'][1] = m
        if update:
            self.setValue()

    #Fix: reimplement the methods for compatibility reasons to QSpinBox and QDoubleSpinBox methods
    def minimum(self):
        """ Reimplement the minimum functionality."""

        min_val = self.opts['bounds'][0]

        if self.opts['int'] and min_val is not None:
            return int(min_val)
        elif min_val is not None:
            return float(min_val)
        else:
            return min_val

    def setMinimum(self, m, update=True):
        """Set the minimum allowed value (or None for no limit)"""
        if m is not None:

            #FIX: insert the integer functionality:
            if self.opts['int']:
                m = int(m)

            m = D(asUnicode(m))
        self.opts['bounds'][0] = m
        if update:
            self.setValue()

    def setPrefix(self, p):
        self.setOpts(prefix=p)

    def setRange(self, r0, r1):
        self.setOpts(bounds=[r0, r1])

    def setProperty(self, prop, val):
        ## for QSpinBox compatibility
        if prop == 'value':
            #if type(val) is QtCore.QVariant:
            #val = val.toDouble()[0]
            self.setValue(val)
        else:
            print("Warning: SpinBox.setProperty('{0!s}', ..) not supported.".
                  format(prop))

    def setSuffix(self, suf):
        self.setOpts(suffix=suf)

    def setSingleStep(self, step):
        self.setOpts(step=step)

    def setDecimals(self, decimals):
        self.setOpts(decimals=decimals)

    def selectNumber(self):
        """
        Select the numerical portion of the text to allow quick editing by the user.
        """
        le = self.lineEdit()
        text = asUnicode(le.text())
        if self.opts['suffix'] == '':
            le.setSelection(0, len(text))
        else:
            try:
                index = text.index(' ')
            except ValueError:
                return
            le.setSelection(0, index)

    #Fix: reimplement the methods for compatibility reasons to QSpinBox and QDoubleSpinBox methods
    def suffix(self):
        return self.opts['suffix']

    #Fix: reimplement the methods for compatibility reasons to QSpinBox and QDoubleSpinBox methods
    def prefix(self):
        return self.opts['prefix']

    def value(self):
        """
        Return the value of this SpinBox.

        """
        if self.opts['int']:
            return int(self.val)
        else:
            return float(self.val)

    def setValue(self, value=None, update=True, delaySignal=False):
        """
        Set the value of this spin.
        If the value is out of bounds, it will be clipped to the nearest boundary.
        If the spin is integer type, the value will be coerced to int.
        Returns the actual value set.

        If value is None, then the current value is used (this is for resetting
        the value after bounds, etc. have changed)
        """

        if value is None:
            value = self.value()

        bounds = self.opts['bounds']
        if bounds[0] is not None and value < bounds[0]:
            value = bounds[0]
        if bounds[1] is not None and value > bounds[1]:
            value = bounds[1]

        if self.opts['int']:
            value = int(value)

        value = D(asUnicode(value))
        if value == self.val:
            return
        prev = self.val

        self.val = value
        if update:
            self.updateText(prev=prev)

        self.sigValueChanging.emit(
            self, float(self.val)
        )  ## change will be emitted in 300ms if there are no subsequent changes.
        if not delaySignal:
            self.emitChanged()

        return value

    value_float = Property(float,
                           value,
                           setValue,
                           doc='Qt property value as type float')
    value_int = Property(int,
                         value,
                         setValue,
                         doc='Qt property value as type int')

    def emitChanged(self):
        self.lastValEmitted = self.val
        self.valueChanged.emit(float(self.val))
        self.sigValueChanged.emit(self)

    def delayedChange(self):
        try:
            if self.val != self.lastValEmitted:
                self.emitChanged()
        except RuntimeError:
            pass  ## This can happen if we try to handle a delayed signal after someone else has already deleted the underlying C++ object.

    def widgetGroupInterface(self):
        return (self.valueChanged, SpinBox.value, SpinBox.setValue)

    def sizeHint(self):
        return QtCore.QSize(120, 0)

    def stepEnabled(self):
        return self.StepUpEnabled | self.StepDownEnabled

    #def fixup(self, *args):
    #print "fixup:", args

    def stepBy(self, n):
        n = D(int(n))  ## n must be integral number of steps.
        s = [D(-1), D(1)][n >= 0]  ## determine sign of step
        val = self.val

        for i in range(int(abs(n))):

            if self.opts['log']:
                raise Exception("Log mode no longer supported.")
            #    step = abs(val) * self.opts['step']
            #    if 'minStep' in self.opts:
            #        step = max(step, self.opts['minStep'])
            #    val += step * s
            if self.opts['dec']:
                if val == 0:
                    step = self.opts['minStep']
                    exp = None
                else:
                    vs = [D(-1), D(1)][val >= 0]
                    #exp = D(int(abs(val*(D('1.01')**(s*vs))).log10()))
                    fudge = D('1.01')**(
                        s * vs
                    )  ## fudge factor. at some places, the step size depends on the step sign.
                    exp = abs(val * fudge).log10().quantize(
                        1, rounding=ROUND_FLOOR)
                    step = self.opts['step'] * D(10)**exp
                if 'minStep' in self.opts:
                    step = max(step, self.opts['minStep'])
                val += s * step
                #print "Exp:", exp, "step", step, "val", val
            else:
                val += s * self.opts['step']

            if 'minStep' in self.opts and abs(val) < self.opts['minStep']:
                val = D(0)
        self.setValue(
            val, delaySignal=True
        )  ## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only.

    def valueInRange(self, value):
        bounds = self.opts['bounds']
        if bounds[0] is not None and value < bounds[0]:
            return False
        if bounds[1] is not None and value > bounds[1]:
            return False
        if self.opts.get('int', False):
            if int(value) != value:
                return False
        return True

    def updateText(self, prev=None):
        # print("Update text.")

        self.skipValidate = True
        if self.opts['siPrefix']:
            if self.val == 0 and prev is not None:
                (s, p) = fn.siScale(prev)
                txt = "0.0 {0!s}{1!s}".format(p, self.opts['suffix'])
            else:
                txt = fn.siFormat(float(self.val),
                                  precision=self.opts['decimals'] + 1,
                                  suffix=self.opts['suffix'])
        else:
            txt = '{0:.14g}{1!s}'.format(self.val, self.opts['suffix'])
        self.lineEdit().setText(txt)
        self.lastText = txt
        self.skipValidate = False

    def validate(self, strn, pos):
        # print('validate', strn, pos)
        if self.skipValidate:
            # print("skip validate")
            #self.textValid = False
            ret = QtGui.QValidator.Acceptable
        else:
            try:
                ## first make sure we didn't mess with the suffix
                suff = self.opts.get('suffix', '')

                # fix: if the whole text is selected and one needs to typ in a
                #      new number, then a single integer character is ignored.
                if len(strn) == 1 and strn.isdigit():
                    scl_str = fn.siScale(self.val)[1]
                    strn = '{0} {1}{2}'.format(strn, scl_str, suff)

                if len(suff) > 0 and asUnicode(strn)[-len(suff):] != suff:
                    #print '"%s" != "%s"' % (unicode(strn)[-len(suff):], suff)
                    ret = QtGui.QValidator.Invalid
                    # print('invalid input', 'suff:', suff, '{0} != {1}'.format(asUnicode(strn)[-len(suff):], suff))

                ## next see if we actually have an interpretable value
                else:
                    val = self.interpret()
                    if val is False:
                        #print "can't interpret"
                        #self.setStyleSheet('SpinBox {border: 2px solid #C55;}')
                        #self.textValid = False
                        ret = QtGui.QValidator.Intermediate
                    else:
                        if self.valueInRange(val):
                            if not self.opts['delayUntilEditFinished']:
                                self.setValue(val, update=False)
                            #print "  OK:", self.val
                            #self.setStyleSheet('')
                            #self.textValid = True

                            ret = QtGui.QValidator.Acceptable
                        else:
                            ret = QtGui.QValidator.Intermediate

            except:
                #print "  BAD"
                #import sys
                #sys.excepthook(*sys.exc_info())
                #self.textValid = False
                #self.setStyleSheet('SpinBox {border: 2px solid #C55;}')
                ret = QtGui.QValidator.Intermediate

        ## draw / clear border
        if ret == QtGui.QValidator.Intermediate:
            self.textValid = False
        elif ret == QtGui.QValidator.Acceptable:
            self.textValid = True
        ## note: if text is invalid, we don't change the textValid flag
        ## since the text will be forced to its previous state anyway
        self.update()

        ## support 2 different pyqt APIs. Bleh.
        if hasattr(QtCore, 'QString'):
            return (ret, pos)
        else:
            return (ret, strn, pos)

    def paintEvent(self, ev):
        QtGui.QAbstractSpinBox.paintEvent(self, ev)

        ## draw red border if text is invalid
        if not self.textValid:
            p = QtGui.QPainter(self)
            p.setRenderHint(p.Antialiasing)
            p.setPen(fn.mkPen((200, 50, 50), width=2))
            p.drawRoundedRect(self.rect().adjusted(2, 2, -2, -2), 4, 4)
            p.end()

    def interpret(self):
        """Return value of text. Return False if text is invalid, raise exception if text is intermediate"""
        strn = self.lineEdit().text()

        #Fix: strip leading blank characters, which produce errors:
        strn = strn.lstrip()

        suf = self.opts['suffix']
        if len(suf) > 0:
            if strn[-len(suf):] != suf:
                return False
            #raise Exception("Units are invalid.")
            strn = strn[:-len(suf)]
        try:
            val = fn.siEval(strn)
        except:
            #sys.excepthook(*sys.exc_info())
            #print "invalid"
            return False
        #print val
        return val

    #def interpretText(self, strn=None):
    #print "Interpret:", strn
    #if strn is None:
    #strn = self.lineEdit().text()
    #self.setValue(siEval(strn), update=False)
    ##QtGui.QAbstractSpinBox.interpretText(self)

    def editingFinishedEvent(self):
        """Edit has finished; set value."""
        #print "Edit finished."
        if asUnicode(self.lineEdit().text()) == self.lastText:
            #print "no text change."
            return
        try:
            val = self.interpret()
        except:
            return

        if val is False:
            #print "value invalid:", str(self.lineEdit().text())
            return
        if val == self.val:
            #print "no value change:", val, self.val
            return
        self.setValue(
            val, delaySignal=False
        )  ## allow text update so that values are reformatted pretty-like

    #def textChanged(self):
    #print "Text changed."

### Drop-in replacement for SpinBox; just for crash-testing
#class SpinBox(QtGui.QDoubleSpinBox):
#valueChanged = QtCore.Signal(object)     # (value)  for compatibility with QSpinBox
#sigValueChanged = QtCore.Signal(object)  # (self)
#sigValueChanging = QtCore.Signal(object)  # (value)
#def __init__(self, parent=None, *args, **kargs):
#QtGui.QSpinBox.__init__(self, parent)

#def  __getattr__(self, attr):
#return lambda *args, **kargs: None

#def widgetGroupInterface(self):
#return (self.valueChanged, SpinBox.value, SpinBox.setValue)

    def isReadOnly(self):
        """ Overwrite the QAbstractSpinBox method to obtain the ReadOnly state.
        """
        return self.opts['readonly']

    def mousePressEvent(self, event):
        """ Handle what happens on press event of the mouse.

        @param event: QEvent of a Mouse Release action
        """

        if self.isReadOnly():
            event.accept()
        else:
            super(SpinBox, self).mousePressEvent(event)

    # Comment out this method, since it is called, if QToolTip is going to be
    # displayed. You would not see any QTooltip if you catch that signal.
    # def mouseMoveEvent(self, event):
    #     """ Handle what happens on move (over) event of the mouse.
    #
    #     @param event: QEvent of a Mouse Move action
    #     """
    #
    #     if ( self.isReadOnly() ):
    #         event.accept()
    #     else:
    #         super(SpinBox, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        """ Handle what happens on release of the mouse.

        @param event: QEvent of a Mouse Release action
        """

        if self.isReadOnly():
            event.accept()
        else:
            super(SpinBox, self).mouseReleaseEvent(event)

    # Handle event in which the widget has focus and the spacebar is pressed.
    def keyPressEvent(self, event):
        """ Handle what happens on keypress.

        @param event: QEvent of a key press Action
        """

        if self.isReadOnly():
            event.accept()
        else:
            super(SpinBox, self).keyPressEvent(event)

    def wheelEvent(self, event):
        """ Handle what happens on a wheel event of the mouse.

        @param event: QEvent of a Mouse Wheel event
        """

        if self.isReadOnly():
            event.accept()
        else:
            super(SpinBox, self).wheelEvent(event)

    @QtCore.pyqtSlot(bool)
    def setReadOnly(self, state):
        """ Overwrite the QAbstractSpinBox method to set the ReadOnly state.

        @param bool state: True or False, for having a readonly QRadioButton.

        Important, declare that slot as a qt slot to provide a C++ signature for
        that. The advantage is less memory consumption and slightly faster
        performance.
        Note: Not using the slot decorator causes that the signal connection
        mechanism is forced to work out manually the type conversion to map from
        the underlying C++ function signatures to the Python functions. When the
        slot decorators are used, the type mapping can be explicit!
        """

        self.setOpts(readonly=state)

    readOnly = QtCore.pyqtProperty(bool, isReadOnly, setReadOnly)
示例#23
0
class LabelBackend(QObject):
    @staticmethod
    def qml_point_to_np(qpoint: QPointF):
        return np.array(qpoint.toTuple())

    @staticmethod
    def qml_rect_to_np(qrect: QRectF):
        return np.array([
            qrect.topLeft().toTuple(),
            qrect.bottomRight().toTuple(),
        ])

    def __init__(self):
        super().__init__()

        self.instances = []
        self.instances_by_id = {}

        self.image_provider = LabelOverlayImageProvider()
        self.config = LabelConfig()

    # Semantic classes
    def load_config(self, cfg_path):
        if cfg_path.is_file():
            self.config.load_from_path(cfg_path)
        else:
            print(f'Config path {cfg_path} is not a file')

    def set_image_path(self, img_path):
        print('Loading image', img_path)

        # Load new image
        self.img_path = Path(img_path)
        self.photo = imageio.imread(self.img_path)
        self.resolution = np.array(self.photo.shape[:2][::-1])
        self.image_provider.init_image(self.resolution)
        self.overlay_data = self.image_provider.image_view

        # Clear instances
        for old_inst in self.instances:
            old_inst.deleted.emit()
        self.instances = []
        self.instances_by_id = {}

        # Load state
        data_dir = self.img_path.with_suffix('.labels')
        if data_dir.is_dir():
            print(f'Loading saved state from {data_dir}')
            self.load(data_dir)

        self.next_instance_id = int(
            np.max([0] + [inst.id for inst in self.instances]) + 1)
        self.instances_by_id = {inst.id: inst for inst in self.instances}
        self.instance_selected = None
        self.overlay_refresh_after_selection_change()

    @Slot(str)
    def set_image(self, path):
        path_prefix = "file://"
        if path.startswith(path_prefix):
            path = path[path_prefix.__len__():]

        self.set_image_path(path)

    @Slot(int, QPointF)
    def paint_circle(self, label_to_paint, center):
        try:  # this has to finish, we don't want to break UI interaction
            #print('paint_circle!', label_to_paint, center)

            if self.instance_selected:
                center_pt = np.rint(center.toTuple()).astype(dtype=np.int)

                self.instance_selected.paint_circle(label_to_paint, center_pt)
                self.instance_selected.grab_cut_update()
                self.overlay_refresh_after_edit()
            else:
                print('paint_circle: no instance is selected')

        except Exception as e:
            print('Error in paint_circle:', e)
            traceback.print_exc()

    @Slot(int, QJSValue)
    def paint_polygon(self, label_to_paint, points):
        try:  # this has to finish, we don't want to break UI interaction

            if self.instance_selected:
                points = np.array([p.toTuple() for p in points.toVariant()])
                #print('paint_polygon!', label_to_paint, points)

                self.instance_selected.paint_polygon(label_to_paint, points)
                self.instance_selected.grab_cut_update()
                self.overlay_refresh_after_edit()
            else:
                print('paint_polygon: no instance is selected')

        except Exception as e:
            print('Error in paint_polygon:', e)
            traceback.print_exc()

    def overlay_refresh_after_selection_change(self):
        if self.instance_selected:

            self.overlay_data[:] = (0, 0, 0, 128)
            self.instance_selected.draw_overlay_edit_interface(
                self.overlay_data)

        else:
            self.overlay_data[:] = 0

            for inst in self.instances:
                inst.draw_overlay_contour(self.overlay_data)

        self.overlayUpdated.emit()
        self.selectedUpdate.emit()

    def overlay_refresh_after_edit(self):
        if self.instance_selected:
            self.instance_selected.draw_overlay_edit_interface(
                self.overlay_data)
            self.overlayUpdated.emit()
        else:
            print('overlay_refresh_after_edit but instance_selected is null')

    @Slot(int)
    def select_instance(self, instance_id):
        if instance_id <= 0:
            instance_id = None

        if instance_id:
            self.instance_selected = self.instances_by_id[instance_id]
        else:
            self.instance_selected = None

        self.overlay_refresh_after_selection_change()

    @Slot(QRectF, int)
    def new_instance(self, roi_rect_qt, sem_class_id):
        try:  # this has to finish, we don't want to break UI interaction
            roi_rect = np.rint(self.qml_rect_to_np(roi_rect_qt)).astype(np.int)
            sem_class = self.config.classes_by_id.get(sem_class_id,
                                                      self.config.classes[0])

            margin = 32
            crop_rect = np.array([
                np.maximum(roi_rect[0] - margin, 0),
                np.minimum(roi_rect[1] + margin, self.resolution),
            ])

            # automatically mark existing instances as excluded from the new instance
            existing_instance_mask = np.zeros(tuple(self.resolution[::-1]),
                                              dtype=np.uint8)
            for inst in self.instances:
                inst.draw_mask(existing_instance_mask, 1)

            instance = GrabCutInstance(self.next_instance_id, sem_class,
                                       self.photo, crop_rect, roi_rect)
            self.next_instance_id += 1

            instance.grab_cut_init(existing_instance_mask)

            self.instances.append(instance)
            self.instances_by_id[instance.id] = instance
            self.select_instance(instance.id)

            self.instanceAdded.emit(instance)

        except Exception as e:
            print('Error in new_instance:', e)
            traceback.print_exc()

    @Slot(int, int)
    def set_instance_class(self, instance_id, class_id):
        try:  # this has to finish, we don't want to break UI interaction
            inst = self.instances_by_id[instance_id]
            cls = self.config.classes_by_id[class_id]

            inst.semantic_class = cls
            inst.update_qt_info()
            self.overlay_refresh_after_selection_change()

        except Exception as e:
            print('Error in set_instance_class:', e)
            traceback.print_exc()

    @Slot(int)
    def delete_instance(self, instance_id):
        try:  # this has to finish, we don't want to break UI interaction
            inst = self.instances_by_id[instance_id]

            if self.instance_selected == inst:
                self.select_instance(0)

            del self.instances_by_id[instance_id]
            self.instances.remove(inst)

            inst.deleted.emit()
            self.overlay_refresh_after_selection_change()

        except Exception as e:
            print('Error in delete_instance:', e)
            traceback.print_exc()

    @Slot()
    def save(self):

        # outputs
        sem_map = np.zeros(tuple(self.resolution[::-1]), dtype=np.uint8)
        sem_colorimg = np.zeros(tuple(self.resolution[::-1]) + (3, ),
                                dtype=np.uint8)
        inst_map = np.zeros(tuple(self.resolution[::-1]), dtype=np.uint8)

        for inst_id, inst in enumerate(self.instances):
            inst.draw_mask(sem_map)
            inst.draw_mask(sem_colorimg, inst.semantic_class.color)
            inst.draw_mask(inst_map, inst_id + 1)

        out_dir = self.img_path.with_suffix('.labels')
        out_dir.mkdir(exist_ok=True)

        imageio.imwrite(out_dir / 'labels_semantic.png', sem_map)
        imageio.imwrite(out_dir / 'labels_semantic_color.png', sem_colorimg)
        imageio.imwrite(out_dir / 'labels_instance.png', inst_map)

        # internal state

        json_data = dict(instances=[inst.to_dict() for inst in self.instances])

        with (out_dir / 'index.json').open('w') as f_out:
            json.dump(json_data, f_out, indent='	')

        for inst in self.instances:
            inst.save_to_dir(out_dir)

    def load(self, in_dir):
        with (in_dir / 'index.json').open('r') as f_in:
            json_data = json.load(f_in)

        self.instances = [
            GrabCutInstance.from_dict(inst_data, self.config, self.photo)
            for inst_data in json_data['instances']
        ]

        for inst in self.instances:
            inst.load_from_dir(in_dir)
            self.instanceAdded.emit(inst)

    # Expose to Qt
    overlayUpdated = Signal()
    instanceAdded = Signal(QObject)

    classesUpdated = Signal()
    classes = Property('QVariant', notify=classesUpdated)

    @classes.getter
    def get_classes(self):
        return self.config.to_simple_objects()

    @Slot(result='QVariant')
    def get_instances(self):
        return self.instances

    selectedUpdate = Signal()
    selected = Property(QObject,
                        attrgetter('instance_selected'),
                        notify=selectedUpdate)
示例#24
0
class QLed(QFrame, ShapeMap):
    """QLed class."""

    ShapeMap = ShapeMap
    Q_ENUMS(ShapeMap)

    abspath = _os.path.abspath(_os.path.dirname(__file__))
    shapesdict = dict()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/circle.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Circle] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/round.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Round] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/square.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Square] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/triangle.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Triangle] = str(f.readAll(), 'utf-8')
    f.close()

    Green = QColor(15, 105, 0)
    Red = QColor(207, 0, 0)
    Gray = QColor(90, 90, 90)
    SelColor = QColor(0, 0, 0)
    NotSelColor1 = QColor(251, 244, 252)
    NotSelColor2 = QColor(173, 173, 173)

    clicked = Signal()
    selected = Signal(bool)

    def __init__(self, parent=None, **kwargs):
        """Class constructor."""
        super().__init__(parent, **kwargs)
        self.m_state = 0
        self.m_stateColors = [self.Red, self.Green]

        self.m_dsblColor = self.Gray
        self.m_shape = self.ShapeMap.Circle

        self._pressed = False
        self._isselected = False
        self.renderer = QSvgRenderer()

    def getState(self):
        """Value property getter."""
        return self.m_state

    def setState(self, value):
        """Value property setter."""
        self.m_state = value
        self.update()

    state = Property(bool, getState, setState)

    def getOnColor(self):
        """On color property getter."""
        return self.m_stateColors[1]

    def setOnColor(self, newColor):
        """On color property setter."""
        self.m_stateColors[1] = newColor
        self.update()

    onColor = Property(QColor, getOnColor, setOnColor)

    def getOffColor(self):
        """Off color property getter."""
        return self.m_stateColors[0]

    def setOffColor(self, newColor):
        """Off color property setter."""
        self.m_stateColors[0] = newColor
        self.update()

    offColor = Property(QColor, getOffColor, setOffColor)

    @property
    def stateColors(self):
        """Color list property getter."""
        return list(self.m_stateColors)

    @stateColors.setter
    def stateColors(self, new_colors):
        """Color list property setter."""
        if not isinstance(new_colors, (list, tuple)) or\
                len(new_colors) < 2 or not isinstance(new_colors[0], QColor):
            return
        self.m_stateColors = list(new_colors)

    def getDsblColor(self):
        """Disabled color property getter."""
        return self.m_dsblColor

    def setDsblColor(self, newColor):
        """Disabled color property setter."""
        self.m_dsblColor = newColor
        self.update()

    dsblColor = Property(QColor, getDsblColor, setDsblColor)

    def getShape(self):
        """Shape property getter."""
        return self.m_shape

    def setShape(self, newShape):
        """Shape property setter."""
        self.m_shape = newShape
        self.update()

    shape = Property(ShapeMap, getShape, setShape)

    def sizeHint(self):
        """Return the base size of the widget according to shape."""
        if self.m_shape == self.ShapeMap.Triangle:
            return QSize(48, 36)
        elif self.m_shape == self.ShapeMap.Round:
            return QSize(72, 36)
        return QSize(36, 36)

    def adjust(self, r, g, b):
        """Adjust the color to set on svg code."""
        def normalise(x):
            return x / 255.0

        def denormalise(x):
            if x <= 1:
                return int(x * 255.0)
            else:
                return 255.0

        (h, l, s) = rgb_to_hls(normalise(r), normalise(g), normalise(b))
        (nr, ng, nb) = hls_to_rgb(h, l * 1.5, s)

        return (denormalise(nr), denormalise(ng), denormalise(nb))

    def getRGBfromQColor(self, qcolor):
        """Convert QColors to a tupple of rgb colors to set on svg code."""
        redhex = qcolor.red()
        greenhex = qcolor.green()
        bluehex = qcolor.blue()
        return (redhex, greenhex, bluehex)

    def paintEvent(self, event):
        """Handle appearence of the widget on state updates."""
        self.style().unpolish(self)
        self.style().polish(self)
        option = QStyleOption()
        option.initFrom(self)

        h = option.rect.height()
        w = option.rect.width()
        if self.m_shape in (self.ShapeMap.Triangle, self.ShapeMap.Round):
            aspect = (4 /
                      3.0) if self.m_shape == self.ShapeMap.Triangle else 2.0
            ah = w / aspect
            aw = w
            if ah > h:
                ah = h
                aw = h * aspect
            x = abs(aw - w) / 2.0
            y = abs(ah - h) / 2.0
            bounds = QRectF(x, y, aw, ah)
        else:
            size = min(w, h)
            x = abs(size - w) / 2.0
            y = abs(size - h) / 2.0
            bounds = QRectF(x, y, size, size)

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        ind = self.m_state % len(self.m_stateColors)
        dark_r, dark_g, dark_b = self.getRGBfromQColor(self.m_stateColors[ind])
        if not self.isEnabled():
            dark_r, dark_g, dark_b = self.getRGBfromQColor(self.m_dsblColor)

        sel1_r, sel1_g, sel1_b = self.getRGBfromQColor(self.SelColor)
        sel2_r, sel2_g, sel2_b = self.getRGBfromQColor(self.SelColor)
        opc = '1.000'
        if not self.isSelected():
            sel1_r, sel1_g, sel1_b = self.getRGBfromQColor(self.NotSelColor1)
            sel2_r, sel2_g, sel2_b = self.getRGBfromQColor(self.NotSelColor2)
            opc = '0.145'

        dark_str = "rgb(%d,%d,%d)" % (dark_r, dark_g, dark_b)
        light_str = "rgb(%d,%d,%d)" % self.adjust(dark_r, dark_g, dark_b)
        sel1_str = "rgb(%d,%d,%d)" % (sel1_r, sel1_g, sel1_b)
        sel2_str = "rgb(%d,%d,%d)" % (sel2_r, sel2_g, sel2_b)

        shape_bytes = bytes(
            self.shapesdict[self.m_shape] %
            (sel1_str, opc, sel2_str, dark_str, light_str), 'utf-8')

        self.renderer.load(QByteArray(shape_bytes))
        self.renderer.render(painter, bounds)

    def mousePressEvent(self, event):
        """Handle mouse press event."""
        self._pressed = True
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """Handle mouse release event."""
        if self._pressed:
            self._pressed = False
            self.clicked.emit()
        super().mouseReleaseEvent(event)

    def toggleState(self):
        """Toggle state property."""
        self.m_state = 0 if self.m_state else 1
        self.update()

    def isSelected(self):
        """Return selected state of object."""
        return self._isselected

    def setSelected(self, sel):
        """Configure selected state of object."""
        self._isselected = bool(sel)
        self.selected.emit(self._isselected)
        self.update()

    def toggleSelected(self):
        """Toggle isSelected property."""
        self.setSelected(not self.isSelected())
示例#25
0
文件: shell.py 项目: wpfeder/spyder
class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin, BrowseHistoryMixin):
    """
    Shell base widget
    """

    redirect_stdio = Signal(bool)
    sig_keyboard_interrupt = Signal()
    execute = Signal(str)
    append_to_history = Signal(str, str)

    def __init__(self,
                 parent,
                 history_filename,
                 profile=False,
                 initial_message=None,
                 default_foreground_color=None,
                 error_foreground_color=None,
                 traceback_foreground_color=None,
                 prompt_foreground_color=None,
                 background_color=None):
        """
        parent : specifies the parent widget
        """
        ConsoleBaseWidget.__init__(self, parent)
        SaveHistoryMixin.__init__(self, history_filename)
        BrowseHistoryMixin.__init__(self)

        # Prompt position: tuple (line, index)
        self.current_prompt_pos = None
        self.new_input_line = True

        # History
        assert is_text_string(history_filename)
        self.history = self.load_history()

        # Session
        self.historylog_filename = CONF.get('main', 'historylog_filename',
                                            get_conf_path('history.log'))

        # Context menu
        self.menu = None
        self.setup_context_menu()

        # Simple profiling test
        self.profile = profile

        # Buffer to increase performance of write/flush operations
        self.__buffer = []
        if initial_message:
            self.__buffer.append(initial_message)

        self.__timestamp = 0.0
        self.__flushtimer = QTimer(self)
        self.__flushtimer.setSingleShot(True)
        self.__flushtimer.timeout.connect(self.flush)

        # Give focus to widget
        self.setFocus()

        # Cursor width
        self.setCursorWidth(CONF.get('main', 'cursor/width'))

        # Adjustments to completion_widget to use it here
        self.completion_widget.currentRowChanged.disconnect()

    def toggle_wrap_mode(self, enable):
        """Enable/disable wrap mode"""
        self.set_wrap_mode('character' if enable else None)

    def set_font(self, font):
        """Set shell styles font"""
        self.setFont(font)
        self.set_pythonshell_font(font)
        cursor = self.textCursor()
        cursor.select(QTextCursor.Document)
        charformat = QTextCharFormat()
        charformat.setFontFamily(font.family())
        charformat.setFontPointSize(font.pointSize())
        cursor.mergeCharFormat(charformat)

    #------ Context menu
    def setup_context_menu(self):
        """Setup shell context menu"""
        self.menu = QMenu(self)
        self.cut_action = create_action(self,
                                        _("Cut"),
                                        shortcut=keybinding('Cut'),
                                        icon=ima.icon('editcut'),
                                        triggered=self.cut)
        self.copy_action = create_action(self,
                                         _("Copy"),
                                         shortcut=keybinding('Copy'),
                                         icon=ima.icon('editcopy'),
                                         triggered=self.copy)
        paste_action = create_action(self,
                                     _("Paste"),
                                     shortcut=keybinding('Paste'),
                                     icon=ima.icon('editpaste'),
                                     triggered=self.paste)
        save_action = create_action(self,
                                    _("Save history log..."),
                                    icon=ima.icon('filesave'),
                                    tip=_(
                                        "Save current history log (i.e. all "
                                        "inputs and outputs) in a text file"),
                                    triggered=self.save_historylog)
        self.delete_action = create_action(self,
                                           _("Delete"),
                                           shortcut=keybinding('Delete'),
                                           icon=ima.icon('editdelete'),
                                           triggered=self.delete)
        selectall_action = create_action(self,
                                         _("Select All"),
                                         shortcut=keybinding('SelectAll'),
                                         icon=ima.icon('selectall'),
                                         triggered=self.selectAll)
        add_actions(
            self.menu,
            (self.cut_action, self.copy_action, paste_action,
             self.delete_action, None, selectall_action, None, save_action))

    def contextMenuEvent(self, event):
        """Reimplement Qt method"""
        state = self.has_selected_text()
        self.copy_action.setEnabled(state)
        self.cut_action.setEnabled(state)
        self.delete_action.setEnabled(state)
        self.menu.popup(event.globalPos())
        event.accept()

    #------ Input buffer
    def get_current_line_from_cursor(self):
        return self.get_text('cursor', 'eof')

    def _select_input(self):
        """Select current line (without selecting console prompt)"""
        line, index = self.get_position('eof')
        if self.current_prompt_pos is None:
            pline, pindex = line, index
        else:
            pline, pindex = self.current_prompt_pos
        self.setSelection(pline, pindex, line, index)

    @Slot()
    def clear_terminal(self):
        """
        Clear terminal window
        Child classes reimplement this method to write prompt
        """
        self.clear()

    # The buffer being edited
    def _set_input_buffer(self, text):
        """Set input buffer"""
        if self.current_prompt_pos is not None:
            self.replace_text(self.current_prompt_pos, 'eol', text)
        else:
            self.insert(text)
        self.set_cursor_position('eof')

    def _get_input_buffer(self):
        """Return input buffer"""
        input_buffer = ''
        if self.current_prompt_pos is not None:
            input_buffer = self.get_text(self.current_prompt_pos, 'eol')
            input_buffer = input_buffer.replace(os.linesep, '\n')
        return input_buffer

    input_buffer = Property("QString", _get_input_buffer, _set_input_buffer)

    #------ Prompt
    def new_prompt(self, prompt):
        """
        Print a new prompt and save its (line, index) position
        """
        if self.get_cursor_line_column()[1] != 0:
            self.write('\n')
        self.write(prompt, prompt=True)
        # now we update our cursor giving end of prompt
        self.current_prompt_pos = self.get_position('cursor')
        self.ensureCursorVisible()
        self.new_input_line = False

    def check_selection(self):
        """
        Check if selected text is r/w,
        otherwise remove read-only parts of selection
        """
        if self.current_prompt_pos is None:
            self.set_cursor_position('eof')
        else:
            self.truncate_selection(self.current_prompt_pos)

    #------ Copy / Keyboard interrupt
    @Slot()
    def copy(self):
        """Copy text to clipboard... or keyboard interrupt"""
        if self.has_selected_text():
            ConsoleBaseWidget.copy(self)
        elif not sys.platform == 'darwin':
            self.interrupt()

    def interrupt(self):
        """Keyboard interrupt"""
        self.sig_keyboard_interrupt.emit()

    @Slot()
    def cut(self):
        """Cut text"""
        self.check_selection()
        if self.has_selected_text():
            ConsoleBaseWidget.cut(self)

    @Slot()
    def delete(self):
        """Remove selected text"""
        self.check_selection()
        if self.has_selected_text():
            ConsoleBaseWidget.remove_selected_text(self)

    @Slot()
    def save_historylog(self):
        """Save current history log (all text in console)"""
        title = _("Save history log")
        self.redirect_stdio.emit(False)
        filename, _selfilter = getsavefilename(
            self, title, self.historylog_filename,
            "%s (*.log)" % _("History logs"))
        self.redirect_stdio.emit(True)
        if filename:
            filename = osp.normpath(filename)
            try:
                encoding.write(to_text_string(self.get_text_with_eol()),
                               filename)
                self.historylog_filename = filename
                CONF.set('main', 'historylog_filename', filename)
            except EnvironmentError:
                pass

    #------ Basic keypress event handler
    def on_enter(self, command):
        """on_enter"""
        self.execute_command(command)

    def execute_command(self, command):
        self.execute.emit(command)
        self.add_to_history(command)
        self.new_input_line = True

    def on_new_line(self):
        """On new input line"""
        self.set_cursor_position('eof')
        self.current_prompt_pos = self.get_position('cursor')
        self.new_input_line = False

    @Slot()
    def paste(self):
        """Reimplemented slot to handle multiline paste action"""
        if self.new_input_line:
            self.on_new_line()
        ConsoleBaseWidget.paste(self)

    def keyPressEvent(self, event):
        """
        Reimplement Qt Method
        Basic keypress event handler
        (reimplemented in InternalShell to add more sophisticated features)
        """
        if self.preprocess_keyevent(event):
            # Event was accepted in self.preprocess_keyevent
            return
        self.postprocess_keyevent(event)

    def preprocess_keyevent(self, event):
        """Pre-process keypress event:
        return True if event is accepted, false otherwise"""
        # Copy must be done first to be able to copy read-only text parts
        # (otherwise, right below, we would remove selection
        #  if not on current line)
        ctrl = event.modifiers() & Qt.ControlModifier
        meta = event.modifiers() & Qt.MetaModifier  # meta=ctrl in OSX
        if event.key() == Qt.Key_C and \
          ((Qt.MetaModifier | Qt.ControlModifier) & event.modifiers()):
            if meta and sys.platform == 'darwin':
                self.interrupt()
            elif ctrl:
                self.copy()
            event.accept()
            return True

        if self.new_input_line and ( len(event.text()) or event.key() in \
           (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right) ):
            self.on_new_line()

        return False

    def postprocess_keyevent(self, event):
        """Post-process keypress event:
        in InternalShell, this is method is called when shell is ready"""
        event, text, key, ctrl, shift = restore_keyevent(event)

        # Is cursor on the last line? and after prompt?
        if len(text):
            #XXX: Shouldn't it be: `if len(unicode(text).strip(os.linesep))` ?
            if self.has_selected_text():
                self.check_selection()
            self.restrict_cursor_position(self.current_prompt_pos, 'eof')

        cursor_position = self.get_position('cursor')

        if key in (Qt.Key_Return, Qt.Key_Enter):
            if self.is_cursor_on_last_line():
                self._key_enter()
            # add and run selection
            else:
                self.insert_text(self.get_selected_text(), at_end=True)

        elif key == Qt.Key_Insert and not shift and not ctrl:
            self.setOverwriteMode(not self.overwriteMode())

        elif key == Qt.Key_Delete:
            if self.has_selected_text():
                self.check_selection()
                self.remove_selected_text()
            elif self.is_cursor_on_last_line():
                self.stdkey_clear()

        elif key == Qt.Key_Backspace:
            self._key_backspace(cursor_position)

        elif key == Qt.Key_Tab:
            self._key_tab()

        elif key == Qt.Key_Space and ctrl:
            self._key_ctrl_space()

        elif key == Qt.Key_Left:
            if self.current_prompt_pos == cursor_position:
                # Avoid moving cursor on prompt
                return
            method = self.extend_selection_to_next if shift \
                     else self.move_cursor_to_next
            method('word' if ctrl else 'character', direction='left')

        elif key == Qt.Key_Right:
            if self.is_cursor_at_end():
                return
            method = self.extend_selection_to_next if shift \
                     else self.move_cursor_to_next
            method('word' if ctrl else 'character', direction='right')

        elif (key == Qt.Key_Home) or ((key == Qt.Key_Up) and ctrl):
            self._key_home(shift, ctrl)

        elif (key == Qt.Key_End) or ((key == Qt.Key_Down) and ctrl):
            self._key_end(shift, ctrl)

        elif key == Qt.Key_Up:
            if not self.is_cursor_on_last_line():
                self.set_cursor_position('eof')
            y_cursor = self.get_coordinates(cursor_position)[1]
            y_prompt = self.get_coordinates(self.current_prompt_pos)[1]
            if y_cursor > y_prompt:
                self.stdkey_up(shift)
            else:
                self.browse_history(backward=True)

        elif key == Qt.Key_Down:
            if not self.is_cursor_on_last_line():
                self.set_cursor_position('eof')
            y_cursor = self.get_coordinates(cursor_position)[1]
            y_end = self.get_coordinates('eol')[1]
            if y_cursor < y_end:
                self.stdkey_down(shift)
            else:
                self.browse_history(backward=False)

        elif key in (Qt.Key_PageUp, Qt.Key_PageDown):
            #XXX: Find a way to do this programmatically instead of calling
            # widget keyhandler (this won't work if the *event* is coming from
            # the event queue - i.e. if the busy buffer is ever implemented)
            ConsoleBaseWidget.keyPressEvent(self, event)

        elif key == Qt.Key_Escape and shift:
            self.clear_line()

        elif key == Qt.Key_Escape:
            self._key_escape()

        elif key == Qt.Key_L and ctrl:
            self.clear_terminal()

        elif key == Qt.Key_V and ctrl:
            self.paste()

        elif key == Qt.Key_X and ctrl:
            self.cut()

        elif key == Qt.Key_Z and ctrl:
            self.undo()

        elif key == Qt.Key_Y and ctrl:
            self.redo()

        elif key == Qt.Key_A and ctrl:
            self.selectAll()

        elif key == Qt.Key_Question and not self.has_selected_text():
            self._key_question(text)

        elif key == Qt.Key_ParenLeft and not self.has_selected_text():
            self._key_parenleft(text)

        elif key == Qt.Key_Period and not self.has_selected_text():
            self._key_period(text)

        elif len(text) and not self.isReadOnly():
            self.hist_wholeline = False
            self.insert_text(text)
            self._key_other(text)

        else:
            # Let the parent widget handle the key press event
            ConsoleBaseWidget.keyPressEvent(self, event)

    #------ Key handlers
    def _key_enter(self):
        command = self.input_buffer
        self.insert_text('\n', at_end=True)
        self.on_enter(command)
        self.flush()

    def _key_other(self, text):
        raise NotImplementedError

    def _key_backspace(self, cursor_position):
        raise NotImplementedError

    def _key_tab(self):
        raise NotImplementedError

    def _key_ctrl_space(self):
        raise NotImplementedError

    def _key_home(self, shift, ctrl):
        if self.is_cursor_on_last_line():
            self.stdkey_home(shift, ctrl, self.current_prompt_pos)

    def _key_end(self, shift, ctrl):
        if self.is_cursor_on_last_line():
            self.stdkey_end(shift, ctrl)

    def _key_pageup(self):
        raise NotImplementedError

    def _key_pagedown(self):
        raise NotImplementedError

    def _key_escape(self):
        raise NotImplementedError

    def _key_question(self, text):
        raise NotImplementedError

    def _key_parenleft(self, text):
        raise NotImplementedError

    def _key_period(self, text):
        raise NotImplementedError

    #------ History Management
    def load_history(self):
        """Load history from a .py file in user home directory"""
        if osp.isfile(self.history_filename):
            rawhistory, _ = encoding.readlines(self.history_filename)
            rawhistory = [line.replace('\n', '') for line in rawhistory]
            if rawhistory[1] != self.INITHISTORY[1]:
                rawhistory[1] = self.INITHISTORY[1]
        else:
            rawhistory = self.INITHISTORY
        history = [line for line in rawhistory \
                   if line and not line.startswith('#')]

        # Truncating history to X entries:
        while len(history) >= CONF.get('historylog', 'max_entries'):
            del history[0]
            while rawhistory[0].startswith('#'):
                del rawhistory[0]
            del rawhistory[0]

        # Saving truncated history:
        try:
            encoding.writelines(rawhistory, self.history_filename)
        except EnvironmentError:
            pass

        return history

    #------ Simulation standards input/output
    def write_error(self, text):
        """Simulate stderr"""
        self.flush()
        self.write(text, flush=True, error=True)
        if get_debug_level():
            STDERR.write(text)

    def write(self, text, flush=False, error=False, prompt=False):
        """Simulate stdout and stderr"""
        if prompt:
            self.flush()
        if not is_string(text):
            # This test is useful to discriminate QStrings from decoded str
            text = to_text_string(text)
        self.__buffer.append(text)
        ts = time.time()
        if flush or prompt:
            self.flush(error=error, prompt=prompt)
        elif ts - self.__timestamp > 0.05:
            self.flush(error=error)
            self.__timestamp = ts
            # Timer to flush strings cached by last write() operation in series
            self.__flushtimer.start(50)

    def flush(self, error=False, prompt=False):
        """Flush buffer, write text to console"""
        # Fix for spyder-ide/spyder#2452
        if PY3:
            try:
                text = "".join(self.__buffer)
            except TypeError:
                text = b"".join(self.__buffer)
                try:
                    text = text.decode(locale.getdefaultlocale()[1])
                except:
                    pass
        else:
            text = "".join(self.__buffer)

        self.__buffer = []
        self.insert_text(text, at_end=True, error=error, prompt=prompt)

        # The lines below are causing a hard crash when Qt generates
        # internal warnings. We replaced them instead for self.update(),
        # which prevents the crash.
        # See spyder-ide/spyder#10893
        # QCoreApplication.processEvents()
        # self.repaint()
        self.update()

        # Clear input buffer:
        self.new_input_line = True

    #------ Text Insertion
    def insert_text(self, text, at_end=False, error=False, prompt=False):
        """
        Insert text at the current cursor position
        or at the end of the command line
        """
        if at_end:
            # Insert text at the end of the command line
            self.append_text_to_shell(text, error, prompt)
        else:
            # Insert text at current cursor position
            ConsoleBaseWidget.insert_text(self, text)

    #------ Re-implemented Qt Methods
    def focusNextPrevChild(self, next):
        """
        Reimplemented to stop Tab moving to the next window
        """
        if next:
            return False
        return ConsoleBaseWidget.focusNextPrevChild(self, next)

    #------ Drag and drop
    def dragEnterEvent(self, event):
        """Drag and Drop - Enter event"""
        event.setAccepted(event.mimeData().hasFormat("text/plain"))

    def dragMoveEvent(self, event):
        """Drag and Drop - Move event"""
        if (event.mimeData().hasFormat("text/plain")):
            event.setDropAction(Qt.MoveAction)
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Drag and Drop - Drop event"""
        if (event.mimeData().hasFormat("text/plain")):
            text = to_text_string(event.mimeData().text())
            if self.new_input_line:
                self.on_new_line()
            self.insert_text(text, at_end=True)
            self.setFocus()
            event.setDropAction(Qt.MoveAction)
            event.accept()
        else:
            event.ignore()

    def drop_pathlist(self, pathlist):
        """Drop path list"""
        raise NotImplementedError
示例#26
0
class MTag(QLabel):
    """
    Tag for categorizing or markup.
    """

    sig_closed = Signal()
    sig_clicked = Signal()

    def __init__(self, text="", parent=None):
        super(MTag, self).__init__(text=text, parent=parent)
        self._is_pressed = False
        self._close_button = MToolButton().tiny().svg(
            "close_line.svg").icon_only()
        self._close_button.clicked.connect(self.sig_closed)
        self._close_button.clicked.connect(self.close)
        self._close_button.setVisible(False)

        self._main_lay = QHBoxLayout()
        self._main_lay.setContentsMargins(0, 0, 0, 0)
        self._main_lay.addStretch()
        self._main_lay.addWidget(self._close_button)

        self.setLayout(self._main_lay)

        self._clickable = False
        self._border = True
        self._border_style = QssTemplate("""
            MTag{
                font-size: @font_size_base@font_unit;
                padding: @padding_small@unit;
                color: @text_color;
                border-radius: @border_radius;
                border: 1px solid @border_color;
                background-color: @background_color;
            }
            MTag:hover{
                color: @hover_color;
            }
            """)
        self._no_border_style = QssTemplate("""
            MTag{
                font-size: @font_size_base@font_unit;
                padding: @padding@unit;
                border-radius: @border_radius;
                color: @text_color;
                border: 0 solid @border_color;
                background-color: @background_color;
            }
            MTag:hover{
                background-color:@hover_color;
            }
        """)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

        self._color = None
        self.set_dayu_color(dayu_theme.secondary_text_color)

    def minimumSizeHint(self, *args, **kwargs):
        """Override minimumSizeHint for expand width when the close button is visible."""
        orig = super(MTag, self).minimumSizeHint(*args, **kwargs)
        orig.setWidth(orig.width() + (
            dayu_theme.tiny if self._close_button.isVisible() else 0))
        return orig

    def get_dayu_color(self):
        """Get tag's color"""
        return self._color

    def set_dayu_color(self, value):
        """Set Tag primary color."""
        self._color = value
        self._update_style()

    def _update_style(self):
        scale_x, _ = get_scale_factor()
        if self._border:
            self.setStyleSheet(
                self._border_style.substitute(
                    padding_small=3 * scale_x,
                    font_size_base=dayu_theme.font_size_base,
                    font_unit=dayu_theme.font_unit,
                    unit=dayu_theme.unit,
                    background_color=utils.fade_color(self._color, "15%"),
                    border_radius=dayu_theme.border_radius_base,
                    border_color=utils.fade_color(self._color, "35%"),
                    hover_color=utils.generate_color(self._color, 5),
                    text_color=self._color,
                ))
        else:
            self.setStyleSheet(
                self._no_border_style.substitute(
                    padding=4 * scale_x,
                    font_size_base=dayu_theme.font_size_base,
                    font_unit=dayu_theme.font_unit,
                    unit=dayu_theme.unit,
                    background_color=utils.generate_color(self._color, 6),
                    border_radius=dayu_theme.border_radius_base,
                    border_color=utils.generate_color(self._color, 6),
                    hover_color=utils.generate_color(self._color, 5),
                    text_color=dayu_theme.text_color_inverse,
                ))

    dayu_color = Property(str, get_dayu_color, set_dayu_color)

    def mousePressEvent(self, event):
        """Override mousePressEvent to flag _is_pressed."""
        if event.button() == Qt.LeftButton:
            self._is_pressed = True
        return super(MTag, self).mousePressEvent(event)

    def leaveEvent(self, event):
        """Override leaveEvent to reset _is_pressed flag."""
        self._is_pressed = False
        return super(MTag, self).leaveEvent(event)

    def mouseReleaseEvent(self, event):
        """Override mouseReleaseEvent to emit sig_clicked signal."""
        if event.button() == Qt.LeftButton and self._is_pressed:
            if self._clickable:
                self.sig_clicked.emit()
        self._is_pressed = False
        return super(MTag, self).mouseReleaseEvent(event)

    def closeable(self):
        """Set Tag can be closed and show the close icon button."""
        self._close_button.setVisible(True)
        return self

    def clickable(self):
        """Set Tag can be clicked and change the cursor to pointing-hand shape when enter."""
        self.setCursor(Qt.PointingHandCursor)
        self._clickable = True
        return self

    def no_border(self):
        """Set Tag style is border or fill."""
        self._border = False
        self._update_style()
        return self

    def coloring(self, color):
        """Same as set_dayu_color. Support chain."""
        self.set_dayu_color(color)
        return self
示例#27
0
class TyphosSignalPanel(TyphosBase, TyphosDesignerMixin, SignalOrder):
    """
    Panel of Signals for a given device, using :class:`SignalPanel`.

    Parameters
    ----------
    parent : QtWidgets.QWidget, optional
        The parent widget.

    init_channel : str, optional
        The PyDM channel with which to initialize the widget.
    """

    Q_ENUMS(SignalOrder)  # Necessary for display in Designer
    SignalOrder = SignalOrder  # For convenience
    # From top of page to bottom
    kind_order = (Kind.hinted, Kind.normal, Kind.config, Kind.omitted)
    _panel_class = SignalPanel
    updated = QtCore.Signal()

    _kind_to_property = {
        'hinted': 'showHints',
        'normal': 'showNormal',
        'config': 'showConfig',
        'omitted': 'showOmitted',
    }

    def __init__(self, parent=None, init_channel=None):
        super().__init__(parent=parent)
        # Create a SignalPanel layout to be modified later
        self._panel_layout = self._panel_class()
        self.setLayout(self._panel_layout)
        self._name_filter = ''
        # Add default Kind values
        self._kinds = dict.fromkeys([kind.name for kind in Kind], True)
        self._signal_order = SignalOrder.byKind

        self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
        self.contextMenuEvent = self.open_context_menu

    def _get_kind(self, kind):
        """Property getter for show[kind]."""
        return self._kinds[kind]

    def _set_kind(self, value, kind):
        """Property setter for show[kind] = value."""
        # If we have a new value store it
        if value != self._kinds[kind]:
            # Store it internally
            self._kinds[kind] = value
            # Remodify the layout for the new Kind
            self._update_panel()

    @property
    def filter_settings(self):
        """Get the filter settings dictionary."""
        return dict(
            name_filter=self.nameFilter,
            kinds=self.show_kinds,
        )

    def _update_panel(self):
        """Apply filters and emit the update signal."""
        self._panel_layout.filter_signals(**self.filter_settings)
        self.updated.emit()

    @property
    def show_kinds(self):
        """Get a list of the :class:`ophyd.Kind` that should be shown."""
        return [kind for kind in Kind if self._kinds[kind.name]]

    # Kind Configuration pyqtProperty
    showHints = Property(bool,
                         partial(_get_kind, kind='hinted'),
                         partial(_set_kind, kind='hinted'),
                         doc='Show ophyd.Kind.hinted signals')
    showNormal = Property(bool,
                          partial(_get_kind, kind='normal'),
                          partial(_set_kind, kind='normal'),
                          doc='Show ophyd.Kind.normal signals')
    showConfig = Property(bool,
                          partial(_get_kind, kind='config'),
                          partial(_set_kind, kind='config'),
                          doc='Show ophyd.Kind.config signals')
    showOmitted = Property(bool,
                           partial(_get_kind, kind='omitted'),
                           partial(_set_kind, kind='omitted'),
                           doc='Show ophyd.Kind.omitted signals')

    @Property(str)
    def nameFilter(self):
        """Get or set the current name filter."""
        return self._name_filter

    @nameFilter.setter
    def nameFilter(self, name_filter):
        if name_filter != self._name_filter:
            self._name_filter = name_filter.strip()
            self._update_panel()

    @Property(SignalOrder)
    def sortBy(self):
        """Get or set the order that the signals will be placed in layout."""
        return self._signal_order

    @sortBy.setter
    def sortBy(self, value):
        if value != self._signal_order:
            self._signal_order = value
            self._update_panel()

    def add_device(self, device):
        """Typhos hook for adding a new device."""
        self.devices.clear()
        super().add_device(device)
        # Configure the layout for the new device
        self._panel_layout.add_device(device)
        self._update_panel()

    def set_device_display(self, display):
        """Typhos hook for when the TyphosDeviceDisplay is associated."""
        self.display = display

    def generate_context_menu(self):
        """Generate a context menu for this TyphosSignalPanel."""
        menu = QtWidgets.QMenu(parent=self)
        menu.addSection('Kinds')
        for kind, property_name in self._kind_to_property.items():

            def selected(new_value, *, name=property_name):
                setattr(self, name, new_value)

            action = menu.addAction('Show &' + kind)
            action.setCheckable(True)
            action.setChecked(getattr(self, property_name))
            action.triggered.connect(selected)
        return menu

    def open_context_menu(self, ev):
        """
        Open a context menu when the Default Context Menu is requested.

        Parameters
        ----------
        ev : QEvent
        """
        menu = self.generate_context_menu()
        menu.exec_(self.mapToGlobal(ev.pos()))
示例#28
0
文件: editor.py 项目: slaclab/nalms
class PyDMAlarmTree(QTreeView, PyDMWritableWidget):
    def __init__(self,
                 parent,
                 init_channel=None,
                 config_name=None,
                 edit_mode=False):
        super(PyDMAlarmTree, self).__init__()

        QTreeView.__init__(self, parent)
        PyDMWritableWidget.__init__(self)

        self.setup_ui()

        self._nodes = []

        self.config_name = config_name

        self.tree_model = AlarmTreeModel(self)
        self.setModel(self.tree_model)
        self.edit_mode = edit_mode

        self.setContextMenuPolicy(Qt.CustomContextMenu)

        if not edit_mode:
            self.customContextMenuRequested.connect(self._open_menu)

        self.expandAll()

    def setup_ui(self):
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)

        self.setDragDropOverwriteMode(False)
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        #  self.setHeaderHidden(True)
        self.setColumnWidth(0, 160)
        self.setColumnWidth(1, 160)
        self.setColumnWidth(2, 160)

    def get_configuration_name(self):
        return self.config_name

    def set_configuration_name(self, config_name):
        self.config_name = config_name

    configuration_name = Property(str,
                                  get_configuration_name,
                                  set_configuration_name,
                                  designable=False)

    def _open_menu(self, point):
        menu = QMenu()
        index = self.indexAt(point)
        item = self.model().getItem(index)
        self.value_action = QAction(item.status, self)
        self.value_action.setEnabled(False)

        menu.addAction(self.value_action)

        self.acknowledge_action = QAction("Acknowledge", self)
        self.acknowledge_action.triggered.connect(
            partial(self._acknowledge_at_index, index))
        menu.addAction(self.acknowledge_action)

        self.remove_acknowledge_action = QAction("Remove Acknowledge", self)
        self.remove_acknowledge_action.triggered.connect(
            partial(self._remove_acknowledge_at_index, index))
        self.remove_acknowledge_action.setEnabled(False)

        menu.addAction(self.remove_acknowledge_action)

        menu.exec_(self.viewport().mapToGlobal(point))

    def _acknowledge_at_index(self, index):
        item = self.tree_model.getItem(index)
        item.acknowledge()

    def _remove_acknowledge_at_index(self, index):
        item = self.tree_model.getItem(index)
        item.unacknowledge()

    def mousePressEvent(self, event):
        self.clearSelection()
        self.selectionModel().reset()
        QTreeView.mousePressEvent(self, event)
示例#29
0
文件: tab_bar.py 项目: tynanford/pydm
class PyDMTabWidget(QTabWidget):
    """PyDMTabWidget provides a tabbed container widget.  Each tab has an
    alarm channel property which can be used to show an alarm indicator on
    the tab.  The indicator is driven by the alarm severity of the specified
    channel, not the value.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Tab Widget
    """
    def __init__(self, parent=None):
        super(PyDMTabWidget, self).__init__(parent=parent)
        self.tb = PyDMTabBar(parent=self)
        self.setTabBar(self.tb)

    @Property(str)
    def currentTabAlarmChannel(self):
        """
        A channel to use for the current tab's alarm indicator.

        Returns
        -------
        str
        """
        return self.tabBar().currentTabAlarmChannel

    @currentTabAlarmChannel.setter
    def currentTabAlarmChannel(self, new_alarm_channel):
        self.tabBar().currentTabAlarmChannel = new_alarm_channel

    def channels(self):
        """
        A list of the channels used by the tab widget.

        Returns
        -------
        list
        """
        return self.tabBar().channels()

    def getAlarmChannels(self):
        """alarmChannels is a property used to store the configuration of this tab bar
        when it has been created in Qt Designer.  This property isn't directly editable
        by users, they will go through the currentTabAlarmChannel property to edit this
        information."""
        return self.tabBar().getAlarmChannels()

    def setAlarmChannels(self, new_alarm_channels):
        """
        Sets the list of alarm channels for each tab.  This is needed for instantiating
        a tab widget from a .ui file, and is probably not very useful for users.
        """
        self.tabBar().setAlarmChannels(new_alarm_channels)

    @Property(QColor)
    def noAlarmIconColor(self):
        """
        A color to use for alarm-sensitive tabs that have PyDMWidget.ALARM_NONE severity level.
        This property can be defined in a stylesheet by using 'qproperty-noAlarmIconColor'.

        Returns
        -------
        QColor
        """
        return self.tabBar().noAlarmIconColor

    @noAlarmIconColor.setter
    def noAlarmIconColor(self, new_color):
        if self.tabBar().noAlarmIconColor != new_color:
            self.tabBar().noAlarmIconColor = new_color
            self.tabBar().generate_alarm_icons()

    @Property(QColor)
    def minorAlarmIconColor(self):
        """
        A color to use for alarm-sensitive tabs that have PyDMWidget.ALARM_MINOR severity level.
        This property can be defined in a stylesheet by using 'qproperty-minorAlarmIconColor'.

        Returns
        -------
        QColor
        """
        return self.tabBar().minorAlarmIconColor

    @minorAlarmIconColor.setter
    def minorAlarmIconColor(self, new_color):
        self.tabBar().minorAlarmIconColor = new_color

    @Property(QColor)
    def majorAlarmIconColor(self):
        """
        A color to use for alarm-sensitive tabs that have PyDMWidget.ALARM_MAJOR severity level.
        This property can be defined in a stylesheet by using 'qproperty-majorAlarmIconColor'.

        Returns
        -------
        QColor
        """
        return self.tabBar().majorAlarmIconColor

    @majorAlarmIconColor.setter
    def majorAlarmIconColor(self, new_color):
        self.tabBar().majorAlarmIconColor = new_color

    @Property(QColor)
    def invalidAlarmIconColor(self):
        """
        A color to use for alarm-sensitive tabs that have PyDMWidget.ALARM_INVALID severity level.
        This property can be defined in a stylesheet by using 'qproperty-majorAlarmIconColor'.

        Returns
        -------
        QColor
        """
        return self.tabBar().invalidAlarmIconColor

    @invalidAlarmIconColor.setter
    def invalidAlarmIconColor(self, new_color):
        self.tabBar().invalidAlarmIconColor = new_color

    @Property(QColor)
    def disconnectedAlarmIconColor(self):
        """
        A color to use for alarm-sensitive tabs that have PyDMWidget.ALARM_DISCONNECTED severity level.
        This property can be defined in a stylesheet by using 'qproperty-disconnectedAlarmIconColor'.

        Returns
        -------
        QColor
        """
        return self.tabBar().disconnectedAlarmIconColor

    @disconnectedAlarmIconColor.setter
    def disconnectedAlarmIconColor(self, new_color):
        self.tabBar().disconnectedAlarmIconColor = new_color

    alarmChannels = Property("QStringList",
                             getAlarmChannels,
                             setAlarmChannels,
                             designable=False)

    # We make a bunch of dummy properties to block out properties available on QTabWidget,
    # but that we don't want to support on PyDMTabWidget.
    currentTabIcon = Property("QIcon", None, None, designable=False)
    documentMode = Property(bool, None, None, designable=False)
    tabsClosable = Property(bool, None, None, designable=False)
    movable = Property(bool, None, None, designable=False)
示例#30
0
class TyphonSignalPanel(TyphonBase, TyphonDesignerMixin, SignalOrder):
    """
    Panel of Signals for Device
    """
    Q_ENUMS(SignalOrder)  # Necessary for display in Designer
    SignalOrder = SignalOrder  # For convenience
    # From top of page to bottom
    kind_order = (Kind.hinted, Kind.normal, Kind.config, Kind.omitted)

    def __init__(self, parent=None, init_channel=None):
        super().__init__(parent=parent)
        # Create a SignalPanel layout to be modified later
        self.setLayout(SignalPanel())
        # Add default Kind values
        self._kinds = dict.fromkeys([kind.name for kind in Kind], True)
        self._signal_order = SignalOrder.byKind

    def _get_kind(self, kind):
        return self._kinds[kind]

    def _set_kind(self, value, kind):
        # If we have a new value store it
        if value != self._kinds[kind]:
            # Store it internally
            self._kinds[kind] = value
            # Remodify the layout for the new Kind
            self._set_layout()

    # Kind Configuration pyqtProperty
    showHints = Property(bool, partial(_get_kind, kind='hinted'),
                         partial(_set_kind, kind='hinted'))
    showNormal = Property(bool, partial(_get_kind, kind='normal'),
                          partial(_set_kind, kind='normal'))
    showConfig = Property(bool, partial(_get_kind, kind='config'),
                          partial(_set_kind, kind='config'))
    showOmitted = Property(bool, partial(_get_kind, kind='omitted'),
                           partial(_set_kind, kind='omitted'))

    @Property(SignalOrder)
    def sortBy(self):
        """Order signals will be placed in layout"""
        return self._signal_order

    @sortBy.setter
    def sortBy(self, value):
        if value != self._signal_order:
            self._signal_order = value
            self._set_layout()

    def add_device(self, device):
        """Add a device to the widget"""
        # Only allow a single device
        self.devices.clear()
        # Add the new device
        super().add_device(device)
        # Configure the layout for the new device
        self._set_layout()

    def _set_layout(self):
        """Set the layout based on the current device and kind"""
        # We can't set a layout if we don't have any devices
        if not self.devices:
            return
        # Clear our layout
        self.layout().clear()
        shown_kind = [kind for kind in Kind if self._kinds[kind.name]]
        # Iterate through kinds
        signals = list()
        for kind in Kind:
            if kind in shown_kind:
                try:
                    for (attr, signal) in grab_kind(self.devices[0],
                                                    kind.name):
                        label = clean_attr(attr)
                        # Check twice for Kind as signal might have multiple
                        # kinds
                        if signal.kind in shown_kind:
                            signals.append((label, signal))
                except Exception:
                    logger.exception("Unable to add %s signals from %r",
                                     kind.name, self.devices[0])
        # Pick our sorting function
        if self._signal_order == SignalOrder.byKind:

            # Sort by kind
            def sorter(x):
                return self.kind_order.index(x[1].kind)

        elif self._signal_order == SignalOrder.byName:

            # Sort by name
            def sorter(x):
                return x[0]
        else:
            logger.exception("Unknown sorting type %r", self.sortBy)
            return
        # Add to layout
        for (label, signal) in sorted(set(signals), key=sorter):
            self.layout().add_signal(signal, label)

    def sizeHint(self):
        """Default SizeHint"""
        return QSize(240, 140)