text: # -*- coding: UTF-8 -*-
  
  
  import sys
  
  try:
      import wx
  except ImportError as error:
      print(error)
      sys.exit(1)
  
  
  def crt_command_event(event_type, event_id=0):
      """Shortcut to create command events."""
      return wx.CommandEvent(event_type.typeId, event_id)
  
  
  class ListBoxWithHeaders(wx.ListBox):
      # noinspection PyUnresolvedReferences
      """Custom ListBox object that supports 'headers'.
  
          Attributes:
              NAME (string): Default name for the name argument of the __init__.
  
              TEXT_PREFIX (string): Text to add before normal items in order to
                  distinguish them (normal items) from headers.
  
              EVENTS (list): List with events to overwrite to avoid header selection.
  
          """
  
      NAME = "listBoxWithHeaders"
  
      TEXT_PREFIX = "    "
  
      EVENTS = [
          wx.EVT_LEFT_DOWN,
          wx.EVT_LEFT_DCLICK,
          wx.EVT_RIGHT_DOWN,
          wx.EVT_RIGHT_DCLICK,
          wx.EVT_MIDDLE_DOWN,
          wx.EVT_MIDDLE_DCLICK
      ]
  
      def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
                   size=wx.DefaultSize, choices=None, style=0, validator=wx.DefaultValidator, name=NAME):
          super(ListBoxWithHeaders, self).__init__(parent, id, pos, size, [], style, validator, name)
          if choices is None:
              choices = []
          self.__headers = set()
  
          # Ignore all key events i'm bored to handle the header selection
          self.Bind(wx.EVT_KEY_DOWN, lambda event: None)
  
          # Make sure that a header is never selected
          self.Bind(wx.EVT_LISTBOX, self._on_listbox)
          for _event in self.EVENTS:
              self.Bind(_event, self._disable_header_selection)
  
          # Append the items in our own way in order to add the TEXT_PREFIX
          self.AppendItems(choices)
  
      def _disable_header_selection(self, event):
          """Stop event propagation if the selected item is a header."""
          row = self.HitTest(event.GetPosition())
          event_skip = True
  
          if row != wx.NOT_FOUND and self.GetString(row) in self.__headers:
              event_skip = False
  
          event.Skip(event_skip)
  
      def _on_listbox(self, event):
          """Make sure no header is selected."""
          if event.GetString() in self.__headers:
              self.Deselect(event.GetSelection())
          event.Skip()
  
      def _add_prefix(self, string):
          return self.TEXT_PREFIX + string
  
      def _remove_prefix(self, string):
          if string[:len(self.TEXT_PREFIX)] == self.TEXT_PREFIX:
              return string[len(self.TEXT_PREFIX):]
          return string
  
      # wx.ListBox methods
  
      def FindString(self, string, **kwargs):
          index = super(ListBoxWithHeaders, self).FindString(string, **kwargs)
  
          if index == wx.NOT_FOUND:
              # This time try with prefix
              index = super(ListBoxWithHeaders, self).FindString(self._add_prefix(string), **kwargs)
  
          return index
  
      def GetStringSelection(self):
          return self._remove_prefix(super(ListBoxWithHeaders, self).GetStringSelection())
  
      def GetString(self, index):
          if index < 0 or index >= self.GetCount():
              # Return empty string based on the wx.ListBox docs
              # for some reason parent GetString does not handle
              # invalid indices
              return ""
  
          return self._remove_prefix(super(ListBoxWithHeaders, self).GetString(index))
  
      def InsertItems(self, items, pos):
          items = [self._add_prefix(item) for item in items]
          super(ListBoxWithHeaders, self).InsertItems(items, pos)
  
      def SetSelection(self, index):
          if index == wx.NOT_FOUND:
              self.Deselect(self.GetSelection())
          elif self.GetString(index) not in self.__headers:
              super(ListBoxWithHeaders, self).SetSelection(index)
  
      def SetString(self, index, string):
          old_string = self.GetString(index)
  
          if old_string in self.__headers and string != old_string:
              self.__headers.remove(old_string)
              self.__headers.add(string)
  
          super(ListBoxWithHeaders, self).SetString(index, string)
  
      def SetStringSelection(self, string):
          if string in self.__headers:
              return False
  
          self.SetSelection(self.FindString(string, ))
          return True
  
      # wx.ItemContainer methods
  
      def AppendItems(self, strings):
          strings = [self._add_prefix(string) for string in strings]
          super(ListBoxWithHeaders, self).AppendItems(strings)
  
      def Clear(self):
          self.__headers.clear()
          super(ListBoxWithHeaders, self).Clear()
  
      def Delete(self, index):
          string = self.GetString(index)
  
          if string in self.__headers:
              self.__headers.remove(string)
  
          super(ListBoxWithHeaders, self).Delete(index)
  
      # Extra methods
  
      def add_header(self, header_string):
          self.__headers.add(header_string)
          super(ListBoxWithHeaders, self).Append(header_string)
  
      def add_item(self, item, with_prefix=True):
          if with_prefix:
              item = self._add_prefix(item)
  
          super(ListBoxWithHeaders, self).Append(item)
  
      def add_items(self, items, with_prefix=True):
          if with_prefix:
              items = [self._add_prefix(item) for item in items]
  
          super(ListBoxWithHeaders, self).AppendItems(items)
  
  
  # noinspection PyPep8Naming
  class ListBoxPopup(wx.PopupTransientWindow):
      # noinspection PyUnresolvedReferences
      """ListBoxWithHeaders as a popup.
  
          This class uses the wx.PopupTransientWindow to create the popup and the
          API is based on the wx.combo.ComboPopup class.
  
          Attributes:
              EVENTS_TABLE (dict): Dictionary that contains all the events
                  that this class emits.
  
          """
  
      EVENTS_TABLE = {
          "EVT_COMBOBOX": crt_command_event(wx.EVT_COMBOBOX),
          "EVT_COMBOBOX_DROPDOWN": crt_command_event(wx.EVT_COMBOBOX_DROPDOWN),
          "EVT_COMBOBOX_CLOSEUP": crt_command_event(wx.EVT_COMBOBOX_CLOSEUP)
      }
  
      def __init__(self, parent=None, flags=wx.BORDER_NONE):
          super(ListBoxPopup, self).__init__(parent, flags)
          self.__listbox = None
  
      def _on_motion(self, event):
          row = self.__listbox.HitTest(event.GetPosition())
  
          if row != wx.NOT_FOUND:
              self.__listbox.SetSelection(row)
  
              if self.__listbox.IsSelected(row):
                  self.curitem = row
  
      # noinspection PyUnusedLocal
      def _on_left_down(self, event):
          self.value = self.curitem
          self.Dismiss()
  
          # Send EVT_COMBOBOX to inform the parent for changes
          wx.PostEvent(self, self.EVENTS_TABLE["EVT_COMBOBOX"])
  
      def Popup(self, **kwargs):
          super(ListBoxPopup, self).Popup(**kwargs)
          wx.PostEvent(self, self.EVENTS_TABLE["EVT_COMBOBOX_DROPDOWN"])
  
      def OnDismiss(self):
          wx.PostEvent(self, self.EVENTS_TABLE["EVT_COMBOBOX_CLOSEUP"])
  
      # wx.combo.ComboPopup methods
  
      # noinspection PyAttributeOutsideInit
      def Init(self):
          self.value = self.curitem = -1
  
      def Create(self, parent, **kwargs):
          self.__listbox = ListBoxWithHeaders(parent, style=wx.LB_SINGLE)
  
          self.__listbox.Bind(wx.EVT_MOTION, self._on_motion)
          self.__listbox.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
  
          sizer = wx.BoxSizer()
          sizer.Add(self.__listbox, 1, wx.EXPAND)
          self.SetSizer(sizer)
          return True
  
      def GetAdjustedSize(self, min_width, pref_height, max_height):
          width, height = self.GetBestSize()
  
          if width < min_width:
              width = min_width
  
          if pref_height != -1:
              height = pref_height * self.__listbox.GetCount() + 5
  
          if height > max_height:
              height = max_height
  
          return wx.Size(width, height)
  
      def GetControl(self):
          return self.__listbox
  
      def GetStringValue(self):
          return self.__listbox.GetString(self.value)
  
      # def SetStringValue(self, string):
      # self.__listbox.SetStringSelection(string)
  
  
  # noinspection PyPep8Naming
  class CustomComboBox(wx.Panel):
      # noinspection PyUnresolvedReferences
      """Custom combobox.
  
          Attributes:
              CB_READONLY (long): Read-only style. The only one supported from the
                  wx.ComboBox styles.
  
              NAME (string): Default name for the name argument of the __init__.
  
          """
      # NOTE wx.ComboBox does not support EVT_MOTION inside the popup
      # NOTE Tried with ComboCtrl but i was not able to draw the button
  
      CB_READONLY = wx.TE_READONLY
  
      NAME = "customComboBox"
  
      def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition,
                   size=wx.DefaultSize, choices=None, style=0, validator=wx.DefaultValidator, name=NAME):
          super(CustomComboBox, self).__init__(parent, id, pos, size, 0, name)
  
          if choices is None:
              choices = []
          assert style == self.CB_READONLY or style == 0
  
          # Create components
          self.textctrl = wx.TextCtrl(self, wx.ID_ANY, style=style, validator=validator)
          tc_height = self.textctrl.GetSize()[1]
  
          self.button = wx.Button(self, wx.ID_ANY, "▾", size=(tc_height, tc_height))
  
          # Create the ListBoxPopup in two steps
          self.listbox = ListBoxPopup(self)
          self.listbox.Init()
          self.listbox.Create(self.listbox, )
  
          # Set layout
          sizer = wx.BoxSizer()
          sizer.Add(self.textctrl, 1, wx.ALIGN_CENTER_VERTICAL)
          sizer.Add(self.button)
          self.SetSizer(sizer)
  
          # Bind events
          self.button.Bind(wx.EVT_BUTTON, self._on_button)
  
          for event in ListBoxPopup.EVENTS_TABLE.values():
              self.listbox.Bind(wx.PyEventBinder(event.GetEventType()), self._propagate)
  
          # Append items since the ListBoxPopup does not have the 'choices' arg
          self.listbox.GetControl().AppendItems(choices)
          self.SetStringSelection(value)
  
      def _propagate(self, event):
          if event.GetEventType() == wx.EVT_COMBOBOX.typeId:
              self.textctrl.SetValue(self.listbox.GetStringValue())
  
          wx.PostEvent(self, event)
  
      # noinspection PyUnusedLocal
      def _on_button(self, event):
          self.Popup()
  
      def _calc_popup_position(self):
          tc_x_axis, tc_y_axis = self.textctrl.ClientToScreen((0, 0))
          _, tc_height = self.textctrl.GetSize()
  
          return tc_x_axis, tc_y_axis + tc_height
  
      def _calc_popup_size(self):
          me_width, _ = self.GetSize()
          _, tc_height = self.textctrl.GetSize()
          _, screen_height = wx.DisplaySize()
  
          _, me_y_axis = self.GetScreenPosition()
  
          available_height = screen_height - (me_y_axis + tc_height)
          sug_width, sug_height = self.listbox.GetAdjustedSize(me_width, tc_height, available_height)
  
          return me_width, sug_height
  
      # wx.ComboBox methods
  
      def Dismiss(self):
          self.listbox.Dismiss()
  
      # noinspection PyUnusedLocal
      def FindString(self, string, caseSensitive=False):
          # TODO handle caseSensitive
          return self.listbox.GetControl().FindString(string, )
  
      def GetCount(self):
          return self.listbox.GetControl().GetCount()
  
      def GetCurrentSelection(self):
          return self.GetSelection()
  
      def GetInsertionPoint(self):
          return self.textctrl.GetInsertionPoint()
  
      def GetSelection(self):
          return self.listbox.value
  
      def GetTextSelection(self):
          return self.textctrl.GetSelection()
  
      def GetString(self, index):
          return self.listbox.GetControl().GetString(index)
  
      def GetStringSelection(self):
          return self.listbox.GetStringValue()
  
      def IsListEmpty(self):
          return self.listbox.GetControl().GetCount() == 0
  
      def IsTextEmpty(self):
          return not self.textctrl.GetValue()
  
      def Popup(self):
          self.listbox.SetPosition(self._calc_popup_position())
          self.listbox.SetSize(self._calc_popup_size())
  
          self.listbox.Popup()
  
      def SetSelection(self, index):
          self.listbox.GetControl().SetSelection(index)
          if self.listbox.GetControl().IsSelected(index):
              self.listbox.value = index
              self.textctrl.SetValue(self.listbox.GetStringValue())
  
      def SetString(self, index, string):
          self.listbox.GetControl().SetString(index, string)
  
      def SetTextSelection(self, from_, to_):
          self.textctrl.SetSelection(from_, to_)
  
      def SetStringSelection(self, string):
          index = self.listbox.GetControl().FindString(string, )
          self.listbox.GetControl().SetSelection(index)
  
          if index != wx.NOT_FOUND and self.listbox.GetControl().GetSelection() == index:
              self.listbox.value = index
              self.textctrl.SetValue(string)
  
      def SetValue(self, value):
          self.textctrl.SetValue(value)
  
      # wx.ItemContainer methods
  
      def Clear(self):
          self.textctrl.Clear()
          self.listbox.GetControl().Clear()
  
      def Append(self, item):
          self.listbox.GetControl().Append(item)
  
      def AppendItems(self, items):
          self.listbox.GetControl().AppendItems(items)
  
      def Delete(self, index):
          self.listbox.GetControl().Delete(index)
  
      # wx.TextEntry methods
  
      def GetValue(self):
          return self.textctrl.GetValue()
  
      # ListBoxWithHeaders methods
  
      def add_header(self, header):
          self.listbox.GetControl().add_header(header)
  
      def add_item(self, item, with_prefix=True):
          self.listbox.GetControl().add_item(item, with_prefix)
  
      def add_items(self, items, with_prefix=True):
          self.listbox.GetControl().add_items(items, with_prefix)
  