Beispiel #1
0
class AxisLegend(wx.Panel):
    """
    The axis legend displayed in plots

    Properties:
    .unit: Unit displayed
    .range: the range of the scale
    .lo_ellipsis, hi_ellipsis: These properties, when true, show an ellipsis
        on the first and/or last tick marks
        e.g.: ...-10,  -5 , 0, 5, 10...
    .lock_va: a boolean VA which is connected to the lock toggle button.
        Typically, this button shows when the scale is locked from manipulation.
        If this property is set to None, the lock button will be hidden.
    """

    def __init__(self, parent, wid=wx.ID_ANY, pos=(0, 0), size=wx.DefaultSize,
                 style=wx.NO_BORDER, orientation=wx.HORIZONTAL):

        style |= wx.NO_BORDER
        wx.Panel.__init__(self, parent, wid, pos, size, style)

        self.SetBackgroundColour(parent.GetBackgroundColour())
        self.SetForegroundColour(parent.GetForegroundColour())

        self.tick_colour = wxcol_to_frgb(self.ForegroundColour)

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_SIZE, self.on_size)

        self._orientation = orientation
        self._max_tick_width = 32  # Largest pixel width of any label in use
        self._tick_spacing = 120 if orientation == wx.HORIZONTAL else 80
        self._unit = None
        self._lo_ellipsis = False
        self._hi_ellipsis = False
        self._lock_va = None

        # Explicitly set the min size
        if self._orientation == wx.HORIZONTAL:
            self.SetMinSize((-1, 32))
        else:
            self.SetMinSize((42, -1))

        # The following properties are volatile, meaning that they can change often
        self._value_range = None  # 2 tuple with the minimum and maximum value
        self._tick_list = None  # Lust of 2 tuples, containing the pixel position and value
        self._vtp_ratio = None  # Ratio to convert value to pixel
        self._pixel_space = None  # Number of available pixels

        # Axis lock button
        # This button allows a user to lock the scales from manipulation
        bmp = img.getBitmap("menu/btn_lock.png")
        bmpa = img.getBitmap("menu/btn_lock_a.png")
        bmph = img.getBitmap("menu/btn_lock.png")

        self.lockBtn = ImageToggleButton(self, pos=(0, 0), bitmap=bmp, size=(24, 24))
        self.lockBtn.bmpSelected = bmpa
        self.lockBtn.bmpHover = bmph
        self.lockBtn.Hide()

        self.on_size()  # Force a refresh

        if self._orientation == wx.HORIZONTAL:
            self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE))
        else:
            self.SetCursor(wx.Cursor(wx.CURSOR_SIZENS))

    @property
    def unit(self):
        return self._unit

    @property
    def lo_ellipsis(self):
        return self._lo_ellipsis

    @property
    def hi_ellipsis(self):
        return self._hi_ellipsis

    @unit.setter
    def unit(self, val):
        if self._unit != val:
            self._unit = val
            self.Refresh()

    @lo_ellipsis.setter
    def lo_ellipsis(self, val):
        if self._lo_ellipsis != val:
            self._lo_ellipsis = bool(val)
            self.Refresh()

    @hi_ellipsis.setter
    def hi_ellipsis(self, val):
        if self._hi_ellipsis != val:
            self._hi_ellipsis = bool(val)
            self.Refresh()

    @property
    def lock_va(self):
        return self._lock_va

    @lock_va.setter
    def lock_va(self, va):
        """
        Note: The caller must run in the main GUI Thread
        """
        if self._lock_va != va:
            if self._lock_va is not None:
                self._lock_va.unsubscribe(self._on_lock_va_change)

            # Show the lock button
            self.lockBtn.Show()
            self._lock_va = va

            self.lockBtn.Bind(wx.EVT_BUTTON, self._on_lock_click)
            self._lock_va.subscribe(self._on_lock_va_change)

            self.Refresh()

    def _on_lock_click(self, evt):
        self._lock_va.value = evt.isDown

    @call_in_wx_main
    def _on_lock_va_change(self, new_value):
        self.lockBtn.SetToggle(new_value)

    @property
    def range(self):
        return self._value_range

    @range.setter
    def range(self, val):
        if self._value_range != val:
            self._value_range = val
            self.Refresh()

    def clear(self):
        self._value_range = None
        self._tick_list = None
        self.Refresh()

    def on_size(self, _=None):
        if self._value_range:
            self.Refresh()

        if self._orientation == wx.HORIZONTAL:
            self.lockBtn.SetPosition((self.ClientSize.x - 24, 0))
        else:
            self.lockBtn.SetPosition((self.ClientSize.x - self.ClientSize.x / 2 - 12, 0))

    def on_mouse_enter(self, _=None):
        if self._orientation == wx.HORIZONTAL:
            self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE))
        else:
            self.SetCursor(wx.Cursor(wx.CURSOR_SIZENS))

    def on_mouse_leave(self, _=None):
        self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))

    @wxlimit_invocation(0.2)
    def Refresh(self):
        """
        Refresh, which can be called safely from other threads
        """
        if self:
            wx.Panel.Refresh(self)

    def on_paint(self, _):
        if self._value_range is None:
            return

        # shared function with the export method
        self._tick_list, self._vtp_ratio = calculate_ticks(self._value_range, self.ClientSize, self._orientation, self._tick_spacing)
        csize = self.ClientSize

        if self.lock_va is not None and self.lock_va.value == False:
            if self._orientation == wx.HORIZONTAL:
                csize.x -= 24

        # If min and max are very close, we need more significant numbers to
        # ensure the values displayed are different (ex 17999 -> 18003)
        rng = self._value_range
        if rng[0] == rng[1]:
            sig = None
        else:
            ratio_rng = max(abs(v) for v in rng) / (max(rng) - min(rng))
            sig = max(3, 1 + math.ceil(math.log10(ratio_rng * len(self._tick_list))))

        # Set Font
        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
        ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self))
        ctx.select_font_face(font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        ctx.set_font_size(font.GetPointSize())

        ctx.set_source_rgb(*self.tick_colour)
        ctx.set_line_width(2)
        ctx.set_line_join(cairo.LINE_JOIN_MITER)

        max_width = 0
        prev_lpos = 0 if self._orientation == wx.HORIZONTAL else csize.y

        for i, (pos, val) in enumerate(self._tick_list):
            label = units.readable_str(val, self.unit, sig)

            if i == 0 and self._lo_ellipsis:
                label = u"…" + label

            if i == len(self._tick_list) - 1 and self._hi_ellipsis:
                label = label + u"…"

            _, _, lbl_width, lbl_height, _, _ = ctx.text_extents(label)

            if self._orientation == wx.HORIZONTAL:
                lpos = pos - (lbl_width // 2)
                lpos = max(min(lpos, csize.x - lbl_width - 2), 2)
                # print (i, prev_right, lpos)
                if prev_lpos < lpos:
                    ctx.move_to(lpos, lbl_height + 8)
                    ctx.show_text(label)
                    ctx.move_to(pos, 5)
                    ctx.line_to(pos, 0)
                prev_lpos = lpos + lbl_width
            else:
                max_width = max(max_width, lbl_width)
                lpos = pos + (lbl_height // 2)
                lpos = max(min(lpos, csize.y), 2)

                if prev_lpos >= lpos + 20 or i == 0:
                    ctx.move_to(csize.x - lbl_width - 9, lpos)
                    ctx.show_text(label)
                    ctx.move_to(csize.x - 5, pos)
                    ctx.line_to(csize.x, pos)
                prev_lpos = lpos + lbl_height

            ctx.stroke()

        if self._orientation == wx.VERTICAL and max_width != self._max_tick_width:
            self._max_tick_width = max_width
            self.SetMinSize((self._max_tick_width + 14, -1))
            self.Parent.GetSizer().Layout()
Beispiel #2
0
class InfoLegend(wx.Panel):
    """ This class describes a legend containing the default controls that
    provide information about live data streams.

    TODO: give this class a more descriptive name

    """
    def __init__(self,
                 parent,
                 wid=-1,
                 pos=(0, 0),
                 size=wx.DefaultSize,
                 style=wx.NO_BORDER):

        style = style | wx.NO_BORDER
        super(InfoLegend, self).__init__(parent, wid, pos, size, style)

        # Cannot be a constant because loading bitmaps only works after wx.App
        # has been created.
        self._type_to_icon = (
            (MD_AT_AR, img.getBitmap("icon/ico_blending_ang.png")),
            (MD_AT_SPECTRUM, img.getBitmap("icon/ico_blending_spec.png")),
            (MD_AT_EM, img.getBitmap("icon/ico_blending_sem.png")),
            (MD_AT_OVV_TILES, img.getBitmap("icon/ico_blending_map.png")),
            (MD_AT_OVV_FULL, img.getBitmap("icon/ico_blending_navcam.png")),
            (MD_AT_HISTORY, img.getBitmap("icon/ico_blending_history.png")),
            (MD_AT_CL, img.getBitmap("icon/ico_blending_opt.png")),
            (MD_AT_FLUO, img.getBitmap("icon/ico_blending_opt.png")),
            (MD_AT_SLIT, img.getBitmap("icon/ico_blending_slit.png")),
        )

        self.SetBackgroundColour(parent.GetBackgroundColour())
        self.SetForegroundColour(parent.GetForegroundColour())

        ### Create child windows

        # Merge slider
        # TODO: should be possible to use VAConnector
        self.merge_slider = Slider(
            self,
            wx.ID_ANY,
            50,  # val
            0,
            100,
            size=(100, 12),
            style=(wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_TICKS
                   | wx.NO_BORDER))

        self.merge_slider.SetBackgroundColour(parent.GetBackgroundColour())
        self.merge_slider.SetForegroundColour(FG_COLOUR_DIS)  # "#4d4d4d"
        self.merge_slider.SetToolTip("Merge ratio")

        self.bmp_slider_left = wx.StaticBitmap(
            self, wx.ID_ANY, img.getBitmap("icon/ico_blending_opt.png"))
        self.bmp_slider_right = wx.StaticBitmap(
            self, wx.ID_ANY, img.getBitmap("icon/ico_blending_sem.png"))

        # Horizontal Field Width text
        self.hfw_text = wx.TextCtrl(self, style=wx.NO_BORDER | wx.CB_READONLY)
        self.hfw_text.SetBackgroundColour(parent.GetBackgroundColour())
        self.hfw_text.SetForegroundColour(parent.GetForegroundColour())
        self.hfw_text.SetToolTip("Horizontal Field Width")

        # Magnification text
        self.magnification_text = wx.TextCtrl(self,
                                              style=wx.NO_BORDER
                                              | wx.CB_READONLY)
        self.magnification_text.SetBackgroundColour(
            parent.GetBackgroundColour())
        self.magnification_text.SetForegroundColour(
            parent.GetForegroundColour())
        self.magnification_text.SetToolTip("Magnification")

        # Z position text
        self.zPos_text = wx.TextCtrl(self, style=wx.NO_BORDER | wx.CB_READONLY)
        self.zPos_text.SetBackgroundColour(parent.GetBackgroundColour())
        self.zPos_text.SetForegroundColour(parent.GetForegroundColour())
        self.zPos_text.SetToolTip("Z Position")
        self.zPos_text.Hide()

        # Feature show/hide button
        self._feature_toggle_va = None
        bmp = img.getBitmap("menu/btn_feature_toggle_off.png")
        bmpa = img.getBitmap("menu/btn_feature_toggle_on.png")
        bmph = img.getBitmap("menu/btn_feature_toggle_off.png")

        self.featureBtn = ImageToggleButton(self,
                                            pos=(0, 0),
                                            bitmap=bmp,
                                            size=(24, 24))
        self.featureBtn.bmpSelected = bmpa
        self.featureBtn.bmpHover = bmph
        self.featureBtn.Show(False)
        # Scale window
        self.scale_win = ScaleWindow(self)

        ## Child window layout

        # Sizer composition:
        #
        # +-------------------------------------------------------+
        # |  <Mag>  | <HFW> |    <Scale>    |  [Icon|Slider|Icon] |
        # +-------------------------------------------------------+

        slider_sizer = wx.BoxSizer(wx.HORIZONTAL)
        # TODO: need to have the icons updated according to the streams type
        slider_sizer.Add(self.bmp_slider_left,
                         0,
                         border=3,
                         flag=wx.ALIGN_CENTER | wx.RIGHT)
        slider_sizer.Add(self.merge_slider, 1, flag=wx.EXPAND)
        slider_sizer.Add(self.bmp_slider_right,
                         0,
                         border=3,
                         flag=wx.ALIGN_CENTER | wx.LEFT)
        slider_sizer.Add(self.featureBtn,
                         0,
                         border=3,
                         flag=wx.ALIGN_CENTER | wx.LEFT)

        control_sizer = wx.BoxSizer(wx.HORIZONTAL)
        control_sizer.Add(self.magnification_text,
                          2,
                          border=10,
                          flag=wx.RIGHT | wx.EXPAND)
        control_sizer.Add(self.hfw_text,
                          2,
                          border=10,
                          flag=wx.RIGHT | wx.EXPAND)
        control_sizer.Add(self.scale_win,
                          3,
                          border=10,
                          flag=wx.RIGHT | wx.EXPAND)
        control_sizer.Add(self.zPos_text,
                          2,
                          border=10,
                          flag=wx.RIGHT | wx.EXPAND)
        control_sizer.Add(slider_sizer,
                          0,
                          border=10,
                          flag=wx.ALIGN_CENTER | wx.RIGHT)
        # border_sizer is needed to add a border around the legend
        border_sizer = wx.BoxSizer(wx.VERTICAL)
        border_sizer.Add(control_sizer, border=6, flag=wx.ALL | wx.EXPAND)

        self.SetSizerAndFit(border_sizer)

        ## Event binding

        # Dragging the slider should set the focus to the right view
        self.merge_slider.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.merge_slider.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)

        # Make sure that mouse clicks on the icons set the correct focus
        self.bmp_slider_left.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.bmp_slider_right.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)

        # Set slider to min/max
        self.bmp_slider_left.Bind(wx.EVT_LEFT_UP, parent.OnSliderIconClick)
        self.bmp_slider_right.Bind(wx.EVT_LEFT_UP, parent.OnSliderIconClick)

        self.hfw_text.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.magnification_text.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.zPos_text.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)

        # Explicitly set the
        # self.SetMinSize((-1, 40))

    def clear(self):
        pass

    # Make mouse events propagate to the parent
    def OnLeftDown(self, evt):
        evt.ResumePropagation(1)
        evt.Skip()

    def OnLeftUp(self, evt):
        evt.ResumePropagation(1)
        evt.Skip()

    def set_hfw_label(self, label):
        approx_width = len(label) * 7
        self.hfw_text.SetMinSize((approx_width, -1))
        self.hfw_text.SetValue(label)
        self.Layout()

    def set_mag_label(self, label):
        # TODO: compute the real size needed (using GetTextExtent())
        approx_width = len(label) * 7
        self.magnification_text.SetMinSize((approx_width, -1))
        self.magnification_text.SetValue(label)
        self.Layout()

    def set_zPos_label(self, label):
        """
        label (unicode or None): if None, zPos is hidden, otherwise show the value
        """
        if label is None:
            self.zPos_text.Hide()
        else:
            self.zPos_text.Show()
            # TODO: compute the real size needed (using GetTextExtent())
            approx_width = len(label) * 7
            self.zPos_text.SetMinSize((approx_width, -1))
            self.zPos_text.SetValue(label)
        self.Layout()

    def set_stream_type(self, side, acq_type):
        """
        Set the stream type, to put the right icon on the merge slider

        :param side: (wx.LEFT or wx.RIGHT): whether this set the left or right
            stream
        :param acq_type: (String): acquisition type associated with stream
        """

        for t, type_icon in self._type_to_icon:
            if acq_type == t:
                icon = type_icon
                break
        else:
            # Don't fail too bad
            icon = img.getBitmap("icon/ico_blending_opt.png")
            if self.merge_slider.IsShown():
                logging.warning("Failed to find icon for stream of type %s",
                                acq_type)
        if side == wx.LEFT:
            self.bmp_slider_left.SetBitmap(icon)
        else:
            self.bmp_slider_right.SetBitmap(icon)

    @property
    def feature_toggle_va(self):
        return self._feature_toggle_va

    @feature_toggle_va.setter
    def feature_toggle_va(self, va):
        """
        Set feature toggle button with the va, show it and listen to the va changes to set/unset its toggle value
        Note: The caller must run in the main GUI Thread
        """
        if self._feature_toggle_va != va:
            if self._feature_toggle_va is not None:
                self._feature_toggle_va.unsubscribe(
                    self._feature_toggle_change)

            # Show the feature toggle button
            self.featureBtn.Show()
            self.featureBtn.SetToggle(va.value)
            self._feature_toggle_va = va

            self.featureBtn.Bind(wx.EVT_BUTTON, self._feature_toggle_click)
            self._feature_toggle_va.subscribe(self._feature_toggle_change)

            self.Refresh()

    def _feature_toggle_click(self, evt):
        self._feature_toggle_va.value = evt.isDown

    @call_in_wx_main
    def _feature_toggle_change(self, new_value):
        self.featureBtn.SetToggle(new_value)