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()
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)