#!/usr/bin/env python

# This program demonstrates a bug involving multi-column ListCtrl on Windows.
# The first column contains the wrong text and most of the cells in the remaining
# columns are mostly blank. This doesn't happen on MacOSX or Linux.
#
# Start this program and click on the 'ListCtrl' button which will create a
# dialog that contains a multi-column ListCtrl.
#
# On MacOSX and Linux, this contains:
#
# 11111 John   Smith
# 22222 Julie  Jones
# 33333 Peter  Wallace
# 44444 Jane   Brown
# 55555 Philip Jenkins
# 66666 Mary   Smith
# 77777 Sarah  Donaldson
# 88888 Jacob  Silk
# 99999 Sally  Moss
#
# But on Windows, it contains:
#
# 99999
# 88888
# 77777
# 66666
# 55555
# 44444
# 33333
# 22222
# 11111 Sally  Moss
#
# Also ListCtrl.GetFirstSelected() on Windows always returns 0 no matter
# what item is actually the first/only selected item.

import sys
import string
from wx import *
from wx.aui import *
from wx.lib.mixins.listctrl import *
from wx.lib.scrolledpanel import *

class TestApp(App):
	def GetAppName(self):
		return 'Test'
	def OnInit(self):
		global app
		app = self
		self.mainframe = MainFrame(self)
		self.SetTopWindow(self.mainframe)
		self.mainframe.Show()
		return True

class AutoWidthListCtrl(ListCtrl, ListCtrlAutoWidthMixin):
	'''Required by ListCtrlAutoWidthMixin. Seems to do nothing on MacOSX.'''

	def __init__(self, parent, id=-1, pos=DefaultPosition, size=DefaultSize, style=0):
		'''Initialise this control. Just calls the ListCtrl.__init__() and
		ListCtrlAutoWidthMixin.__init__().'''
		ListCtrl.__init__(self, parent, id, pos, size, style)
		ListCtrlAutoWidthMixin.__init__(self)

class SearchDialog(Dialog, ColumnSorterMixin):
	'''Popup dialog to allow the user to choose from a list of particular
	database records.'''

	def __init__(self, parent, id=-1, title=EmptyString, pos=DefaultPosition, size=DefaultSize, style=DEFAULT_DIALOG_STYLE, name=DialogNameStr, data=None, label='records', columns=None, persist=None, cmp2=None, orig=None):
		'''Initialise the SearchDialog. Most arguments are the same as for
		any wx window object. Data is a list of rows to display (each of
		which is a list of strings). It should be derived from the contents
		of orig which is a list of any object you like (probably database
		records). The contents of data are shown to the user in multiple
		columns. When they select a row, the corresponding item in orig is
		returned to the caller by get_selected(). Label is displayed to the
		user as the name of the type of records being searched for. Columns
		is a list of dict objects that specify the columns to show to the
		user. Each dict must contain the keys 'name' and 'width' with
		appropriate values. The name is used as the text in the column
		header. The width is used as the width of the column in pixels.
		Persist is a name to use as a prefix when saving the column widths
		to persistant storage. If persist is not supplied, any changes that
		the user makes to the widths of each column will not be remembered
		for the next time. Be nice and supply a unique value for persist.
		Cmp2 is a comparison function to be used as a tie-breaker. It is
		used by the ColumnSorterMixin as the GetSecondarySortValues()
		method.'''
		Dialog.__init__(self, parent, id, title, pos, size, style, name)
		self.data = data
		self.orig = orig
		self.columns = columns
		self.persist = persist
		self.cmp2 = cmp2
		sz1 = BoxSizer(VERTICAL)
		sz2 = StaticBoxSizer(StaticBox(self, label='Matching ' + label), VERTICAL)
		style = LC_REPORT | LC_SINGLE_SEL | LC_SORT_DESCENDING
		self.list = AutoWidthListCtrl(self, size=(500, 300), style=style)
		for i in range(len(self.columns)):
			self.list.InsertColumn(i, self.columns[i]['name'])
			if 'width' in self.columns[i]:
				w = self.columns[i]['width']
			else:
				w = 200 if i else 50
			#if self.persist:
			#	w = app.env.get_int('%s.col%d.width' % (self.persist, i), w)
			self.list.SetColumnWidth(i, w)

		self.itemDataMap = {} # Needed by ColumnSorterMixin
		for i in range(len(self.data)):
			r = self.data[i]
			self.list.InsertStringItem(i, r[0])
			for j in range(1, len(self.columns)):
				self.list.SetStringItem(i, j, r[j] if r[j] != None else '')
			self.list.SetItemData(i, i) # Needed by ColumnSorterMixin
			self.itemDataMap[i] = r # Needed by ColumnSorterMixin

		ColumnSorterMixin.__init__(self, 3)
		self.SortListItems(0, True)

		self.list.Bind(EVT_LIST_ITEM_ACTIVATED, self.OnListItemActivated)

		sz2.Add(self.list, 1, EXPAND | ALL, 3)
		sz1.Add(sz2, 1, EXPAND | ALL, 3)
		bsz = self.CreateButtonSizer(OK | CANCEL)
		if bsz:
			sz1.Add(bsz, 0, EXPAND | ALL, 3)
		self.SetSizerAndFit(sz1)
		self.Centre()
		# Select the first item in the list
		self.list.SetFocus()
		state = LIST_STATE_FOCUSED | LIST_STATE_SELECTED
		self.list.SetItemState(0, state, state)

	def get_selected(self):
		'''Call this after the dialog's ShowModal() method returns ID_OK to see
		which record was selected.'''
		i = self.list.GetFirstSelected() # Index of sorted items
		if i == -1: return None
		i = self.list.GetItemData(i) # Original index into employee array
		print 'selected index =', i
		return self.orig[i]

	def OnListItemActivated(self, event):
		'''Handler for the EVT_LIST_ITEM_ACTIVATED event. Simulate clicking
		the OK button.'''
		CallAfter(self.ProcessEvent, CommandEvent(wxEVT_COMMAND_BUTTON_CLICKED, ID_OK))

	def Destroy(self):
		'''Save column widths to persistant storage if desired before
		destroying the dialog.'''
		#if self.persist:
		#	for i in range(len(self.columns)):
		#		app.env.set_int('%s.col%d.width' % (self.persist, i), self.list.GetColumnWidth(i))
		Dialog.Destroy(self)

	def GetListCtrl(self):
		'''Required by ColumnSorterMixin. Return the ListCtrl.'''
		return self.list

	def GetSecondarySortValues(self, col, key1, key2):
		'''Used by ColumnSorterMixin. Comparison function for tie-breaking.'''
		if self.cmp2:
			return self.cmp2(self, col, key1, key2)
		return (key1, key2) # Default implementation

