# These are Quisk widgets

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import sys, re, json
import wx, wx.lib.buttons, wx.lib.stattext
# The main script will alter quisk_conf_defaults to include the user's config file.
import quisk_conf_defaults as conf
import _quisk as QS

wxVersion = wx.version()[0]

def EmptyBitmap(width, height):
  if wxVersion in ('2', '3'):
    return wx.EmptyBitmap(width, height)
  else:
    return wx.Bitmap(width, height)

def MakeWidgetGlobals():
  global button_font, button_uline_font, button_bezel, button_width, button_height, button_text_width, button_text_height
  global _bitmap_menupop, _bitmap_sliderpop, _bitmap_cyclepop
  button_bezel = 3		# size of button bezel in pixels
  button_font = wx.Font(conf.button_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
           wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
  button_uline_font = wx.Font(conf.button_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
           wx.FONTWEIGHT_NORMAL, True, conf.quisk_typeface)
  dc = wx.MemoryDC()
  dc.SetFont(button_font)
  tmp_bm = EmptyBitmap(1, 1)		# Thanks to NS4Y
  dc.SelectObject(tmp_bm)
  button_text_width, button_text_height = dc.GetTextExtent('0')
  button_width = button_text_width + 2 + 2 * button_bezel # + 4 * int(self.useFocusInd)
  button_height = button_text_height + 2 + 2 * button_bezel # + 4 * int(self.useFocusInd)
  # Make a bitmap for the slider pop button
  height = button_text_height + 2	# button height less bezel
  width = height
  _bitmap_sliderpop = EmptyBitmap(height, height)
  dc.SelectObject(_bitmap_sliderpop)
  pen = wx.Pen(conf.color_enable, 1)
  dc.SetPen(pen)
  brush = wx.Brush(conf.color_btn)
  dc.SetBackground(brush)
  dc.Clear()
  w = width * 5 // 10
  w += w % 2
  bd = (width - 1 - w) // 2
  x1 = bd
  x2 = x1 + w
  y1 = 0
  y2 = height - y1 - 1
  dc.DrawLine(x1, y1, x2, y1)
  dc.DrawLine(x2, y1, x2, y2)
  dc.DrawLine(x2, y2, x1, y2)
  dc.DrawLine(x1, y2, x1, y1)
  x0 = (x2 + x1) // 2
  dc.DrawLine(x0, y1 + 3, x0, y2 - 2)
  y0 = height * 6 // 10
  dc.DrawLine(x0 - 2, y0, x0 + 3, y0)
  y0 -= 1
  color = pen.GetColour()
  r = color.Red()
  g = color.Green()
  b = color.Blue()
  f = 160
  r = min(r + f, 255)
  g = min(g + f, 255)
  b = min(b + f, 255)
  color = wx.Colour(r, g, b)
  dc.SetPen(wx.Pen(color, 1, wx.SOLID))
  dc.DrawLine(x0 - 2, y0, x0 + 3, y0)
  dc.SelectObject(wx.NullBitmap)
  # Make a bitmap for the menu pop button
  _bitmap_menupop = EmptyBitmap(height, height)
  dc.SelectObject(_bitmap_menupop)
  dc.SetBackground(brush)
  dc.Clear()
  dc.SetPen(wx.Pen(conf.color_enable, 1))
  dc.SetBrush(wx.Brush(conf.color_enable))
  x = 3
  for y in range(2, height - 3, 5):
    dc.DrawRectangle(x, y, 3, 3)
    dc.DrawLine(x + 5, y + 1, width - 3, y + 1)
  dc.SelectObject(wx.NullBitmap)
  # Make a bitmap for the cycle button
  _bitmap_cyclepop = EmptyBitmap(height, height)
  dc.SelectObject(_bitmap_cyclepop)
  dc.SetBackground(brush)
  dc.SetFont(button_font)
  dc.Clear()
  w, h = dc.GetTextExtent(conf.btn_text_cycle)
  dc.DrawText(conf.btn_text_cycle, (height - x) // 2, (height - y) // 2)
  dc.SelectObject(wx.NullBitmap)

def FreqFormatter(freq):	# Format the string or integer frequency by adding blanks
  freq = int(freq)
  if freq >= 0:
    t = str(freq)
    minus = ''
  else:
    t = str(-freq)
    minus = '- '
  l = len(t)
  if l > 9:
    txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:])
  elif l > 6:
    txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:])
  elif l > 3:
    txt = "%s%s %s" % (minus, t[0:-3], t[-3:])
  else:
    txt = minus + t
  return txt

class FrequencyDisplay(wx.lib.stattext.GenStaticText):
  """Create a frequency display widget."""
  def __init__(self, frame, width, height):
    wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3',
         style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE)
    border = 4
    for points in range(30, 6, -1):
      font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
      self.SetFont(font)
      w, h = self.GetTextExtent('333 444 555 Hz')
      if w < width and h < height - border * 2:
        break
    self.SetSizeHints(w, h, w * 5, h)
    self.height = h
    self.points = points
    border = self.border = (height - self.height) // 2
    self.height_and_border = h + border * 2
    self.SetBackgroundColour(conf.color_freq)
    self.SetForegroundColour(conf.color_freq_txt)
    self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)	# Click on a digit changes the frequency
    self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)
    self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
    self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
    self.timer = wx.Timer(self)                     # Holding a digit continuously changes the frequency
    self.Bind(wx.EVT_TIMER, self.OnTimer)
    self.repeat_time = 0                           # Repeat function is inactive
  def Clip(self, clip):
    """Change color to indicate clipping."""
    if clip:
      self.SetBackgroundColour('deep pink')
    else:
      self.SetBackgroundColour(conf.color_freq)
    self.Refresh()
  def Display(self, freq):
    """Set the frequency to be displayed."""
    txt = FreqFormatter(freq)
    self.SetLabel('%s Hz' % txt)
  def GetIndex(self, event):		# Determine which digit is being changed
    mouse_x, mouse_y = event.GetPosition()
    width, height = self.GetClientSize().Get()
    text = self.GetLabel()
    tw, th = self.GetTextExtent(text)
    edge = (width - tw) // 2
    digit = self.GetTextExtent('0')[0]
    blank = self.GetTextExtent(' ')[0]
    if mouse_x < edge - digit:
      return None
    x = width - edge - self.GetTextExtent(" Hz")[0] - mouse_x
    if x < 0:
      return None
    #print ('size', width, height, 'mouse', mouse_x, mouse_y, 'digit', digit, 'blank', blank)
    shift = 0
    while x > digit * 3:
      shift += 1
      x -= digit * 3 + blank
    if x < 0:
      return None
    return x // digit + shift * 3	# index of digit being changed
  def OnLeftDown(self, event):		# Click on a digit changes the frequency
    if self.repeat_time:
      self.timer.Stop()
      self.repeat_time = 0
    index = self.GetIndex(event)
    if index is not None:
      self.index = index
      mouse_x, mouse_y = event.GetPosition()
      width, height = self.GetClientSize().Get()
      if mouse_y < height // 2:
        self.increase = True
      else:
        self.increase = False
      self.ChangeFreq()
      self.repeat_time = 300		# first button push
      self.timer.Start(milliseconds=300, oneShot=True)
  def OnLeftUp(self, event):
    self.timer.Stop()
    self.repeat_time = 0
  def ChangeFreq(self):
    text = self.GetLabel()
    text = text.replace(' ', '')[:-2]
    text = text[:len(text)-self.index] + '0' * self.index
    if self.increase:
      freq = int(text) + 10 ** self.index
    else:
      freq = int(text) - 10 ** self.index
      if freq <= 0 and self.index > 0:
        freq = 10 ** (self.index - 1)
    #print ('X', x, 'N', n, text, 'freq', freq)
    application.ChangeRxTxFrequency(None, freq)
  def OnTimer(self, event):
    if self.repeat_time == 300:     # after first push, turn on repeats
      self.repeat_time = 150
    elif self.repeat_time > 20:
      self.repeat_time -= 5
    self.ChangeFreq()
    self.timer.Start(milliseconds=self.repeat_time, oneShot=True)
  def OnWheel(self, event):
    index = self.GetIndex(event)
    if index is not None:
      self.index = index
      if event.GetWheelRotation() > 0:
        self.increase = True
      else:
        self.increase = False
      self.ChangeFreq()

