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
434 lines
13 KiB
#!/usr/bin/env python
|
|
# -*- coding: UTF-8 -*-
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
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):
|
|
|
|
"""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=[], style=0, validator=wx.DefaultValidator, name=NAME):
|
|
super(ListBoxWithHeaders, self).__init__(parent, id, pos, size, [], style, validator, name)
|
|
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):
|
|
index = super(ListBoxWithHeaders, self).FindString(string)
|
|
|
|
if index == wx.NOT_FOUND:
|
|
# This time try with prefix
|
|
index = super(ListBoxWithHeaders, self).FindString(self._add_prefix(string))
|
|
|
|
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 Append(self, string):
|
|
super(ListBoxWithHeaders, self).Append(self._add_prefix(string))
|
|
|
|
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)
|
|
|
|
|
|
class ListBoxPopup(wx.PopupTransientWindow):
|
|
|
|
"""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):
|
|
super(ListBoxPopup, self).__init__(None)
|
|
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
|
|
|
|
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):
|
|
super(ListBoxPopup, self).Popup()
|
|
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
|
|
|
|
def Init(self):
|
|
self.value = self.curitem = -1
|
|
|
|
def Create(self, parent):
|
|
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)
|
|
|
|
|
|
class CustomComboBox(wx.Panel):
|
|
|
|
"""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=[], style=0, validator=wx.DefaultValidator, name=NAME):
|
|
super(CustomComboBox, self).__init__(parent, id, pos, size, 0, name)
|
|
|
|
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.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)
|
|
|
|
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()
|
|
|
|
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)
|