class T_employee():
	def __init__(self, row):
		self.id, self.employee_number, self.first_names, self.family_name = row
	def __str__(self):
		return 'T_Employee(%d, %s, %s, %s)' % (self.id, self.employee_number, self.first_names, self.family_name)

class EmployeeSearchDialog(SearchDialog):
	'''Popup dialog to search for employee records.'''

	def __init__(self, parent, records, id=-1, title=EmptyString, pos=DefaultPosition, size=DefaultSize, style=DEFAULT_DIALOG_STYLE, name=DialogNameStr):
		'''Initialise this dialog. Records is the list of T_employee objects
		to show to the user.'''
		orig = records
		data = [[e.employee_number, e.first_names, e.family_name] for e in records]
		label = 'employees'
		persist = 'employee_search_results'
		columns = \
		[
			dict(name='Number', width=50),
			dict(name='First Names', width=200),
			dict(name='Family Name', width=200)
		]

		def cmp2(self, col, key1, key2):
			'''Used by ColumnSorterMixin.'''
			r1, r2 = self.data[key1], self.data[key2]
			if col == 1: # If sorting by first_names, secondary is family_name
				return (r1[2], r2[2])
			if col == 2: # If sorting by family_name, secondary is first_names
				return (r1[1], r2[1])
			return (key1, key2) # Default implementation (never reached as employee_number is unique)

		SearchDialog.__init__(self, parent, id, title, pos, size, style, name, data, label, columns, persist, cmp2, orig)

class MainFrame(Frame):
	def __init__(self, app):
		Frame.__init__(self, None, title=app.GetAppName())
		self.panel = Panel(self)
		self.button = Button(self.panel, label='ListCtrl')
		self.button.Bind(EVT_BUTTON, self.OnButton)
		self.CreateStatusBar()

	def OnButton(self, event):
		records = [
			T_employee([1, '11111', 'John', 'Smith']),
			T_employee([2, '22222', 'Julie', 'Jones']),
			T_employee([3, '33333', 'Peter', 'Wallace']),
			T_employee([4, '44444', 'Jane', 'Brown']),
			T_employee([5, '55555', 'Philip', 'Jenkins']),
			T_employee([6, '66666', 'Mary', 'Smith']),
			T_employee([7, '77777', 'Sarah', 'Donaldson']),
			T_employee([8, '88888', 'Jacob', 'Silk']),
			T_employee([9, '99999', 'Sally', 'Moss']),
		]
		d = EmployeeSearchDialog(self, records=records)
		record = d.get_selected() if d.ShowModal() == ID_OK else None
		d.Destroy()
		if record:
			print str(record)

def main(redirect=False):
	'''Call this function from another module to simulate running this module
	directly or to override the lack of stdout/stderr redirection to a window.'''
	TestApp(redirect=redirect).MainLoop()

if __name__ == '__main__':
	main()