class SliderBoxH:
  """A horizontal control with a slider and text with a value.  The text must have a %d or %f if display is True."""
  def __init__(self, parent, text, init, themin, themax, handler, display, pos, width, scale=1):
    self.text = text
    self.themin = themin
    self.themax = themax
    self.handler = handler
    self.display = display
    self.scale = scale
    self.idName = text
    if display:		# Display the slider value
      t1 = self.text % (themin * scale)
      t2 = self.text % (themax * scale)
      if len(t1) > len(t2):		# set text size to the largest
        t = t1
      else:
        t = t2
    else:
      t = self.text
    if pos is None:
      self.text_ctrl = wx.StaticText(parent, -1, t, style=wx.ST_NO_AUTORESIZE)
      w2, h2 = self.text_ctrl.GetSize()
      self.text_ctrl.SetSizeHints(w2, -1, w2)
      self.slider = wx.Slider(parent, -1, init, themin, themax)
    else:	# Thanks to Stephen Hurd
      self.text_ctrl = wx.StaticText(parent, -1, t, pos=pos)
      w2, h2 = self.text_ctrl.GetSize()
      self.slider = wx.Slider(parent, -1, init, themin, themax)
      w3, h3 = self.slider.GetSize()
      p2 = pos[1]
      if h3 > h2:
        p2 -= (h3 - h2) / 2
      else:
        p2 += (h2 - h3) / 2
      self.slider.SetSize((width - w2, h3))
      self.slider.SetPosition((pos[0] + w2, p2))
    self.slider.Bind(wx.EVT_SCROLL, self.OnScroll)
    self.text_ctrl.SetForegroundColour(parent.GetForegroundColour())
    self.OnScroll()
  def OnScroll(self, event=None):
    if event:
      event.Skip()
      if self.handler:
        if application.remote_control_head:
          application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetValue()}\n')
        self.handler(event)
    if self.display:
      t = self.text % (self.slider.GetValue() * self.scale)
    else:
      t = self.text
    self.text_ctrl.SetLabel(t)
  def GetValue(self):
    return self.slider.GetValue()
  def SetValue(self, value):
    # Set slider visual position; does not call handler
    self.slider.SetValue(value)
    self.OnScroll()
  def GetDecValue(self):	# Return the value 0.0 to 1.0
    return float(self.slider.GetValue() - self.themin) / (self.themax - self.themin)
  def SetDecValue(self, value, do_cmd=True):	# Set the value with a decimal 0.0 to 1.0
    if value < 0.0:
      value = 0.0
    elif value > 1.0:
      value = 1.0
    value = value * (self.themax - self.themin) + self.themin
    value = int(value + 0.5)
    self.SetValue(value)
    if do_cmd:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetValue()}\n')
      self.handler(None)

class SliderBoxHH(SliderBoxH, wx.BoxSizer):
  """A horizontal control with a slider and text with a value.  The text must have a %d if display is True."""
  def __init__(self, parent, text, init, themin, themax, handler, display, scale=1):
    wx.BoxSizer.__init__(self, wx.HORIZONTAL)
    SliderBoxH.__init__(self, parent, text, init, themin, themax, handler, display, None, None, scale)
    #font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
    #self.text_ctrl.SetFont(font)
    self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER)
    self.Add(self.slider, 1, wx.ALIGN_CENTER)

class SliderBoxV(wx.BoxSizer):
  """A vertical box containing a slider and a text heading"""
  # Note: A vertical wx slider has the max value at the bottom.  This is
  # reversed for this control.
  def __init__(self, parent, text, init, themax, handler, display=False, themin=0):
    wx.BoxSizer.__init__(self, wx.VERTICAL)
    self.slider = wx.Slider(parent, -1, init, themin, themax, style=wx.SL_VERTICAL|wx.SL_INVERSE)
    self.slider.Bind(wx.EVT_SCROLL, self.OnScroll)
    self.handler = handler
    self.idName = text
    sw, sh = self.slider.GetSize()
    self.text = text
    self.themin = themin
    self.themax = themax
    if display:		# Display the slider value when it is thumb'd
      self.text_ctrl = wx.StaticText(parent, -1, str(themax))
      self.text_ctrl.SetFont(button_font)
      charw = self.text_ctrl.GetCharWidth()
      w1, self.text_height = self.text_ctrl.GetSize()	# Measure size with max number
      w1 += charw
      self.text_ctrl.SetLabel(str(themin))
      w3, h3 = self.text_ctrl.GetSize()	# Measure size with min number
      w3 += charw
      self.text_ctrl.SetLabel(text)
      w2, h2 = self.text_ctrl.GetSize()	# Measure size with text
      w2 += charw
      self.width = max(w1, w2, w3, sw)
      self.text_ctrl.SetSizeHints(self.width, -1, self.width)
      self.slider.SetSizeHints(self.width, -1, self.width)
      self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change)
      self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone)
    else:
      self.text_ctrl = wx.StaticText(parent, -1, text)
      self.text_ctrl.SetFont(button_font)
      charw = self.text_ctrl.GetCharWidth()
      w2, self.text_height = self.text_ctrl.GetSize()	# Measure size with text
      w2 += charw
      self.width = max(w2, sw)
      self.text_ctrl.SetSizeHints(self.width, -1, self.width)
      self.slider.SetSizeHints(self.width, -1, self.width)
    self.text_ctrl.SetForegroundColour(parent.GetForegroundColour())
    self.Add(self.text_ctrl, 0, 0)
    self.Add(self.slider, 1, 0)
  def OnScroll(self, event):
    if application.remote_control_head:
      application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetValue()}\n')
    self.handler(event)
  def Change(self, event):
    event.Skip()
    self.text_ctrl.SetLabel(str(self.slider.GetValue()))
  def ChangeDone(self, event):
    event.Skip()
    self.text_ctrl.SetLabel(self.text)
  def GetValue(self):
    return self.slider.GetValue()
  def SetValue(self, value):
    # Set slider visual position; does not call handler
    self.slider.SetValue(value)
  def GetDecValue(self):	# Return the value 0.0 to 1.0
    return float(self.slider.GetValue() - self.themin) / (self.themax - self.themin)
  def SetDecValue(self, value, do_cmd=True):	# Set the value with a decimal 0.0 to 1.0
    if value < 0.0:
      value = 0.0
    elif value > 1.0:
      value = 1.0
    value = value * (self.themax - self.themin) + self.themin
    value = int(value + 0.5)
    self.SetValue(value)
    if do_cmd:
      self.OnScroll(None)

