Browse Source

Add new combobox widget

doc-issue-template
MrS0m30n3 8 years ago
parent
commit
feef97cbc4
2 changed files with 698 additions and 0 deletions
  1. 273
      tests/test_widgets.py
  2. 425
      youtube_dl_gui/widgets.py

273
tests/test_widgets.py

@ -0,0 +1,273 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""Contains test cases for the widgets.py module."""
from __future__ import unicode_literals
import sys
import os.path
import unittest
PATH = os.path.realpath(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(os.path.dirname(PATH)))
try:
import wx
import mock
from youtube_dl_gui.widgets import (
ListBoxWithHeaders,
CustomComboBox,
ListBoxPopup
)
except ImportError as error:
print error
sys.exit(1)
class TestListBoxWithHeaders(unittest.TestCase):
"""Test cases for the ListBoxWithHeaders widget."""
def setUp(self):
self.app = wx.App()
self.frame = wx.Frame(None)
self.listbox = ListBoxWithHeaders(self.frame)
self.listbox.add_header("Header")
self.listbox.add_items(["item%s" % i for i in xrange(10)])
def tearDown(self):
self.frame.Destroy()
def test_find_string_header_found(self):
self.assertEqual(self.listbox.FindString("Header"), 0)
def test_find_string_header_not_found(self):
self.assertEqual(self.listbox.FindString("Header2"), wx.NOT_FOUND)
def test_find_string_item_found(self):
self.assertEqual(self.listbox.FindString("item1"), 2)
def test_find_string_item_not_found(self):
self.assertEqual(self.listbox.FindString("item"), wx.NOT_FOUND)
def test_get_string_header(self):
self.assertEqual(self.listbox.GetString(0), "Header")
def test_get_string_item(self):
self.assertEqual(self.listbox.GetString(10), "item9")
def test_get_string_item_not_found(self):
self.assertEqual(self.listbox.GetString(11), "")
def test_get_string_item_negative_index(self):
self.assertEqual(self.listbox.GetString(-1), "")
def test_insert_items(self):
self.listbox.SetSelection(1)
self.listbox.InsertItems(["new_item1", "new_item2"], 1)
self.assertEqual(self.listbox.GetString(1), "new_item1")
self.assertEqual(self.listbox.GetString(2), "new_item2")
self.assertEqual(self.listbox.GetString(3), "item0")
self.assertTrue(self.listbox.IsSelected(3)) # Old selection + 2
def test_set_selection_header(self):
self.listbox.SetSelection(0)
self.assertFalse(self.listbox.IsSelected(0))
def test_set_selection_item_valid_index(self):
self.listbox.SetSelection(1)
self.assertEqual(self.listbox.GetSelection(), 1)
def test_set_selection_item_invalid_index(self):
self.listbox.SetSelection(1)
self.assertEqual(self.listbox.GetSelection(), 1)
self.listbox.SetSelection(wx.NOT_FOUND)
self.assertEqual(self.listbox.GetSelection(), wx.NOT_FOUND)
def test_set_string_item(self):
self.listbox.SetString(1, "item_mod0")
self.assertEqual(self.listbox.GetString(1), "item_mod0")
def test_set_string_header(self):
self.listbox.SetString(0, "New header")
self.assertEqual(self.listbox.GetString(0), "New header")
# Make sure that the header is not selectable
self.listbox.SetSelection(0)
self.assertFalse(self.listbox.IsSelected(0))
def test_set_string_selection_header(self):
self.assertFalse(self.listbox.SetStringSelection("Header"))
self.assertFalse(self.listbox.IsSelected(0))
def test_set_string_selection_item(self):
self.assertTrue(self.listbox.SetStringSelection("item1"))
self.assertTrue(self.listbox.IsSelected(2))
def test_get_string_selection(self):
self.listbox.SetSelection(1)
self.assertEqual(self.listbox.GetStringSelection(), "item0")
def test_get_string_selection_empty(self):
self.assertEqual(self.listbox.GetStringSelection(), "")
# wx.ItemContainer methods
def test_append(self):
self.listbox.Append("item666")
self.assertEqual(self.listbox.GetString(11), "item666")
def test_append_items(self):
self.listbox.AppendItems(["new_item1", "new_item2"])
self.assertEqual(self.listbox.GetString(11), "new_item1")
self.assertEqual(self.listbox.GetString(12), "new_item2")
def test_clear(self):
self.listbox.Clear()
self.assertEqual(self.listbox.GetItems(), [])
def test_delete(self):
self.listbox.Delete(0)
self.assertEqual(self.listbox.GetString(0), "item0")
# Test item selection
self.listbox.SetSelection(0)
self.assertTrue(self.listbox.IsSelected(0))
# Test object extra methods
def test_add_header(self):
self.listbox.add_header("Header2")
self.listbox.SetSelection(11)
self.assertFalse(self.listbox.IsSelected(11))
@mock.patch("wx.ListBox.Append")
def test_add_item_with_prefix(self, mock_append):
self.listbox.add_item("new_item")
mock_append.assert_called_once_with(ListBoxWithHeaders.TEXT_PREFIX + "new_item")
@mock.patch("wx.ListBox.Append")
def test_add_item_without_prefix(self, mock_append):
self.listbox.add_item("new_item", with_prefix=False)
mock_append.assert_called_once_with("new_item")
@mock.patch("wx.ListBox.AppendItems")
def test_add_items_with_prefix(self, mock_append):
self.listbox.add_items(["new_item1", "new_item2"])
mock_append.assert_called_once_with([ListBoxWithHeaders.TEXT_PREFIX + "new_item1",
ListBoxWithHeaders.TEXT_PREFIX + "new_item2"])
@mock.patch("wx.ListBox.AppendItems")
def test_add_items_without_prefix(self, mock_append):
self.listbox.add_items(["new_item1", "new_item2"], with_prefix=False)
mock_append.assert_called_once_with(["new_item1", "new_item2"])
class TestCustomComboBox(unittest.TestCase):
"""Test cases for the CustomComboBox widget."""
def setUp(self):
self.app = wx.App()
self.frame = wx.Frame(None)
self.combobox = CustomComboBox(self.frame)
# Call directly the ListBoxWithHeaders methods
self.combobox.listbox.GetControl().add_header("Header")
self.combobox.listbox.GetControl().add_items(["item%s" % i for i in xrange(10)])
def tearDown(self):
self.frame.Destroy()
def test_init(self):
combobox = CustomComboBox(self.frame, -1, "item1", choices=["item0", "item1", "item2"])
self.assertEqual(combobox.GetValue(), "item1")
self.assertEqual(combobox.GetCount(), 3)
self.assertEqual(combobox.GetSelection(), 1)
# wx.ComboBox methods
# Not all of them since most of them are calls to ListBoxWithHeaders
# methods and we already have tests for those
def test_is_list_empty_false(self):
self.assertFalse(self.combobox.IsListEmpty())
def test_is_list_empty_true(self):
self.combobox.Clear()
self.assertTrue(self.combobox.IsListEmpty())
def test_is_text_empty_false(self):
self.combobox.SetValue("somevalue")
self.assertFalse(self.combobox.IsTextEmpty())
def test_is_text_empty_true(self):
self.assertTrue(self.combobox.IsTextEmpty())
def test_set_selection_item(self):
self.combobox.SetSelection(1)
self.assertEqual(self.combobox.GetSelection(), 1)
self.assertEqual(self.combobox.GetValue(), "item0")
def test_set_selection_header(self):
self.combobox.SetSelection(0)
self.assertEqual(self.combobox.GetSelection(), wx.NOT_FOUND)
self.assertEqual(self.combobox.GetValue(), "")
def test_set_string_selection_item(self):
self.combobox.SetStringSelection("item0")
self.assertEqual(self.combobox.GetStringSelection(), "item0")
self.assertEqual(self.combobox.GetValue(), "item0")
def test_set_string_selection_header(self):
self.combobox.SetStringSelection("Header")
self.assertEqual(self.combobox.GetStringSelection(), "")
self.assertEqual(self.combobox.GetValue(), "")
def test_set_string_selection_invalid_string(self):
self.combobox.SetStringSelection("abcde")
self.assertEqual(self.combobox.GetStringSelection(), "")
self.assertEqual(self.combobox.GetValue(), "")
# wx.ItemContainer methods
def test_clear(self):
self.combobox.SetValue("value")
self.combobox.Clear()
self.assertEqual(self.combobox.GetCount(), 0)
self.assertTrue(self.combobox.IsTextEmpty())
def test_append(self):
self.combobox.Append("item10")
self.assertEqual(self.combobox.GetCount(), 12)
def test_append_items(self):
self.combobox.AppendItems(["item10", "item11"])
self.assertEqual(self.combobox.GetCount(), 13)
def test_delete(self):
self.combobox.Delete(1)
self.assertEqual(self.combobox.GetString(1), "item1")
# wx.TextEntry methods
def test_get_value(self):
self.combobox.SetValue("value")
self.assertEqual(self.combobox.GetValue(), "value")
def main():
unittest.main()
if __name__ == '__main__':
main()

425
youtube_dl_gui/widgets.py

@ -0,0 +1,425 @@
#!/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()
# 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):
# Calculate the position & size of the popup
tc_x_axis, tc_y_axis = self.textctrl.ClientToScreen((0, 0))
_, tc_height = self.textctrl.GetSize()
_, self_y_axis = self.GetScreenPosition()
self_width, _ = self.GetSize()
_, screen_height = wx.DisplaySize()
max_height = screen_height - (self_y_axis + tc_height)
self.listbox.SetPosition((tc_x_axis, tc_y_axis + tc_height))
self.listbox.SetSize(self.listbox.GetAdjustedSize(self_width, tc_height, max_height))
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)
Loading…
Cancel
Save