#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4

import random
import sys
import wx
import wx.grid

NUM_DATA = 200
ROW_SIZE = 30
MAGIC_LIST_HEIGHT = 5


class Scroll_binder():
	"""Inherit to be able to bind vscrolling to another widget."""
	def __init__(self):
		"""f() -> Scroll_binder

		Initialises the internal data required for vertical scrolling.
		"""
		self._locked = False
		self._bound_widget = None
		self._is_list_control = hasattr(self, "ScrollList")

		self.Bind(wx.EVT_SCROLLWIN, self._did_scroll)
		self.Bind(wx.EVT_MOUSEWHEEL, self._mousewheel)
		self.Bind(wx.EVT_SCROLLWIN_LINEUP, self._lineup)
		self.Bind(wx.EVT_SCROLLWIN_LINEDOWN, self._linedown)
		self.Bind(wx.EVT_SCROLLWIN_PAGEUP, self._pageup)
		self.Bind(wx.EVT_SCROLLWIN_PAGEDOWN, self._pagedown)
		self.Bind(wx.EVT_SCROLLWIN_TOP, self._top)
		self.Bind(wx.EVT_SCROLLWIN_BOTTOM, self._bottom)
		self.Bind(wx.EVT_KEY_DOWN, self._key_down)


	def bind_scroll(self, to):
		self._bound_widget = to


	def _key_down(self, event):
		pass


	def _mousewheel(self, event):
		"""Mouse wheel scrolled. Up or down, give or take."""
		if event.m_wheelRotation > 0:
			do_scroll = self._lineup
		else:
			do_scroll = self._linedown

		for r in range(event.m_linesPerAction):
			do_scroll()


	def _pageup(self, event):
		"""Clicked on a scrollbar space, performing a page up."""
		if event.GetOrientation() != wx.VERTICAL:
			event.Skip()
			return

		pos = self.GetScrollPos(wx.VERTICAL)
		if self._is_list_control:
			amount = self.GetCountPerPage()
		else:
			amount = self.GetScrollPageSize(wx.VERTICAL)

		print "PageUp %d from %d" % (amount, pos)
		self._bound_widget.scroll_to(max(0, pos - amount))
		self.scroll_to(max(0, pos - amount))


	def _pagedown(self, event):
		"""Clicked on a scrollbar space, performing a page down."""
		if event.GetOrientation() != wx.VERTICAL:
			event.Skip()
			return

		pos = self.GetScrollPos(wx.VERTICAL)
		if self._is_list_control:
			amount = self.GetCountPerPage()
		else:
			amount = self.GetScrollPageSize(wx.VERTICAL)

		print "PageDown %d from %d" % (amount, pos)
		self._bound_widget.scroll_to(pos + amount)
		self.scroll_to(pos + amount)


	def _top(self, event):
		"""Event handler for going to the top."""
		if event.GetOrientation() != wx.VERTICAL:
			event.Skip()
			return

		print "Top"
		self._bound_widget.scroll_to(0)
		self.scroll_to(0)


	def _bottom(self, event):
		"""Event handler for going to the bottom."""
		if event.GetOrientation() != wx.VERTICAL:
			event.Skip()
			return

		print "Bottom"
		pos = 10000000
		self._bound_widget.scroll_to(pos)
		self.scroll_to(pos)


	def _lineup(self, event = None):
		"""Event handler for pressing the up arrow."""
		if event and event.GetOrientation() != wx.VERTICAL:
			event.Skip()
			return

		pos = self.GetScrollPos(wx.VERTICAL)
		print "LineUp from", pos
		self._bound_widget.scroll_to(pos - 1)
		self.scroll_to(pos - 1)


	def _linedown(self, event = None):
		"""Event handler for pressing the down arrow."""
		if event and event.GetOrientation() != wx.VERTICAL:
			event.Skip()
			return

		pos = self.GetScrollPos(wx.VERTICAL)
		print "LineDown from", pos
		self._bound_widget.scroll_to(pos + 1)
		self.scroll_to(pos + 1)


	def _did_scroll(self, event):
		"""Event handler for manual scrolling."""
		try:
			if event.GetOrientation() != wx.VERTICAL or self._locked:
				return

			print "list pos was %d, will be %d" % (
				self.GetScrollPos(wx.VERTICAL), event.GetPosition())
			self._bound_widget.scroll_to(event.GetPosition())
		finally:
			event.Skip()


	def scroll_to(self, position):
		"""f(int) -> None

		Scrolls to a specific vertical position.
		"""
		self._locked = True

		if self._is_list_control:
			dif = position - self.GetScrollPos(wx.VERTICAL)
			self.ScrollList(-1, dif * ROW_SIZE)
		else:
			# Presume we are a grid.
			self.Scroll(-1, position)

		self._locked = False