class QuiskText1(wx.lib.stattext.GenStaticText):
  # Self-drawn text for QuiskText.
  def __init__(self, parent, size_text, height, style=0, fixed=False):
    wx.lib.stattext.GenStaticText.__init__(self, parent, -1, '',
                 pos = wx.DefaultPosition, size = wx.DefaultSize,
                 style = wx.ST_NO_AUTORESIZE|style,
                 name = "QuiskText1")
    self.fixed = fixed
    self.size_text = size_text
    self.pen = wx.Pen(conf.color_btn, 2)
    self.brush = wx.Brush(conf.color_freq)
    self.SetForegroundColour(conf.color_freq_txt)
    self.SetSizeHints(1, height, 9999, height)
  def _MeasureFont(self, dc, width, height):
    # Set decreasing point size until size_text fits in the space available
    for points in range(20, 6, -1):
      if self.fixed:
        font = wx.Font(points, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
      else:
        font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
      dc.SetFont(font)
      w, h = dc.GetTextExtent(self.size_text)
      if w < width and h < height:
        break
    self.size_text = ''
    self.SetFont(font)
  def OnPaint(self, event):
    dc = wx.PaintDC(self)
    width, height = self.GetClientSize().Get()
    if not width or not height:
      return
    dc.SetPen(self.pen)
    dc.SetBrush(self.brush)
    dc.DrawRectangle(1, 1, width-1, height-1)
    label = self.GetLabel()
    if not label:
      return
    if self.size_text:
      self._MeasureFont(dc, width-2, height-2)
    else:
      dc.SetFont(self.GetFont())
    if self.IsEnabled():
      dc.SetTextForeground(self.GetForegroundColour())
    else:
      dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
    style = self.GetWindowStyleFlag()
    w, h = dc.GetTextExtent(label)
    y = (height - h) // 2
    if y < 0:
      y = 0
    if style & wx.ALIGN_RIGHT:
      x = width - w - 4
    elif style & wx.ALIGN_CENTER:
      x = (width - w - 1)//2
    else:
      x = 3
    dc.DrawText(label, x, y)

class QuiskText(wx.BoxSizer):
  # A one-line text display left/right/center justified and vertically centered.
  # The height of the control is fixed as "height".  The width is expanded.
  # The font is chosen so size_text fits in the client area.
  def __init__(self, parent, size_text, height, style=0, fixed=False):
    wx.BoxSizer.__init__(self, wx.HORIZONTAL)
    self.TextCtrl = QuiskText1(parent, size_text, height, style, fixed)
    self.Add(self.TextCtrl, 1, flag=wx.ALIGN_CENTER_VERTICAL)
  def SetLabel(self, label):
    self.TextCtrl.SetLabel(label)
  def GetLabel(self):
    return self.TextCtrl.GetLabel()

# Start of our button classes.  They are compatible with wxPython GenButton
# buttons.  Use the usual methods for access:
# GetLabel(self), SetLabel(self, label):	Get and set the label
# Enable(self, flag), Disable(self), IsEnabled(self):	Enable / Disable
# GetValue(self), SetValue(self, value):	Get / Set check button state True / False
# SetIndex(self, index):	For cycle buttons, set the label from its index

class QuiskButtons:
  """Base class for special buttons."""
  def __init__(self, idName):
    self.idName = idName	# idName can be ''
    self.up_brush = wx.Brush(conf.color_btn)
    r, g, b = self.up_brush.GetColour().Get(False)
    r, g, b = min(255,r+32), min(255,g+32), min(255,b+32)
    self.down_brush = wx.Brush(wx.Colour(r, g, b))
    self.color_disable = conf.color_disable
  def InitButtons(self, text, text_color=None):
    if text_color:
      self.text_color = text_color
    else:
      self.text_color = conf.color_enable
    self.SetBezelWidth(button_bezel)
    self.SetBackgroundColour(conf.color_btn)
    self.SetUseFocusIndicator(False)
    self.decoration = None
    self.char_shortcut = ''
    self.SetFont(button_font)
    if text:
      w, h = self.GetTextExtent(text)
    else:
      w, h = self.GetTextExtent("OK")
      self.Disable()	# create a size for null text, but Disable()
    w += button_bezel * 2 + self.GetCharWidth()
    h = h * 12 // 10
    h += button_bezel * 2
    self.SetSizeHints(w, h, 999, h, 1, 1)
  def DrawLabel(self, dc, width, height, dx=0, dy=0):	# Override to change Disable text color
      if self.up:	# Clear the background here
        dc.SetBrush(self.up_brush)
      else:
        dc.SetBrush(self.down_brush)
      dc.SetPen(wx.TRANSPARENT_PEN)
      bw = self.bezelWidth
      dc.DrawRectangle(bw, bw, width - bw * 2, height - bw * 2)
      dc.SetFont(self.GetFont())
      label = self.GetLabel()
      tw, th = dc.GetTextExtent(label)
      self.label_width = tw
      dx = dy = self.labelDelta
      slabel = re.split('('+u'\u25CF'+')', label)	# unicode symbol for record: a filled dot
      for part in slabel:		# This code makes the symbol red.  Thanks to Christof, DJ4CM.
        if self.IsEnabled():
          if part == u'\u25CF':
            dc.SetTextForeground('red')
          else:
            dc.SetTextForeground(self.text_color)
        else:
          dc.SetTextForeground(self.color_disable)
        if self.char_shortcut:
          scut = part.split(self.char_shortcut, 1)
          if len(scut) == 2:	# The shortcut character is present in the string
            dc.DrawText(scut[0], (width-tw)//2+dx, (height-th)//2+dy)
            dx += dc.GetTextExtent(scut[0])[0]
            dc.SetFont(button_uline_font)
            dc.DrawText(self.char_shortcut, (width-tw)//2+dx, (height-th)//2+dy)
            dx += dc.GetTextExtent(self.char_shortcut)[0]
            dc.SetFont(self.GetFont())
            dc.DrawText(scut[1], (width-tw)//2+dx, (height-th)//2+dy)
            dx += dc.GetTextExtent(scut[1])[0]
          else:
            dc.DrawText(part, (width-tw)//2+dx, (height-th)//2+dy)
        else:
          dc.DrawText(part, (width-tw)//2+dx, (height-th)//2+dy)
        dx += dc.GetTextExtent(part)[0]
      if self.decoration and conf.decorate_buttons:
        wd, ht = dc.GetTextExtent(self.decoration)
        dc.DrawText(self.decoration, width - wd * 15 // 10, (height - ht) // 2)
  def OnKeyDown(self, event):
    pass
  def OnKeyUp(self, event):
    pass
  def DrawGlyphCycle(self, dc, width, height):	# Add a cycle indicator to the label
    if not conf.decorate_buttons:
      return
    uch = conf.btn_text_cycle
    wd, ht = dc.GetTextExtent(uch)
    if wd * 2 + self.label_width > width:		# not enough space
      uch = conf.btn_text_cycle_small
      wd, ht = dc.GetTextExtent(uch)
      dc.DrawText(uch, width - wd, (height - ht) // 2)
    else:
      dc.DrawText(uch, width - wd * 15 // 10, (height - ht) // 2)
  def SetColorGray(self):
    self.SetBackgroundColour(wx.Colour(220, 220, 220))	# This sets the bezel colors
    self.SetBezelWidth(2)
    self.text_color = 'black'
    self.color_disable = 'white'
    self.up_brush = wx.Brush(wx.Colour(220, 220, 220))
    self.down_brush = wx.Brush(wx.Colour(240, 240, 240))

class QuiskBitmapButton(wx.lib.buttons.GenBitmapButton):
  def __init__(self, parent, command, bitmap, use_right=False):
    self.idName = "Bitmap"
    self.command = command
    self.bitmap = bitmap
    wx.lib.buttons.GenBitmapButton.__init__(self, parent, -1, bitmap)
    self.SetFont(button_font)
    self.SetBezelWidth(button_bezel)
    self.SetBackgroundColour(conf.color_btn)
    self.SetUseFocusIndicator(False)
    self.Bind(wx.EVT_BUTTON, self.OnButton)
    self.direction = 1
    if use_right:
      self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
      self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
  def DoGetBestSize(self):
    return self.bitmap.GetWidth() + button_bezel * 2, self.bitmap.GetHeight() + button_bezel * 2
  def OnButton(self, event):
    if self.command:
      self.command(event)
  def OnRightDown(self, event):
    if self.GetBitmapLabel() == _bitmap_cyclepop:
      self.OnLeftDown(event) 
  def OnRightUp(self, event):
    if self.GetBitmapLabel() == _bitmap_cyclepop:
      self.direction = -1
      self.OnLeftUp(event)
      self.direction = 1

class QuiskPushbutton(QuiskButtons, wx.lib.buttons.GenButton):
  """A plain push button widget."""
  def __init__(self, parent, command, text, use_right=False, text_color=None, style=0):
    QuiskButtons.__init__(self, text)
    wx.lib.buttons.GenButton.__init__(self, parent, -1, text, style=style)
    self.command = command
    self.Bind(wx.EVT_BUTTON, self.OnButton)
    self.InitButtons(text, text_color)
    self.direction = 1
    if use_right:
      self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
      self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
  def OnButton(self, event):
    if self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(self.idName + ';1\n')
      self.command(event)
  def OnRightDown(self, event):
    self.direction = -1
    self.OnLeftDown(event) 
  def OnRightUp(self, event):
    self.OnLeftUp(event)
    self.direction = 1
  def SetIndex(self, index):
    if self.command and index:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(self.idName + ';1\n')
      event = wx.PyEvent()
      event.SetEventObject(self)
      self.command(event)
      

class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.GenButton):
  """A push button that repeats when held down."""
  def __init__(self, parent, command, text, up_command=None, use_right=False):
    QuiskButtons.__init__(self, text)
    wx.lib.buttons.GenButton.__init__(self, parent, -1, text)
    self.command = command
    self.up_command = up_command
    self.timer = wx.Timer(self)
    self.Bind(wx.EVT_TIMER, self.OnTimer)
    self.Bind(wx.EVT_BUTTON, self.OnButton)
    self.InitButtons(text)
    self.repeat_state = 0		# repeater button inactive
    self.direction = 1
    if use_right:
      self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
      self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
  def SendCommand(self, command):
    if command:
      event = wx.PyEvent()
      event.SetEventObject(self)
      command(event)
  def OnLeftDown(self, event):
    if self.IsEnabled():
      self.shift = event.ShiftDown()
      self.control = event.ControlDown()
      self.SendCommand(self.command)
      self.repeat_state = 1		# first button push
      self.timer.Start(milliseconds=300, oneShot=True)
    wx.lib.buttons.GenButton.OnLeftDown(self, event)
  def OnLeftUp(self, event):
    if self.IsEnabled():
      self.repeat_state = 0
      self.timer.Stop()
      self.SendCommand(self.up_command)
    wx.lib.buttons.GenButton.OnLeftUp(self, event)
  def OnRightDown(self, event):
    if self.IsEnabled():
      self.shift = event.ShiftDown()
      self.control = event.ControlDown()
      self.direction = -1
      self.OnLeftDown(event) 
  def OnRightUp(self, event):
    if self.IsEnabled():
      self.OnLeftUp(event)
      self.direction = 1
  def OnTimer(self, event):
    if self.repeat_state == 1:	# after first push, turn on repeats
      self.timer.Start(milliseconds=150, oneShot=False)
      self.repeat_state = 2
    if self.repeat_state:		# send commands until button is released
      self.SendCommand(self.command)
  def OnButton(self, event):
    pass	# button command not used
  def Shortcut(self, event, button_name=None):
    if not self.IsEnabled():
      return
    if button_name == "_end_":
      self.repeat_state = 0
      self.timer.Stop()
      self.SendCommand(self.up_command)
    else:
      self.shift = False
      self.control = False
      self.SendCommand(self.command)
      self.repeat_state = 1		# first button push
      self.timer.Start(milliseconds=300, oneShot=True)

class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.GenToggleButton):
  """A button that pops up and down, and changes color with each push."""
  # Check button; get the checked state with self.GetValue()
  def __init__(self, parent, command, text, color=None, use_right=False):
    QuiskButtons.__init__(self, text)
    wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text)
    self.InitButtons(text)
    self.Bind(wx.EVT_BUTTON, self.OnButton)
    self.button_down = 0		# used for radio buttons
    self.command = command
    if color is None:
      self.down_brush = wx.Brush(conf.color_check_btn)
    else:
      self.down_brush = wx.Brush(color)
    self.direction = 1
    if use_right:
      self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
      self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
  def SetValue(self, value, do_cmd=False):
    wx.lib.buttons.GenToggleButton.SetValue(self, value)
    self.button_down = value
    if do_cmd and self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
      event = wx.PyEvent()
      event.SetEventObject(self)
      self.command(event)
  def SetIndex(self, index, do_cmd=False):
    self.SetValue(bool(index), do_cmd)
  def GetIndex(self):
    if self.GetValue():
      return 1
    else:
      return 0
  def OnButton(self, event):
    if self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
      self.command(event)
  def OnRightDown(self, event):
    self.direction = -1
    self.OnLeftDown(event) 
  def OnRightUp(self, event):
    self.OnLeftUp(event)
    self.direction = 1
  def Shortcut(self, event, button_name=None):
    if self.IsEnabled():
      self.SetValue(not self.GetValue(), True)

class QuiskBitField(wx.Window):
  """A control used to set/unset bits."""
  def __init__(self, parent, numbits, value, height, command):
    self.numbits = numbits
    self.value = value
    self.height = height
    self.command = command
    self.backgroundBrush = wx.Brush('white')
    self.pen = wx.Pen('light gray', 1)
    self.font = parent.GetFont()
    self.charx, self.chary = parent.GetTextExtent('1')
    self.linex = []	# x pixel of vertical lines
    self.bitx = []	# x pixel of character for bits
    space = self.space = max(2, self.charx * 2 // 10)
    width = 0
    for i in range(numbits - 1):
      width += space + self.charx + space
      self.linex.append(width)
    width = space
    for i in range(numbits):
      self.bitx.append(width)
      width += self.charx + space * 2
    wx.Window.__init__(self, parent, size=(width + 4, height), style=wx.BORDER_SUNKEN)
    self.Bind(wx.EVT_PAINT, self.OnPaint)
    self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
  def OnPaint(self, event):
    dc = wx.PaintDC(self)
    dc.SetBackground(self.backgroundBrush)
    dc.Clear()
    dc.SetFont(self.font)
    dc.SetPen(self.pen)
    for x in self.linex:
      dc.DrawLine(x, 0, x, self.height)
    for i in range(self.numbits):
      power = self.numbits - i - 1
      x = self.bitx[i]
      if self.value & (1 << power):
        dc.DrawText('1', x, 0)
  def OnLeftDown(self, event):
    mouse_x, mouse_y = event.GetPosition().Get()
    for index in range(len(self.linex)):
      if mouse_x < self.linex[index]:
        break
    else:
      index = self.numbits - 1
    power = self.numbits - index - 1
    mask = 1 << power
    if self.value & mask:
      self.value &= ~ mask
    else:
      self.value |= mask
    self.Refresh()
    if self.command:
      self.command(self)

class QFilterButtonWindow(wx.Frame):
  """Create a window with controls for the button"""
  def __init__(self, wrap, value):
    self.wrap = wrap
    l = self.valuelist = []
    bw = 10
    incr = 10
    for i in range(0, 101):
      l.append(bw)
      bw += incr
      if bw == 100:
        incr = 20
      elif bw == 500:
        incr = 50
      elif bw == 1000:
        incr = 100
      elif bw == 5000:
        incr = 500
      elif bw == 10000:
        incr = 1000
    x, y = wrap.GetPosition().Get()
    x, y = wrap.GetParent().ClientToScreen(wx.Point(x, y))
    w, h = wrap.GetSize()
    height = h * 10
    size = (w, height)
    if sys.platform == 'win32':
      pos = (x, y - height)
      t = 'Filter'
    else:
      pos = (x, y - height - h)
      t = ''
    wx.Frame.__init__(self, wrap.GetParent(), -1, t, pos, size,
      wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU)
    self.SetBackgroundColour(conf.color_freq)
    self.Bind(wx.EVT_CLOSE, self.OnClose)
    try:
      index = self.valuelist.index(value)
    except ValueError:
      index = 0
      self.wrap.button.slider_value = self.valuelist[0]
    self.slider = wx.Slider(self, -1, index, 0, 100, (0, 0), (w//2, height), wx.SL_VERTICAL|wx.SL_INVERSE)
    self.slider.Bind(wx.EVT_SCROLL, self.OnSlider)
    self.SetTitle("%d" % self.valuelist[index])
    self.Show()
    self.slider.SetFocus()
  def OnSlider(self, event):
    index = self.slider.GetValue()
    value = self.valuelist[index]
    self.SetTitle("%d" % value)
    self.wrap.ChangeSlider(value)
    #self.wrap.SetLabel(str(value))
    #self.wrap.SetValue(True, True)
    #application.filterAdjBw1 = value
  def OnClose(self, event):
    self.wrap.adjust = None
    self.Destroy()

class QSliderButtonWindow(wx.Frame):
  """Create a window with controls for the button"""
  def __init__(self, button, value):
    self.button = button
    x, y = button.GetPosition().Get()
    x, y = button.GetParent().ClientToScreen(wx.Point(x, y))
    w, h = button.GetSize()
    height = h * 10
    size = (w, height)
    if sys.platform == 'win32':
      pos = (x, y - height)
    else:
      pos = (x, y - height - h)
    wx.Frame.__init__(self, button.GetParent(), -1, '', pos, size,
      wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU)
    self.SetBackgroundColour(conf.color_freq)
    self.Bind(wx.EVT_CLOSE, self.OnClose)
    self.slider = wx.Slider(self, -1, value,
             self.button.slider_min, self.button.slider_max,
             (0, 0), (w//2, height), wx.SL_VERTICAL|wx.SL_INVERSE)
    self.slider.Bind(wx.EVT_SCROLL, self.OnSlider)
    if self.button.display:
      value = float(value) / self.button.slider_max
      self.SetTitle("%6.3f" % value)
    self.Show()
    self.slider.SetFocus()
  def OnSlider(self, event):
    value = self.slider.GetValue()
    if self.button.display:
      v = float(value) / self.button.slider_max
      self.SetTitle("%6.3f" % v)
    self.button.ChangeSlider(value)
  def OnClose(self, event):
    self.button.adjust = None
    self.Destroy()

# Dual slider widget for bias
class QDualSliderButtonWindow(wx.Frame):	# Thanks to Steve, KF7O
  """Create a window with controls for the button"""
  def __init__(self, button):
    self.button = button
    x, y = button.GetPosition().Get()
    x, y = button.GetParent().ClientToScreen(wx.Point(x, y))
    w, h = button.GetSize()
    w = w * 12 // 10
    height = h * 10
    size = (w, height)
    if sys.platform == 'win32':
      pos = (x, y - height)
    else:
      pos = (x, y - height - h)
    wx.Frame.__init__(self, button.GetParent(), -1, '', pos, size,
      wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU)
    self.SetBackgroundColour(conf.color_freq)
    self.Bind(wx.EVT_CLOSE, self.OnClose)
    panel = wx.Panel(self, -1)
    panel.SetBackgroundColour(conf.color_freq)
    hbox = wx.BoxSizer(wx.HORIZONTAL)
    self.lslider = wx.Slider(panel, -1, self.button.lslider_value,
             self.button.slider_min, self.button.slider_max,
             (0, 0), (w//2, height), wx.SL_VERTICAL|wx.SL_INVERSE)
    self.lslider.Bind(wx.EVT_SCROLL, self.OnSlider)
    hbox.Add(self.lslider, flag=wx.LEFT)
    self.rslider = wx.Slider(panel, -1, self.button.rslider_value,
             self.button.slider_min, self.button.slider_max,
             (0, 0), (w//2, height), wx.SL_VERTICAL|wx.SL_INVERSE)
    self.rslider.Bind(wx.EVT_SCROLL, self.OnSlider)
    hbox.Add(self.rslider, flag=wx.RIGHT)
    panel.SetSizer(hbox)
    if self.button.display:
      self.SetTitle("%3d   %3d" % (self.button.lslider_value,self.button.rslider_value))
    self.Show()
    self.lslider.SetFocus()
  def OnSlider(self, event):
    lvalue = self.lslider.GetValue()
    rvalue = self.rslider.GetValue()
    self.button.ChangeSlider(lvalue,rvalue)
    if self.button.display:
      self.SetTitle("%3d    %3d" % (self.button.lslider_value,self.button.rslider_value))
  def OnClose(self, event):
    self.button.adjust = None
    self.Destroy()

class WrapControl(wx.BoxSizer):
  def __init__(self):
    wx.BoxSizer.__init__(self, wx.HORIZONTAL)
  def Enable(self, value=True):
    self.button.Enable(value)
  def SetLabel(self, text=None):
    if text is not None:
      self.button.SetLabel(text)
  def GetParent(self):
    return self.button.GetParent()
  def GetValue(self):
    return self.button.GetValue()
  def SetValue(self, value, do_cmd=False):
    self.button.SetValue(value, do_cmd)
  def SetIndex(self, index, do_cmd=False):
    self.button.SetIndex(index, do_cmd)
  def GetLabel(self):
    return self.button.GetLabel()
  def __getattr__(self, name):
    return getattr(self.button, name)

class WrapPushButton(WrapControl):
  def __init__(self, button, control):
    self.button = button
    WrapControl.__init__(self)
    self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL)
    b = QuiskPushbutton(button.GetParent(), control, conf.btn_text_switch)
    self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2)

class WrapIndicator(WrapControl):
  def __init__(self, button, text, text_color, on_color):
    self.button = button
    self.text = text
    self.on_brush = wx.Brush(on_color)
    WrapControl.__init__(self)
    self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL)
    self.light = QuiskPushbutton(button.GetParent(), None, "", text_color=text_color)
    self.Add(self.light, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2)
  def TurnOn(self, on):
    if on:
      self.light.SetLabel(self.text)
      self.light.up_brush = self.on_brush
    else:
      self.light.SetLabel("")
      self.light.up_brush = self.button.up_brush
    self.light.Refresh()

class WrapMenu(WrapControl):
  def __init__(self, button, menu, on_open=None):
    self.button = button
    self.menu = menu
    self.on_open = on_open
    WrapControl.__init__(self)
    self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL)
    b = QuiskBitmapButton(button.GetParent(), self.OnPopButton, _bitmap_menupop)
    self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2)
    self.pop_button = b
  def OnPopButton(self, event):
    if self.on_open:
      self.on_open(self.menu)
    pos = (5, 5)
    self.pop_button.PopupMenu(self.menu, pos)

class WrapSlider(WrapControl):
  def __init__(self, button, command, slider_value=0, slider_min=0, slider_max=1000, display=False, wintype=''):
    self.adjust = None
    self.dual = False						# dual means separate slider values for on and off
    self.button = button
    self.main_command = button.command
    button.command = self.OnMainButton
    self.command = command
    button.slider_value = slider_value		# value for not dual
    button.slider_value_off = slider_value	# value for dual and button up
    button.slider_value_on = slider_value		# value for dual and button down
    self.slider_min = slider_min
    self.slider_max = slider_max
    self.display = display					# Display the value at the top
    self.wintype = wintype
    WrapControl.__init__(self)
    self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL)
    b = QuiskBitmapButton(button.GetParent(), self.OnPopButton, _bitmap_sliderpop)
    self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2)
  def SetDual(self, dual):		# dual means separate slider values for on and off
    self.dual = dual
    if self.adjust:
      self.adjust.Destroy()
      self.adjust = None
  def DeleteSliderWindow(self):
    if self.adjust:
      self.adjust.Destroy()
      self.adjust = None
  def OnPopButton(self, event):
    if self.adjust:
      self.adjust.Destroy()
      self.adjust = None
    else:
      if not self.dual:
        value = self.button.slider_value
      elif self.button.GetValue():
        value = self.button.slider_value_on
      else:
        value = self.button.slider_value_off
      if self.wintype == 'filter':
        self.adjust = QFilterButtonWindow(self, value)
      else:
        self.adjust = QSliderButtonWindow(self, value)
  def OnMainButton(self, event):
    if self.adjust:
      self.adjust.Destroy()
      self.adjust = None
    if self.main_command:
      self.main_command(event)
  def ChangeSlider(self, value):
    if not self.dual:
      self.button.slider_value = value
    elif self.button.GetValue():
      self.button.slider_value_on = value
    else:
      self.button.slider_value_off = value
    if self.wintype == 'filter':
      self.button.SetLabel(str(value))
      self.button.Refresh()
    if self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName}Slider;{value}\n')
      event = wx.PyEvent()
      event.SetEventObject(self.button)
      self.command(event)
  def SetSlider(self, value=None, value_off=None, value_on=None):
    if value is not None:
      self.button.slider_value = value
    if value_off is not None:
      self.button.slider_value_off = value_off
    if value_on is not None:
      self.button.slider_value_on = value_on

class WrapDualSlider(WrapControl):	# Thanks to Steve, KF7O
  def __init__(self, button, command, lslider_value=0, rslider_value=0, slider_min=0, slider_max=1000, display=0):
    self.adjust = None
    self.button = button
    self.main_command = button.command
    button.command = self.OnMainButton
    self.command = command
    button.lslider_value = lslider_value
    button.rslider_value = rslider_value     
    self.slider_min = slider_min
    self.slider_max = slider_max
    self.display = display                  # Display the value at the top
    WrapControl.__init__(self)
    self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL)
    ## This is a hack to get _bitmap_sliderpop
    ## It would be better if _bitmap_sliderpop were not a global variable 
    ##but a first-class member in another module
    _bitmap_sliderpop = MakeWidgetGlobals.__globals__['_bitmap_sliderpop']
    b = QuiskBitmapButton(button.GetParent(), self.OnPopButton, _bitmap_sliderpop)
    self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2)
  def OnPopButton(self, event):
    if self.adjust:
      self.adjust.Destroy()
      self.adjust = None
    else:
      self.adjust = QDualSliderButtonWindow(self)
  def OnMainButton(self, event):
    if self.adjust:
      self.adjust.Destroy()
      self.adjust = None
    if self.main_command:
      self.main_command(event)
  def ChangeSlider(self, lvalue, rvalue):
    self.button.lslider_value = lvalue
    self.button.rslider_value = rvalue
    if self.command:
      event = wx.PyEvent()
      event.SetEventObject(self.button)
      self.command(event)

class QuiskCycleCheckbutton(QuiskCheckbutton):
  """A button that cycles through its labels with each push.

  The button is up for labels[0], down for all other labels.  Change to the
  next label for each push.  If you call SetLabel(), the label must be in the list.
  The self.index is the index of the current label.
  """
  def __init__(self, parent, command, labels, color=None, is_radio=False):
    self.labels = list(labels)		# Be careful if you change this list
    self.index = 0		# index of selected label 0, 1, ...
    self.direction = 0	# 1 for up, -1 for down, 0 for no change to index
    self.is_radio = is_radio	# Is this a radio cycle button?
    if color is None:
      color = conf.color_cycle_btn
    QuiskCheckbutton.__init__(self, parent, command, labels[0], color)
    self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
    self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDclick)
  def SetLabel(self, label, do_cmd=False):
    self.index = self.labels.index(label)
    QuiskCheckbutton.SetLabel(self, label)
    QuiskCheckbutton.SetValue(self, self.index)
    if do_cmd and self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
      event = wx.PyEvent()
      event.SetEventObject(self)
      self.command(event)
  def SetIndex(self, index, do_cmd=False):
    self.index = index
    QuiskCheckbutton.SetLabel(self, self.labels[index])
    QuiskCheckbutton.SetValue(self, index)
    if do_cmd and self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
      event = wx.PyEvent()
      event.SetEventObject(self)
      self.command(event)
  def GetIndex(self):
    return self.index
  def OnButton(self, event):
    if not self.is_radio or self.button_down:
      self.direction = 1
      self.index += 1
      if self.index >= len(self.labels):
        self.index = 0
      self.SetIndex(self.index)
    else:
      self.direction = 0
    if self.command:
      if application.remote_control_head:
        application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
      self.command(event)
  def OnRightDown(self, event):		# Move left in the list of labels
    if not self.is_radio or self.GetValue():
      self.index -= 1
      if self.index < 0:
        self.index = len(self.labels) - 1
      self.SetIndex(self.index)
      self.direction = -1
      if self.command:
        if application.remote_control_head:
          application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
        self.command(event)
  def OnLeftDclick(self, event):	# Left double-click: Set index zero
    if not self.is_radio or self.GetValue():
      self.index = 0
      self.SetIndex(self.index)
      self.direction = 1
      if self.command:
        if application.remote_control_head:
          application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n')
        self.command(event)
  def DrawLabel(self, dc, width, height, dx=0, dy=0):
    QuiskCheckbutton.DrawLabel(self, dc, width, height, dx, dy)
    self.DrawGlyphCycle(dc, width, height)
  def Shortcut(self, event, button_name=None):
    if self.IsEnabled():
      if not self.is_radio or self.button_down:
        self.direction = 1
        index = self.index + 1
        if index >= len(self.labels):
          index = 0
        self.SetIndex(index, True)
      else:
        self.SetIndex(self.index, True)

class RadioButtonGroup:
  """This class encapsulates a group of radio buttons.  This class is not a button!

  The "labels" is a list of labels for the toggle buttons.  An item
  of labels can be a list/tuple, and the corresponding button will
  be a cycle button.
  """
  def __init__(self, parent, command, labels, default, shortcuts=()):
    self.command = command
    self.buttons = []	# This list of buttons can also contain WrapControls
    self.button = None
    self.shortcuts = list(shortcuts[:])
    self.last_shortcut = 0
    i = 0
    for text in labels:
      if isinstance(text, (list, tuple)):
        b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True)
        if shortcuts:
          b.char_shortcut = shortcuts[i]
        for t in text:
          if t == default and self.button is None:
            b.SetLabel(t)
            self.button = b
      else:
        b = QuiskCheckbutton(parent, self.OnButton, text)
        if shortcuts:
          b.char_shortcut = shortcuts[i]
        if text == default and self.button is None:
          b.SetValue(True)
          self.button = b
      self.buttons.append(b)
      i += 1
  def ReplaceButton(self, index, button):	# introduce a specialized button
    b = self.buttons[index]
    b.Destroy()
    self.buttons[index] = button
    if isinstance(button, WrapSlider):
      button.main_command = self.OnButton
    elif isinstance(button, WrapMenu):
      button.button.command = self.OnButton
    else:
      button.command = self.OnButton
  def SetLabel(self, label, do_cmd=False, direction=None):
    if application.remote_control_head:
      func = "%s.SetLabel" % self.idName
      application.Hardware.RemoteCtlSend(f'JsonAppFunc;{json.dumps((func, label, do_cmd, direction))}\n')
    self.button = None
    for b in self.buttons:
      if self.button is not None:
        b.SetValue(False)
      elif isinstance(b, QuiskCycleCheckbutton) or (isinstance(b, WrapControl) and isinstance(b.button, QuiskCycleCheckbutton)):
        try:
          index = b.labels.index(label)
        except ValueError:
          b.SetValue(False)
          continue
        else:
          b.SetIndex(index)
          self.button = b
          b.SetValue(True)
          if direction is not None:
            if isinstance(b, WrapControl):
              b.button.direction = direction
            else:
              b.direction = direction
      elif b.GetLabel() == label:
        b.SetValue(True)
        self.button = b
      else:
        b.SetValue(False)
    if do_cmd and self.command and self.button:
      event = wx.PyEvent()
      event.SetEventObject(self.button)
      self.command(event)
  def GetButtons(self):
    return self.buttons
  def OnButton(self, event):
    win = event.GetEventObject()
    for b in self.buttons:
      if b is win or (isinstance(b, WrapControl) and b.button is win):
        self.button = b
        b.SetValue(True)
      else:
        b.SetValue(False)
    if self.command:
      self.command(event)
  def GetLabel(self):
    if not self.button:
      return None
    return self.button.GetLabel()
  def GetSelectedButton(self):		# return the selected button
    return self.button
  def GetIndex(self):	# Careful.  Some buttons are WrapControls.
    if not self.button:
      return None
    return self.buttons.index(self.button)
  def Shortcut(self, event):	# Not used for Midi
    # Multiple buttons can have the same shortcut, so move to the next one.
    index = self.last_shortcut + 1
    length = len(self.shortcuts)
    if index >= length:
      index = 0
    for i in range(length):
      shortcut = self.shortcuts[index]
      if shortcut and wx.GetKeyState(ord(shortcut)):
        break
      index += 1
      if index >= length:
        index = 0
    else:
      return
    self.last_shortcut = index
    button = self.buttons[index]
    event = wx.PyEvent()
    event.SetEventObject(button)
    button.OnButton(event)

class _PopWindow(wx.PopupWindow):
  def __init__(self, parent, command, labels, default):
    wx.PopupWindow.__init__(self, parent)
    self.panel = wx.Panel(self)
    self.panel.SetBackgroundColour(conf.color_popchoice)
    self.RbGroup = RadioButtonGroup(self.panel, command, labels, default)
    x = 5
    y = 5
    for b in self.RbGroup.buttons:
      b.SetPosition((x, y))
      w, h = b.GetTextExtent(b.GetLabel())
      width = w + 2 + 2 * b.bezelWidth + 4 * int(b.useFocusInd)
      height = h + 2 + 2 * b.bezelWidth + 4 * int(b.useFocusInd)
      b.SetInitialSize((width, height))
      x += width + 5
    self.SetSize((x, height + 2 * y))
    self.panel.SetSize((x, height + 2 * y))

class RadioBtnPopup:
  """This class contains a button that pops up a row of radio buttons"""
  def __init__(self, parent, command, in_labels, default):
    self.parent = parent
    self.pop_command = command
    self.button_data = {}
    labels = []
    for item in in_labels:
      if isinstance(item, (list, tuple)):
        labels.append(item[0])
        self.button_data[item[0]] = [_bitmap_cyclepop, 0, len(item)]	# bitmap, index, max_index
      else:
        labels.append(item)
    self.RbDialog = _PopWindow(parent, self.OnGroupButton, labels, default)
    self.RbDialog.Hide()
    self.pop_control = wx.BoxSizer(wx.HORIZONTAL)
    self.first_button = QuiskPushbutton(parent, self.OnFirstButton, labels[0], text_color=conf.color_popchoice)
    self.first_button.decoration = u'\u21D2'
    self.second_button = QuiskBitmapButton(parent, self.OnSecondButton, _bitmap_menupop, use_right=True)
    self.pop_control.Add(self.first_button, 1, flag=wx.ALIGN_CENTER_VERTICAL)
    self.pop_control.Add(self.second_button, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2)
    self.pop_control.Show(self.second_button, False)
    self.adjust = None
    self.first_button.index = 0
  def GetPopControl(self):
    return self.pop_control
  def AddMenu(self, label, menu):
    self.button_data[label] = (_bitmap_menupop, menu)
    return self
  def AddSlider(self, label, command, slider_value=0, slider_min=0, slider_max=1000, display=False, wintype=''):
    self.button_data[label] = [_bitmap_sliderpop, command, slider_value, slider_min, slider_max, display, wintype]
  def OnFirstButton(self, event):
    if self.adjust:		# Destroy any slider window
      self.adjust.Destroy()
      self.adjust = None
    if self.RbDialog.IsShown():
      self.RbDialog.Hide()
      return
    dw, dh = self.RbDialog.GetSize().Get()
    bw, bh = self.first_button.GetSize().Get()
    bx, by = self.first_button.ClientToScreen(wx.Point(0, 0))
    self.RbDialog.Position(wx.Point(bx + bw * 8 // 10, by + bh // 2 - dh), wx.Size(1, 1))
    self.RbDialog.Show()
    self.RbDialog.SetFocus()
  def AddSecondButton(self, label):
    data = self.button_data.get(label, None)
    if data is None:
      self.pop_control.Show(self.second_button, False)
    else:
      self.second_button.SetBitmapLabel(data[0])
      self.pop_control.Show(self.second_button, True)
      if data[0] == _bitmap_cyclepop:
        self.first_button.index = data[1]
    self.pop_control.Layout()
  def OnSecondButton(self, event):
    label = self.first_button.GetLabel()
    data = self.button_data.get(label, None)
    if data is None:
      pass
    elif data[0] == _bitmap_menupop:
      self.first_button.PopupMenu(data[1], (5, 5))
    elif data[0] == _bitmap_sliderpop:
      if self.adjust:
        self.adjust.Destroy()
        self.adjust = None
      else:
        bitm, self.command, slider_value, self.slider_min, self.slider_max, self.display, self.wintype = data
        self.adjust = QSliderButtonWindow(self, slider_value)
        self.second_data = data
    elif data[0] == _bitmap_cyclepop:
      if self.second_button.direction >= 0:
        data[1] += 1
        if data[1] >= data[2]:
          data[1] = 0
      else:
        data[1] -= 1
        if data[1] < 0:
          data[1] = data[2] - 1
      self.first_button.index = data[1]
      self.first_button.direction = self.second_button.direction
      if self.pop_command:
        event = wx.PyEvent()
        event.SetEventObject(self.first_button)
        self.pop_command(event)
      self.first_button.direction = 1
  def OnGroupButton(self, event):
    btn = event.GetEventObject()
    label = btn.GetLabel()
    self.first_button.SetLabel(label)
    self.first_button.Refresh()
    self.RbDialog.Hide()
    self.AddSecondButton(label)
    if self.pop_command:
      event = wx.PyEvent()
      event.SetEventObject(self.first_button)
      self.pop_command(event)
  def Enable(self, label, enable):
    for b in self.RbDialog.RbGroup.buttons:
      if b.GetLabel() == label:
        b.Enable(enable)
        break
  def GetLabel(self):
    return self.first_button.GetLabel()
  def SetLabel(self, label, do_cmd=False, direction=None):
    direc =  self.first_button.direction
    if direction is not None:
      self.first_button.direction = direction
    self.first_button.SetLabel(label)
    self.AddSecondButton(label)
    self.RbDialog.RbGroup.SetLabel(label, False)
    if do_cmd and self.pop_command:
      event = wx.PyEvent()
      event.SetEventObject(self.first_button)
      self.pop_command(event)
    self.first_button.direction = direc
  def GetButtons(self):
    return self.RbDialog.RbGroup.GetButtons()
  def Refresh(self):
    pass
  def ChangeSlider(self, slider_value):
    self.second_data[2] = slider_value
    command = self.second_data[1]
    if command:
      event = wx.PyEvent()
      self.first_button.slider_value = slider_value
      event.SetEventObject(self.first_button)
      command(event)
  def GetPositionTuple(self):
    return self.first_button.GetPosition().Get()
  def GetParent(self):
    return self.parent
  def GetSize(self):
    return self.first_button.GetSize()
  def Shortcut(self, event, button_name=None):
    self.SetLabel(button_name, True)

class FreqSetter(wx.TextCtrl):
  def __init__(self, parent, x, y, label, fmin, fmax, freq, command):
    self.pos = (x, y)
    self.label = label
    self.fmin = fmin
    self.fmax = fmax
    self.command = command
    self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
    t = wx.StaticText(parent, -1, label, pos=(x, y))
    t.SetFont(self.font)
    freq_w, freq_h = t.GetTextExtent(" 662 000 000")
    tw, th = t.GetSize().Get()
    x += tw + 20
    wx.TextCtrl.__init__(self, parent, size=(freq_w, freq_h), pos=(x, y),
      style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER)
    self.SetFont(self.font)
    self.Bind(wx.EVT_TEXT, self.OnText)
    self.Bind(wx.EVT_TEXT_ENTER, self.OnEnter)
    w, h = self.GetSize().Get()
    x += w + 1
    self.butn = b = wx.SpinButton(parent, size=(freq_h, freq_h), pos=(x, y))
    w, h = b.GetSize().Get()
    self.end_pos = (x + w, y + h)
    b.Bind(wx.EVT_SPIN, self.OnSpin)	# The spin button frequencies are in kHz
    b.SetMin(fmin // 1000)
    b.SetMax(fmax // 1000)
    self.SetValue(freq)
  def OnText(self, event):
    self.SetBackgroundColour('pink')
  def OnEnter(self, event):
    text = wx.TextCtrl.GetValue(self)
    text = text.replace(' ', '')
    if '-' in text:
      return
    try:
      if '.' in text:
        freq = int(float(text) * 1000000 + 0.5)
      else:
        freq = int(text)
    except:
      return
    self.SetValue(freq)
    self.command(self)
  def OnSpin(self, event):
    freq = self.butn.GetValue() * 1000
    self.SetValue(freq)
    self.command(self)
  def SetValue(self, freq):
    if freq < self.fmin:
      freq = self.fmin
    elif freq > self.fmax:
      freq = self.fmax
    self.butn.SetValue(freq // 1000)
    txt = FreqFormatter(freq)
    wx.TextCtrl.SetValue(self, txt)
    self.SetBackgroundColour(conf.color_entry)
  def GetValue(self):
    value = wx.TextCtrl.GetValue(self)
    value = value.replace(' ', '')
    try:
      value = int(value)
    except:
      value = 7000
    return value

class QuiskMenu(wx.Menu):
  def __init__(self, menu_name):
    wx.Menu.__init__(self)
    self.menu_name = menu_name
    self.id2data = {}
    self.item_text2id = {}
  def InitData(self, item, handler):
    self.Bind(wx.EVT_MENU, self.Handler, item)
    item_text = item.GetItemLabelText()
    self.id2data[item.GetId()] = (item_text, handler)
    self.item_text2id[item_text] = item.GetId()
  def Append(self, item_text, handler):
    item = wx.Menu.Append(self, -1, item_text)
    self.InitData(item, handler)
    return item
  def AppendCheckItem(self, item_text, handler, is_checked=0):
    item = wx.Menu.AppendCheckItem(self, -1, item_text)
    item.Check(is_checked)
    self.InitData(item, handler)
    return item
  def AppendRadioItem(self, item_text, handler, is_checked=0):
    item = wx.Menu.AppendRadioItem(self, -1, item_text)
    item.Check(is_checked)
    self.InitData(item, handler)
    return item
  def IsItemChecked(self, item_text):
    nid = self.item_text2id[item_text]
    menu_item = self.FindItemById(nid)
    return menu_item.IsChecked()
  def Handler(self, event=None, nid=0):
    if event:
      nid = event.GetId()
    item_text, handler = self.id2data[nid]
    if application.remote_control_head:
      menu_item = self.FindItemById(nid)
      if menu_item.IsCheckable():
        checked = menu_item.IsChecked()
      else:
        checked = 0
      application.Hardware.RemoteCtlSend('MENU;%s;%s;%d\n' % (self.menu_name, item_text, int(checked)))
    handler(event)
