You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

434 lines
13 KiB

  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. from __future__ import unicode_literals
  4. import sys
  5. try:
  6. import wx
  7. except ImportError as error:
  8. print error
  9. sys.exit(1)
  10. def crt_command_event(event_type, event_id=0):
  11. """Shortcut to create command events."""
  12. return wx.CommandEvent(event_type.typeId, event_id)
  13. class ListBoxWithHeaders(wx.ListBox):
  14. """Custom ListBox object that supports 'headers'.
  15. Attributes:
  16. NAME (string): Default name for the name argument of the __init__.
  17. TEXT_PREFIX (string): Text to add before normal items in order to
  18. distinguish them (normal items) from headers.
  19. EVENTS (list): List with events to overwrite to avoid header selection.
  20. """
  21. NAME = "listBoxWithHeaders"
  22. TEXT_PREFIX = " "
  23. EVENTS = [
  24. wx.EVT_LEFT_DOWN,
  25. wx.EVT_LEFT_DCLICK,
  26. wx.EVT_RIGHT_DOWN,
  27. wx.EVT_RIGHT_DCLICK,
  28. wx.EVT_MIDDLE_DOWN,
  29. wx.EVT_MIDDLE_DCLICK
  30. ]
  31. def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
  32. size=wx.DefaultSize, choices=[], style=0, validator=wx.DefaultValidator, name=NAME):
  33. super(ListBoxWithHeaders, self).__init__(parent, id, pos, size, [], style, validator, name)
  34. self.__headers = set()
  35. # Ignore all key events i'm bored to handle the header selection
  36. self.Bind(wx.EVT_KEY_DOWN, lambda event: None)
  37. # Make sure that a header is never selected
  38. self.Bind(wx.EVT_LISTBOX, self._on_listbox)
  39. for event in self.EVENTS:
  40. self.Bind(event, self._disable_header_selection)
  41. # Append the items in our own way in order to add the TEXT_PREFIX
  42. self.AppendItems(choices)
  43. def _disable_header_selection(self, event):
  44. """Stop event propagation if the selected item is a header."""
  45. row = self.HitTest(event.GetPosition())
  46. event_skip = True
  47. if row != wx.NOT_FOUND and self.GetString(row) in self.__headers:
  48. event_skip = False
  49. event.Skip(event_skip)
  50. def _on_listbox(self, event):
  51. """Make sure no header is selected."""
  52. if event.GetString() in self.__headers:
  53. self.Deselect(event.GetSelection())
  54. event.Skip()
  55. def _add_prefix(self, string):
  56. return self.TEXT_PREFIX + string
  57. def _remove_prefix(self, string):
  58. if string[:len(self.TEXT_PREFIX)] == self.TEXT_PREFIX:
  59. return string[len(self.TEXT_PREFIX):]
  60. return string
  61. # wx.ListBox methods
  62. def FindString(self, string):
  63. index = super(ListBoxWithHeaders, self).FindString(string)
  64. if index == wx.NOT_FOUND:
  65. # This time try with prefix
  66. index = super(ListBoxWithHeaders, self).FindString(self._add_prefix(string))
  67. return index
  68. def GetStringSelection(self):
  69. return self._remove_prefix(super(ListBoxWithHeaders, self).GetStringSelection())
  70. def GetString(self, index):
  71. if index < 0 or index >= self.GetCount():
  72. # Return empty string based on the wx.ListBox docs
  73. # for some reason parent GetString does not handle
  74. # invalid indices
  75. return ""
  76. return self._remove_prefix(super(ListBoxWithHeaders, self).GetString(index))
  77. def InsertItems(self, items, pos):
  78. items = [self._add_prefix(item) for item in items]
  79. super(ListBoxWithHeaders, self).InsertItems(items, pos)
  80. def SetSelection(self, index):
  81. if index == wx.NOT_FOUND:
  82. self.Deselect(self.GetSelection())
  83. elif self.GetString(index) not in self.__headers:
  84. super(ListBoxWithHeaders, self).SetSelection(index)
  85. def SetString(self, index, string):
  86. old_string = self.GetString(index)
  87. if old_string in self.__headers and string != old_string:
  88. self.__headers.remove(old_string)
  89. self.__headers.add(string)
  90. super(ListBoxWithHeaders, self).SetString(index, string)
  91. def SetStringSelection(self, string):
  92. if string in self.__headers:
  93. return False
  94. self.SetSelection(self.FindString(string))
  95. return True
  96. # wx.ItemContainer methods
  97. def Append(self, string):
  98. super(ListBoxWithHeaders, self).Append(self._add_prefix(string))
  99. def AppendItems(self, strings):
  100. strings = [self._add_prefix(string) for string in strings]
  101. super(ListBoxWithHeaders, self).AppendItems(strings)
  102. def Clear(self):
  103. self.__headers.clear()
  104. super(ListBoxWithHeaders, self).Clear()
  105. def Delete(self, index):
  106. string = self.GetString(index)
  107. if string in self.__headers:
  108. self.__headers.remove(string)
  109. super(ListBoxWithHeaders, self).Delete(index)
  110. # Extra methods
  111. def add_header(self, header_string):
  112. self.__headers.add(header_string)
  113. super(ListBoxWithHeaders, self).Append(header_string)
  114. def add_item(self, item, with_prefix=True):
  115. if with_prefix:
  116. item = self._add_prefix(item)
  117. super(ListBoxWithHeaders, self).Append(item)
  118. def add_items(self, items, with_prefix=True):
  119. if with_prefix:
  120. items = [self._add_prefix(item) for item in items]
  121. super(ListBoxWithHeaders, self).AppendItems(items)
  122. class ListBoxPopup(wx.PopupTransientWindow):
  123. """ListBoxWithHeaders as a popup.
  124. This class uses the wx.PopupTransientWindow to create the popup and the
  125. API is based on the wx.combo.ComboPopup class.
  126. Attributes:
  127. EVENTS_TABLE (dict): Dictionary that contains all the events
  128. that this class emits.
  129. """
  130. EVENTS_TABLE = {
  131. "EVT_COMBOBOX": crt_command_event(wx.EVT_COMBOBOX),
  132. "EVT_COMBOBOX_DROPDOWN" : crt_command_event(wx.EVT_COMBOBOX_DROPDOWN),
  133. "EVT_COMBOBOX_CLOSEUP": crt_command_event(wx.EVT_COMBOBOX_CLOSEUP)
  134. }
  135. def __init__(self, parent=None, flags=wx.BORDER_NONE):
  136. super(ListBoxPopup, self).__init__(parent, flags)
  137. self.__listbox = None
  138. def _on_motion(self, event):
  139. row = self.__listbox.HitTest(event.GetPosition())
  140. if row != wx.NOT_FOUND:
  141. self.__listbox.SetSelection(row)
  142. if self.__listbox.IsSelected(row):
  143. self.curitem = row
  144. def _on_left_down(self, event):
  145. self.value = self.curitem
  146. self.Dismiss()
  147. # Send EVT_COMBOBOX to inform the parent for changes
  148. wx.PostEvent(self, self.EVENTS_TABLE["EVT_COMBOBOX"])
  149. def Popup(self):
  150. super(ListBoxPopup, self).Popup()
  151. wx.PostEvent(self, self.EVENTS_TABLE["EVT_COMBOBOX_DROPDOWN"])
  152. def OnDismiss(self):
  153. wx.PostEvent(self, self.EVENTS_TABLE["EVT_COMBOBOX_CLOSEUP"])
  154. # wx.combo.ComboPopup methods
  155. def Init(self):
  156. self.value = self.curitem = -1
  157. def Create(self, parent):
  158. self.__listbox = ListBoxWithHeaders(parent, style=wx.LB_SINGLE)
  159. self.__listbox.Bind(wx.EVT_MOTION, self._on_motion)
  160. self.__listbox.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
  161. sizer = wx.BoxSizer()
  162. sizer.Add(self.__listbox, 1, wx.EXPAND)
  163. self.SetSizer(sizer)
  164. return True
  165. def GetAdjustedSize(self, min_width, pref_height, max_height):
  166. width, height = self.GetBestSize()
  167. if width < min_width:
  168. width = min_width
  169. if pref_height != -1:
  170. height = pref_height * self.__listbox.GetCount() + 5
  171. if height > max_height:
  172. height = max_height
  173. return wx.Size(width, height)
  174. def GetControl(self):
  175. return self.__listbox
  176. def GetStringValue(self):
  177. return self.__listbox.GetString(self.value)
  178. #def SetStringValue(self, string):
  179. #self.__listbox.SetStringSelection(string)
  180. class CustomComboBox(wx.Panel):
  181. """Custom combobox.
  182. Attributes:
  183. CB_READONLY (long): Read-only style. The only one supported from the
  184. wx.ComboBox styles.
  185. NAME (string): Default name for the name argument of the __init__.
  186. """
  187. #NOTE wx.ComboBox does not support EVT_MOTION inside the popup
  188. #NOTE Tried with ComboCtrl but i was not able to draw the button
  189. CB_READONLY = wx.TE_READONLY
  190. NAME = "customComboBox"
  191. def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition,
  192. size=wx.DefaultSize, choices=[], style=0, validator=wx.DefaultValidator, name=NAME):
  193. super(CustomComboBox, self).__init__(parent, id, pos, size, 0, name)
  194. assert style == self.CB_READONLY or style == 0
  195. # Create components
  196. self.textctrl = wx.TextCtrl(self, wx.ID_ANY, style=style, validator=validator)
  197. tc_height = self.textctrl.GetSize()[1]
  198. self.button = wx.Button(self, wx.ID_ANY, "", size=(tc_height, tc_height))
  199. # Create the ListBoxPopup in two steps
  200. self.listbox = ListBoxPopup(self)
  201. self.listbox.Init()
  202. self.listbox.Create(self.listbox)
  203. # Set layout
  204. sizer = wx.BoxSizer()
  205. sizer.Add(self.textctrl, 1, wx.ALIGN_CENTER_VERTICAL)
  206. sizer.Add(self.button)
  207. self.SetSizer(sizer)
  208. # Bind events
  209. self.button.Bind(wx.EVT_BUTTON, self._on_button)
  210. for event in ListBoxPopup.EVENTS_TABLE.values():
  211. self.listbox.Bind(wx.PyEventBinder(event.GetEventType()), self._propagate)
  212. # Append items since the ListBoxPopup does not have the 'choices' arg
  213. self.listbox.GetControl().AppendItems(choices)
  214. self.SetStringSelection(value)
  215. def _propagate(self, event):
  216. if event.GetEventType() == wx.EVT_COMBOBOX.typeId:
  217. self.textctrl.SetValue(self.listbox.GetStringValue())
  218. wx.PostEvent(self, event)
  219. def _on_button(self, event):
  220. self.Popup()
  221. def _calc_popup_position(self):
  222. tc_x_axis, tc_y_axis = self.textctrl.ClientToScreen((0, 0))
  223. _, tc_height = self.textctrl.GetSize()
  224. return tc_x_axis, tc_y_axis + tc_height
  225. def _calc_popup_size(self):
  226. me_width, _ = self.GetSize()
  227. _, tc_height = self.textctrl.GetSize()
  228. _, screen_height = wx.DisplaySize()
  229. _, me_y_axis = self.GetScreenPosition()
  230. available_height = screen_height - (me_y_axis + tc_height)
  231. sug_width, sug_height = self.listbox.GetAdjustedSize(me_width, tc_height, available_height)
  232. return me_width, sug_height
  233. # wx.ComboBox methods
  234. def Dismiss(self):
  235. self.listbox.Dismiss()
  236. def FindString(self, string, caseSensitive=False):
  237. #TODO handle caseSensitive
  238. return self.listbox.GetControl().FindString(string)
  239. def GetCount(self):
  240. return self.listbox.GetControl().GetCount()
  241. def GetCurrentSelection(self):
  242. return self.GetSelection()
  243. def GetInsertionPoint(self):
  244. return self.textctrl.GetInsertionPoint()
  245. def GetSelection(self):
  246. return self.listbox.value
  247. def GetTextSelection(self):
  248. return self.textctrl.GetSelection()
  249. def GetString(self, index):
  250. return self.listbox.GetControl().GetString(index)
  251. def GetStringSelection(self):
  252. return self.listbox.GetStringValue()
  253. def IsListEmpty(self):
  254. return self.listbox.GetControl().GetCount() == 0
  255. def IsTextEmpty(self):
  256. return not self.textctrl.GetValue()
  257. def Popup(self):
  258. self.listbox.SetPosition(self._calc_popup_position())
  259. self.listbox.SetSize(self._calc_popup_size())
  260. self.listbox.Popup()
  261. def SetSelection(self, index):
  262. self.listbox.GetControl().SetSelection(index)
  263. if self.listbox.GetControl().IsSelected(index):
  264. self.listbox.value = index
  265. self.textctrl.SetValue(self.listbox.GetStringValue())
  266. def SetString(self, index, string):
  267. self.listbox.GetControl().SetString(index, string)
  268. def SetTextSelection(self, from_, to_):
  269. self.textctrl.SetSelection(from_, to_)
  270. def SetStringSelection(self, string):
  271. index = self.listbox.GetControl().FindString(string)
  272. self.listbox.GetControl().SetSelection(index)
  273. if index != wx.NOT_FOUND and self.listbox.GetControl().GetSelection() == index:
  274. self.listbox.value = index
  275. self.textctrl.SetValue(string)
  276. def SetValue(self, value):
  277. self.textctrl.SetValue(value)
  278. # wx.ItemContainer methods
  279. def Clear(self):
  280. self.textctrl.Clear()
  281. self.listbox.GetControl().Clear()
  282. def Append(self, item):
  283. self.listbox.GetControl().Append(item)
  284. def AppendItems(self, items):
  285. self.listbox.GetControl().AppendItems(items)
  286. def Delete(self, index):
  287. self.listbox.GetControl().Delete(index)
  288. # wx.TextEntry methods
  289. def GetValue(self):
  290. return self.textctrl.GetValue()
  291. # ListBoxWithHeaders methods
  292. def add_header(self, header):
  293. self.listbox.GetControl().add_header(header)
  294. def add_item(self, item, with_prefix=True):
  295. self.listbox.GetControl().add_item(item, with_prefix)
  296. def add_items(self, items, with_prefix=True):
  297. self.listbox.GetControl().add_items(items, with_prefix)