class CustomDataTable(wx.grid.PyGridTableBase):
	def __init__(self):
		wx.grid.PyGridTableBase.__init__(self)

		self.colLabels = ['January', 'February', "March", "April",
			"May", "June", "July", "August", "September", "October",
			"November", "December"]

		self.dataTypes = [wx.grid.GRID_VALUE_FLOAT] * 12

		self.data = []
		for row in range(NUM_DATA):
			self.data.append([0] * 12)
			for f in range(12):
				self.data[row][f] = (random.random(), random.random())

	# Required methods for the wxPyGridTableBase interface
	def GetNumberRows(self):
		return len(self.data)

	def GetNumberCols(self):
		return len(self.data[0])

	def IsEmptyCell(self, row, col):
		try:
			return not self.data[row][col]
		except IndexError:
			return True

	# Get/Set values in the table.  The Python version of these
	# methods can handle any data-type, (as long as the Editor and
	# Renderer understands the type too,) not just strings as in the
	# C++ version.
	def GetValue(self, row, col):
		try:
			return self.data[row][col]
		except IndexError:
			return ''

	def SetValue(self, row, col, value):
		try:
			self.data[row][col] = value
		except IndexError:
			# add a new row
			self.data.append([''] * self.GetNumberCols())
			self.SetValue(row, col, value)

			# tell the grid we've added a row
			msg = wx.grid.GridTableMessage(self,            # The table
				wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, # what we did to it
				1                                       # how many
				)

			self.GetView().ProcessTableMessage(msg)


	#--------------------------------------------------
	# Some optional methods

	# Called when the grid needs to display labels
	def GetColLabelValue(self, col):
		return self.colLabels[col]

	# Called to determine the kind of editor/renderer to use by
	# default, doesn't necessarily have to be the same type used
	# natively by the editor/renderer if they know how to convert.
	def GetTypeName(self, row, col):
		return self.dataTypes[col]

	# Called to determine how the data can be fetched and stored by the
	# editor and renderer.  This allows you to enforce some type-safety
	# in the grid.
	def CanGetValueAs(self, row, col, typeName):
		colType = self.dataTypes[col].split(':')[0]
		if typeName == colType:
			return True
		else:
			return False

	def CanSetValueAs(self, row, col, typeName):
		return self.CanGetValueAs(row, col, typeName)


class CustomTableGrid(wx.grid.Grid, Scroll_binder):
	def __init__(self, parent):
		wx.grid.Grid.__init__(self, parent)
		Scroll_binder.__init__(self)

		self.table = CustomDataTable()

		# The second parameter means that the grid is to take ownership of the
		# table and will destroy it when done.  Otherwise you would need to
		# keep a reference to it and call it's Destroy method later.
		self.SetTable(self.table, True)

		#self.SetRowLabelSize(0)
		self.SetMargins(0, 0)
		self.AutoSizeColumns(True)
		# Disallow rows stretching vertically and set a fixed height.
		self.DisableDragRowSize()
		self.SetRowMinimalAcceptableHeight(ROW_SIZE)
		self.SetDefaultRowSize(ROW_SIZE, True)
		self.SetScrollRate(self.GetScrollLineX(), ROW_SIZE)
		# Don't let the user change the values.
		self.EnableEditing(False)


class List_control(wx.ListCtrl, Scroll_binder):
	def __init__(self, parent, size, style):
		wx.ListCtrl.__init__(self, parent = parent, size = size, style = style)
		Scroll_binder.__init__(self)

	def fill(self):
		# Create the images with the desired height.
		self.image_list = wx.ImageList(1, ROW_SIZE - 1)
		self.SetImageList(self.image_list, wx.IMAGE_LIST_SMALL)

		self.InsertColumn(0, "Col 1")
		self.InsertColumn(1, "Col 2")
		self.InsertColumn(2, "Col 3")
		for f in range(NUM_DATA):
			self.Append(("id%4d" % (f + 1), "Ref %d" % f, "total"))


class App(wx.App):
	"""Application class."""

	def OnInit(self):
		self.frame = wx.Frame(None, title = "My title", size = (700, 500))
		panel = wx.Panel(self.frame)
		self.SetTopWindow(self.frame)

		hbox = wx.BoxSizer(wx.HORIZONTAL)
		vbox1 = wx.BoxSizer(wx.VERTICAL)
		vbox2 = wx.BoxSizer(wx.VERTICAL)

		# The filler panel allows us to set the 'height' of the list.
		self.filler = wx.Panel(panel)
		self.list = List_control(panel, (300, -1),
			wx.LC_REPORT | wx.ALWAYS_SHOW_SB | wx.VSCROLL)
		self.list.fill()
		vbox1.Add(self.filler, 0, wx.EXPAND | wx.ALL, 0)
		vbox1.Add(self.list, 1, wx.EXPAND | wx.ALL, 5)

		self.grid = CustomTableGrid(panel)
		vbox2.Add(self.grid, 1, wx.EXPAND | wx.ALL, 5)
		hbox.Add(vbox1, 0, wx.EXPAND)
		hbox.Add(vbox2, 1, wx.EXPAND)

		# Bind the scrollbars of the widgets.
		self.grid.bind_scroll(self.list)
		self.list.bind_scroll(self.grid)

		self.resize_filler()
		panel.SetSizer(hbox)
		self.frame.Show()
		return True


	def resize_filler(self):
		"""f() -> None

		Resizes the filler panel to adapt the list control height. The position
		of the list control is moved in such a way that its entries correspond
		to the height of the grids entries.
		"""
		# Get the sizes of the list and grid top labels.
		grid_size = ROW_SIZE
		# Unfortunately the height of the list labels is not possible to get,
		# hence the use of the magic 4 number.
		list_size = self.list.GetCharHeight() + MAGIC_LIST_HEIGHT
		self.filler.SetSize((-1, grid_size - list_size))


def main():
	"""Main entry point."""
	app = App(redirect = False)
	app.MainLoop()
	print "main() finished running."


if __name__ == "__main__":
	main()

