# Copyright (c) 2003-2005 by Intevation GmbH
# Authors:
# Jan-Oliver Wagner <jan@intevation.de> (2003-2004)
# Martin Schulze <joey@infodrom.org> (2004)
# Frank Koormann <frank@intevation.de> (2003, 2006)
# Bernhard Herzog <bh@intevation.de> (2003)
# Jonathan Coles <jonathan@intevation.de> (2003)
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

"""Dialog for classifying how layers are displayed"""

__version__ = "$Revision: 2817 $"
# $Source$
# $Id: classifier.py 2817 2008-01-27 00:01:32Z bernhard $

import copy
import re

from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
     FIELDTYPE_STRING

import wx
from wx import grid

from Thuban import _
from Thuban.UI import internal_from_wxstring
from Thuban.UI.common import Color2wxColour, wxColour2Color

from Thuban.Model.messages import MAP_LAYERS_REMOVED, LAYER_SHAPESTORE_REPLACED
from Thuban.Model.range import Range
from Thuban.Model.classification import \
    Classification, ClassGroupDefault, \
    ClassGroupSingleton, ClassGroupPattern, ClassGroupRange, ClassGroupMap, \
    ClassGroupProperties

from Thuban.Model.color import Transparent

from Thuban.Model.layer import Layer
from Thuban.Model.data import SHAPETYPE_ARC, SHAPETYPE_POLYGON, SHAPETYPE_POINT

from Thuban.UI.classgen import ClassGenDialog
from Thuban.UI.colordialog import ColorDialog

from Thuban.UI.layerproperties import LayerProperties
from messages import MAP_REPLACED


# table columns
COL_VISIBLE = 0
COL_SYMBOL  = 1
COL_VALUE   = 2
COL_LABEL   = 3
NUM_COLS    = 4

# indices into the client data lists in Classifier.fields
FIELD_CLASS = 0
FIELD_TYPE = 1
FIELD_NAME = 2

#
# this is a silly work around to ensure that the table that is
# passed into SetTable is the same that is returned by GetTable
#
import weakref
class ClassGrid(grid.Grid):


    def __init__(self, parent, classifier):
        """Constructor.

        parent -- the parent window

        clazz -- the working classification that this grid should
                 use for display. 
        """
        grid.Grid.__init__(self, parent, -1, style = 0)

        self.classifier = classifier

        self.currentSelection = []

        self.Bind(grid.EVT_GRID_CELL_LEFT_DCLICK, self._OnCellDClick)
        self.Bind(grid.EVT_GRID_RANGE_SELECT, self._OnSelectedRange)
        self.Bind(grid.EVT_GRID_SELECT_CELL, self._OnSelectedCell)
        self.Bind(grid.EVT_GRID_COL_SIZE, self._OnCellResize)
        self.Bind(grid.EVT_GRID_ROW_SIZE, self._OnCellResize)
        self.Bind(grid.EVT_GRID_LABEL_RIGHT_CLICK, self._OnLabelRightClicked)


    #def GetCellAttr(self, row, col):
        #print "GetCellAttr ", row, col
        #Grid.GetCellAttr(self, row, col)

    def CreateTable(self, clazz, fieldType, shapeType, group = None):

        assert isinstance(clazz, Classification)

        table = self.GetTable()
        if table is None:
            w = self.GetDefaultColSize() * NUM_COLS \
                + self.GetDefaultRowLabelSize()
            h = self.GetDefaultRowSize() * 4 \
                + self.GetDefaultColLabelSize()

            self.SetDimensions(-1, -1, w, h)
            self.SetSizeHints(w, h, -1, -1)
            table = ClassTable(self)
            self.SetTable(table, True)


        self.SetSelectionMode(grid.Grid.wxGridSelectRows)
        self.ClearSelection()

        table.Reset(clazz, fieldType, shapeType, group)

    def GetCurrentSelection(self):
        """Return the currently highlighted rows as an increasing list
           of row numbers."""
        sel = copy.copy(self.currentSelection)
        sel.sort()
        return sel

    def GetSelectedRows(self):
        return self.GetCurrentSelection()

    #def SetCellRenderer(self, row, col, renderer):
        #raise ValueError(_("Must not allow setting of renderer in ClassGrid!"))

    #
    # [Set|Get]Table is taken from http://wiki.wxpython.org
    # they are needed as a work around to ensure that the table
    # that is passed to SetTable is the one that is returned 
    # by GetTable.
    #
    def SetTable(self, object, *attributes): 
        self.tableRef = weakref.ref(object) 
        return grid.Grid.SetTable(self, object, *attributes) 

    def GetTable(self): 
        try:
            return self.tableRef() 
        except:
            return None

    def DeleteSelectedRows(self):
        """Deletes all highlighted rows.

        If only one row is highlighted then after it is deleted the
        row that was below the deleted row is highlighted."""

        sel = self.GetCurrentSelection()

        # nothing to do
        if len(sel) == 0: return

        # if only one thing is selected check if it is the default
        # data row, because we can't remove that
        if len(sel) == 1:
            #group = self.GetTable().GetValueAsCustom(sel[0], COL_SYMBOL, None)
            group = self.GetTable().GetClassGroup(sel[0])
            if isinstance(group, ClassGroupDefault):
                wx.MessageDialog(self,
                                _("The Default group cannot be removed."),
                                style = wx.OK | wx.ICON_EXCLAMATION).ShowModal()
                return


        self.ClearSelection()

        # we need to remove things from the bottom up so we don't
        # change the indexes of rows that will be deleted next
        sel.reverse()

        #
        # actually remove the rows
        #
        table = self.GetTable()
        for row in sel:
            table.DeleteRows(row)

        #
        # if there was only one row selected highlight the row
        # that was directly below it, or move up one if the
        # deleted row was the last row.
        #
        if len(sel) == 1:
            r = sel[0]
            if r > self.GetNumberRows() - 1:
                r = self.GetNumberRows() - 1
            self.SelectRow(r)


    def SelectGroup(self, group, makeVisible = True):
        if group is None: return

        assert isinstance(group, ClassGroup)

        table = self.GetTable()

        assert table is not None

        for i in range(table.GetNumberRows()):
            g = table.GetClassGroup(i)
            if g is group:
                self.SelectRow(i)
                if makeVisible:
                    self.MakeCellVisible(i, 0)
                break

#
# XXX: This isn't working, and there is no way to deselect rows wxPython!
#
#   def DeselectRow(self, row):
#       self.ProcessEvent(
#           GridRangeSelectEvent(-1, 
#                                  wxEVT_GRID_RANGE_SELECT,
#                                  self,
#                                  (row, row), (row, row),
#                                  sel = False))

    def _OnCellDClick(self, event):
        """Handle a double click on a cell."""

        r = event.GetRow()
        c = event.GetCol()

        if c == COL_SYMBOL:
            self.classifier.EditSymbol(r)
        else:
            event.Skip()

    #
    # _OnSelectedRange() and _OnSelectedCell() were borrowed
    # from http://wiki.wxpython.org to keep track of which
    # cells are currently highlighted
    #
    def _OnSelectedRange(self, event):
        """Internal update to the selection tracking list""" 
        if event.Selecting():
            for index in range( event.GetTopRow(), event.GetBottomRow()+1):
                if index not in self.currentSelection:
                    self.currentSelection.append( index )
        else:
            for index in range( event.GetTopRow(), event.GetBottomRow()+1):
                while index in self.currentSelection:
                    self.currentSelection.remove( index )
        #self.ConfigureForSelection() 

        event.Skip()

    def _OnSelectedCell( self, event ):
        """Internal update to the selection tracking list""" 
        self.currentSelection = [ event.GetRow() ]
        #self.ConfigureForSelection() 
        event.Skip()

    def _OnCellResize(self, event):
        self.FitInside()
        event.Skip()

    def _OnLabelRightClicked(self, event):
        """Process right click on label, raise popup for row labels."""
        row, col = event.GetRow(), event.GetCol()
        if col == -1:
            self.labelPopup(event, row)

    def labelPopup(self, event, row):
        """Raise grid label popup."""
        # check if row label is Pattern or Singleton
        label = self.GetRowLabelValue(row)
        if (label == _("Pattern") or label == _("Singleton")):
            xe,ye = event.GetPosition()
            x=self.GetRowSize(row)/2
            menu = wx.Menu()
            patternID = wx.NewId()
            singletonID = wx.NewId()

            def _SetSingleton(event, self=self, row=row):
                table = self.GetTable()
                group = table.clazz.GetGroup(row - 1)
                if not isinstance(group, ClassGroupSingleton):
                    ngroup = ClassGroupSingleton(
                                group.GetPattern(),
                                group.GetProperties(),
                                group.GetLabel()
                                )
                    table.SetClassGroup(row, ngroup)

            def _SetPattern(event, self=self, row=row):
                table = self.GetTable()
                group = table.clazz.GetGroup(row - 1)
                if not isinstance(group, ClassGroupPattern):
                    try:
                        re.compile(group.GetValue())
                    except:
                        pass
                    else:
                        ngroup = ClassGroupPattern(
                                group.GetValue(),
                                group.GetProperties(),
                                group.GetLabel()
                                )
                        table.SetClassGroup(row, ngroup)

            menu.Append(singletonID, _("Singleton"))
            self.Bind(wx.EVT_MENU, _SetSingleton, id=singletonID)
            if self.GetTable().fieldType == FIELDTYPE_STRING:
                menu.Append(patternID, _("Pattern"))
                self.Bind(wx.EVT_MENU, _SetPattern, id=patternID)
            self.PopupMenu(menu, wx.Point(x,ye))
            menu.Destroy()

class ClassTable(grid.PyGridTableBase):
    """Represents the underlying data structure for the grid."""

    __col_labels = [_("Visible"), _("Symbol"), _("Value"), _("Label")]


    def __init__(self, view = None):
        """Constructor.

        shapeType -- the type of shape that the layer uses

        view -- a Grid object that uses this class for its table
        """

        grid.PyGridTableBase.__init__(self)

        assert len(ClassTable.__col_labels) == NUM_COLS

        self.clazz = None
        self.__colAttr = {}

        self.SetView(view)

    def Reset(self, clazz, fieldType, shapeType, group = None):
        """Reset the table with the given data.

        This is necessary because wxWindows does not allow a grid's
        table to change once it has been intially set and so we
        need a way of modifying the data.

        clazz -- the working classification that this table should
                 use for display. This may be different from the
                 classification in the layer.

        shapeType -- the type of shape that the layer uses
        """

        assert isinstance(clazz, Classification)

        self.GetView().BeginBatch()

        self.fieldType = fieldType
        self.shapeType = shapeType

        self.SetClassification(clazz, group)
        self.__Modified(-1)

        self.__colAttr = {}

        attr = grid.GridCellAttr()
        attr.SetEditor(grid.GridCellBoolEditor())
        attr.SetRenderer(grid.GridCellBoolRenderer())
        attr.SetAlignment(wx.ALIGN_CENTER, wx.ALIGN_CENTER)
        self.__colAttr[COL_VISIBLE] = attr

        attr = grid.GridCellAttr()
        attr.SetRenderer(ClassRenderer(self.shapeType))
        attr.SetReadOnly()
        self.__colAttr[COL_SYMBOL] = attr

        self.GetView().EndBatch()
        self.GetView().FitInside()

    def GetClassification(self):
        """Return the current classification."""
        return self.clazz

    def SetClassification(self, clazz, group = None):
        """Fill in the table with the given classification.
        Select the given group if group is not None.
        """

        self.GetView().BeginBatch()

        old_len = self.GetNumberRows()

        row = -1
        self.clazz = clazz

        self.__NotifyRowChanges(old_len, self.GetNumberRows())

        #
        # XXX: this is dead code at the moment
        #
        if row > -1:
            self.GetView().ClearSelection()
            self.GetView().SelectRow(row)
            self.GetView().MakeCellVisible(row, 0)

        self.__Modified()

        self.GetView().EndBatch()
        self.GetView().FitInside()

    def __NotifyRowChanges(self, curRows, newRows):
        """Make sure table updates correctly if the number of 
        rows changes.
        """
        #
        # silly message processing for updates to the number of
        # rows and columns
        #
        if newRows > curRows:
            msg = grid.GridTableMessage(self,
                        grid.GRIDTABLE_NOTIFY_ROWS_APPENDED,
                        newRows - curRows)    # how many
            self.GetView().ProcessTableMessage(msg)
            self.GetView().FitInside()
        elif newRows < curRows:
            msg = grid.GridTableMessage(self,
                        grid.GRIDTABLE_NOTIFY_ROWS_DELETED,
                        curRows,              # position
                        curRows - newRows)    # how many
            self.GetView().ProcessTableMessage(msg)
            self.GetView().FitInside()

    def __SetRow(self, row, group):
        """Set a row's data to that of the group.

        The table is considered modified after this operation.

        row -- if row is < 0 'group' is inserted at the top of the table
               if row is >= GetNumberRows() or None 'group' is append to 
                    the end of the table.
               otherwise 'group' replaces row 'row'
        """

        # either append or replace
        if row is None or row >= self.GetNumberRows():
            self.clazz.AppendGroup(group)
        elif row < 0:
            self.clazz.InsertGroup(0, group)
        else:
            if row == 0:
                self.clazz.SetDefaultGroup(group)
            else:
                self.clazz.ReplaceGroup(row - 1, group)

        self.__Modified()

    def GetColLabelValue(self, col):
        """Return the label for the given column."""
        return self.__col_labels[col]

    def GetRowLabelValue(self, row):
        """Return the label for the given row."""

        if row == 0:
            return _("Default")
        else:
            group = self.clazz.GetGroup(row - 1)
            if isinstance(group, ClassGroupDefault):   return _("Default")
            if isinstance(group, ClassGroupSingleton): return _("Singleton")
            if isinstance(group, ClassGroupPattern):   return _("Pattern")
            if isinstance(group, ClassGroupRange):     return _("Range")
            if isinstance(group, ClassGroupMap):       return _("Map")

        assert False # shouldn't get here
        return ""

    def GetNumberRows(self):
        """Return the number of rows."""
        if self.clazz is None:
            return 0

        return self.clazz.GetNumGroups() + 1 # +1 for default group

    def GetNumberCols(self):
        """Return the number of columns."""
        return NUM_COLS

    def IsEmptyCell(self, row, col):
        """Determine if a cell is empty. This is always false."""
        return False

    def GetValue(self, row, col):
        """Return the object that is used to represent the given
           cell coordinates. This may not be a string."""
        return self.GetValueAsCustom(row, col, None)

    def SetValue(self, row, col, value):
        """Assign 'value' to the cell specified by 'row' and 'col'.

        The table is considered modified after this operation.
        """

        self.SetValueAsCustom(row, col, None, value)

    def GetValueAsCustom(self, row, col, typeName):
        """Return the object that is used to represent the given
           cell coordinates. This may not be a string.

        typeName -- unused, but needed to overload wxPyGridTableBase
        """

        if row == 0:
            group = self.clazz.GetDefaultGroup()
        else:
            group = self.clazz.GetGroup(row - 1)


        if col == COL_VISIBLE:
            return group.IsVisible()

        if col == COL_SYMBOL:
            return group.GetProperties()

        if col == COL_LABEL:
            return group.GetLabel()

        # col must be COL_VALUE
        assert col == COL_VALUE

        if isinstance(group, ClassGroupDefault):
            return _("DEFAULT")
        elif isinstance(group, ClassGroupSingleton):
            return group.GetValue()
        elif isinstance(group, ClassGroupPattern):
            return group.GetPattern()
        elif isinstance(group, ClassGroupRange):
            return group.GetRange()

        assert False # shouldn't get here
        return None

    def __ParseInput(self, value):
        """Try to determine what kind of input value is 
           (string, number, or range)

        Returns a tuple (type, data) where type is 0 if data is
        a singleton value, 1 if is a range or 2 if it is a pattern.
        """

        type = self.fieldType

        if type == FIELDTYPE_STRING:
            # Approach: if we can compile the value as an expression, 
            # make it a pattern, else a singleton.
            # This is quite crude, however I don't have a better idea:
            # How to distinct the singleton "Thuban" from the pattern "Thuban"?
            try:
                re.compile(value)
            except:
                return (0, value)
            else:
                return (2, value)
        elif type in (FIELDTYPE_INT, FIELDTYPE_DOUBLE):
            if type == FIELDTYPE_INT:
                # the float call allows the user to enter 1.0 for 1
                conv = lambda p: int(float(p))
            else:
                conv = float

            #
            # first try to take the input as a single number
            # if there's an exception try to break it into
            # a range. if there is an exception here, let it 
            # pass up to the calling function.
            #
            try:
                return (0, conv(value))
            except ValueError:
                return (1, Range(value))

        assert False  # shouldn't get here
        return (0,None)

    def SetValueAsCustom(self, row, col, typeName, value):
        """Set the cell specified by 'row' and 'col' to 'value'.

        If column represents the value column, the input is parsed
        to determine if a string, number, or range was entered.
        A new ClassGroup may be created if the type of data changes.

        The table is considered modified after this operation.

        typeName -- unused, but needed to overload wxPyGridTableBase
        """

        assert 0 <= col < self.GetNumberCols()
        assert 0 <= row < self.GetNumberRows()

        if row == 0:
            group = self.clazz.GetDefaultGroup()
        else:
            group = self.clazz.GetGroup(row - 1)

        mod = True # assume the data will change

        if col == COL_VISIBLE:
            group.SetVisible(value)
        elif col == COL_SYMBOL:
            group.SetProperties(value)
        elif col == COL_LABEL:
            group.SetLabel(value)
        elif col == COL_VALUE:
            if isinstance(group, ClassGroupDefault):
                # not allowed to modify the default value 
                pass
            elif isinstance(group, ClassGroupMap):
                # something special
                pass
            else: # SINGLETON, RANGE
                try:
                    dataInfo = self.__ParseInput(value)
                except ValueError:
                    # bad input, ignore the request
                    mod = False
                else:

                    changed = False
                    ngroup = group
                    props = group.GetProperties()

                    #
                    # try to update the values, which may include
                    # changing the underlying group type if the
                    # group was a singleton and a range was entered
                    #
                    if dataInfo[0] == 0:
                        if not isinstance(group, ClassGroupSingleton):
                            ngroup = ClassGroupSingleton(props = props)
                            changed = True
                        ngroup.SetValue(dataInfo[1])
                    elif dataInfo[0] == 1:
                        if not isinstance(group, ClassGroupRange):
                            ngroup = ClassGroupRange(props = props)
                            changed = True
                        ngroup.SetRange(dataInfo[1])
                    elif dataInfo[0] == 2:
                        if not isinstance(group, ClassGroupPattern):
                            ngroup = ClassGroupPattern(props = props)
                            changed = True
                        ngroup.SetPattern(dataInfo[1])
                    else:
                        assert False
                        pass

                    if changed:
                        ngroup.SetLabel(group.GetLabel())
                        self.SetClassGroup(row, ngroup)
        else:
            assert False # shouldn't be here
            pass

        if mod:
            self.__Modified()
            self.GetView().Refresh()

    def GetAttr(self, row, col, someExtraParameter):
        """Returns the cell attributes"""

        return self.__colAttr.get(col, grid.GridCellAttr()).Clone()

    def GetClassGroup(self, row):
        """Return the ClassGroup object representing row 'row'."""

        #return self.GetValueAsCustom(row, COL_SYMBOL, None)
        if row == 0:
            return self.clazz.GetDefaultGroup()
        else:
            return self.clazz.GetGroup(row - 1)

    def SetClassGroup(self, row, group):
        """Set the given row to properties of group."""
        self.__SetRow(row, group)
        self.GetView().Refresh()

    def __Modified(self, mod = True):
        """Adjust the modified flag.

        mod -- if -1 set the modified flag to False, otherwise perform
               an 'or' operation with the current value of the flag and
               'mod'
        """

        if mod == -1:
            self.modified = False
        else:
            self.modified = mod or self.modified

    def IsModified(self):
        """True if this table is considered modified."""
        return self.modified

    def DeleteRows(self, pos, numRows = 1):
        """Deletes 'numRows' beginning at row 'pos'. 

        The row representing the default group is not removed.

        The table is considered modified if any rows are removed.
        """

        assert pos >= 0
        old_len = self.GetNumberRows()
        for row in range(pos, pos - numRows, -1):
            group = self.GetClassGroup(row)
            if row != 0:
                self.clazz.RemoveGroup(row - 1)
                self.__Modified()

        if self.IsModified():
            self.__NotifyRowChanges(old_len, self.GetNumberRows())

    def AppendRows(self, numRows = 1):
        """Append 'numRows' empty rows to the end of the table.

        The table is considered modified if any rows are appended.
        """

        old_len = self.GetNumberRows()
        for i in range(numRows):
            np = ClassGroupSingleton()
            self.__SetRow(None, np)

        if self.IsModified():
            self.__NotifyRowChanges(old_len, self.GetNumberRows())


ID_PROPERTY_REVERT = 4002
ID_PROPERTY_ADD = 4003
ID_PROPERTY_GENCLASS = 4004
ID_PROPERTY_REMOVE = 4005
ID_PROPERTY_MOVEUP = 4006
ID_PROPERTY_MOVEDOWN = 4007
ID_PROPERTY_TRY = 4008
ID_PROPERTY_EDITSYM = 4009
ID_PROPERTY_SELECT = 4011
ID_PROPERTY_TITLE = 4012
ID_PROPERTY_FIELDTEXT = 4013

BTN_ADD = 0
BTN_EDIT = 1
BTN_GEN = 2
BTN_UP = 3
BTN_DOWN = 4
BTN_RM = 5

EB_LAYER_TITLE = 0
EB_SELECT_FIELD = 1
EB_GEN_CLASS = 2

class Classifier(LayerProperties):

    type2string = {None:             _("None"),
                   FIELDTYPE_STRING: _("Text"),
                   FIELDTYPE_INT:    _("Integer"),
                   FIELDTYPE_DOUBLE: _("Decimal")}

    def __init__(self, parent, name, layer, group = None):
        """Create a Properties/Classification dialog for a layer.
        The layer is part of map. If group is not None, select that
        group in the classification table.
        """

        LayerProperties.__init__(self, parent, name, layer)

        self.layer.Subscribe(LAYER_SHAPESTORE_REPLACED,
                             self.layer_shapestore_replaced)

        self.genDlg = None
        self.group = group

        LayerProperties.dialog_layout(self)

    def dialog_layout(self, panel, panelBox):

        if self.layer.HasClassification():

            self.fieldTypeText = wx.StaticText(panel, -1, "")

            self.originalClass = self.layer.GetClassification()
            self.originalClassField = self.layer.GetClassificationColumn()
            field = self.originalClassField
            fieldType = self.layer.GetFieldType(field)

            table = self.layer.ShapeStore().Table()
            #
            # make field choice box
            #
            self.fields = wx.Choice(panel, ID_PROPERTY_SELECT,)

            self.num_cols = table.NumColumns()
            # just assume the first field in case one hasn't been
            # specified in the file.
            self.__cur_field = 0

            self.fields.Append("<None>")

            if fieldType is None:
                self.fields.SetClientData(0, copy.deepcopy(self.originalClass))
            else:
                self.fields.SetClientData(0, None)

            for i in range(self.num_cols):
                name = table.Column(i).name
                self.fields.Append(name)

                if name == field:
                    self.__cur_field = i + 1
                    self.fields.SetClientData(i + 1,
                                              copy.deepcopy(self.originalClass))
                else:
                    self.fields.SetClientData(i + 1, None)

            button_gen = wx.Button(panel, ID_PROPERTY_GENCLASS,
                _("Generate Class"))
            button_add = wx.Button(panel, ID_PROPERTY_ADD,
                _("Add"))
            button_moveup = wx.Button(panel, ID_PROPERTY_MOVEUP,
                _("Move Up"))
            button_movedown = wx.Button(panel, ID_PROPERTY_MOVEDOWN,
                _("Move Down"))
            button_edit = wx.Button(panel, ID_PROPERTY_EDITSYM,
                _("Edit Symbol"))
            button_remove = wx.Button(panel, ID_PROPERTY_REMOVE,
                _("Remove"))

            self.classGrid = ClassGrid(panel, self)

            # calling __SelectField after creating the classGrid fills in the
            # grid with the correct information
            self.fields.SetSelection(self.__cur_field)
            self.__SelectField(self.__cur_field, group = self.group)


            classBox = wx.StaticBoxSizer(
                        wx.StaticBox(panel, -1, _("Classification")), wx.VERTICAL)


            sizer = wx.BoxSizer(wx.HORIZONTAL)
            sizer.Add(wx.StaticText(panel, ID_PROPERTY_FIELDTEXT, _("Field: ")),
                0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 4)
            sizer.Add(self.fields, 1, wx.GROW | wx.ALL, 4)

            classBox.Add(sizer, 0, wx.GROW, 4)

            classBox.Add(self.fieldTypeText, 0,
                        wx.GROW | wx.ALIGN_LEFT | wx.ALL | wx.ADJUST_MINSIZE, 4)

            controlBox = wx.BoxSizer(wx.HORIZONTAL)
            controlButtonBox = wx.BoxSizer(wx.VERTICAL)

            controlButtonBox.Add(button_gen, 0, wx.GROW|wx.ALL, 4)
            controlButtonBox.Add(button_add, 0, wx.GROW|wx.ALL, 4)
            controlButtonBox.Add(button_moveup, 0, wx.GROW|wx.ALL, 4)
            controlButtonBox.Add(button_movedown, 0, wx.GROW|wx.ALL, 4)
            controlButtonBox.Add(button_edit, 0, wx.GROW|wx.ALL, 4)
            controlButtonBox.Add( (60, 20), 0, wx.GROW|wx.ALL|wx.ALIGN_BOTTOM, 4)
            controlButtonBox.Add(button_remove, 0,
                                 wx.GROW|wx.ALL|wx.ALIGN_BOTTOM, 4)

            controlBox.Add(self.classGrid, 1, wx.GROW, 0)
            controlBox.Add(controlButtonBox, 0, wx.GROW, 10)

            classBox.Add(controlBox, 1, wx.GROW, 10)
            panelBox.Add(classBox, 1, wx.GROW, 0)


        self.Bind(wx.EVT_CHOICE, self._OnFieldSelect, id=ID_PROPERTY_SELECT)
        self.Bind(wx.EVT_BUTTON, self._OnAdd, id=ID_PROPERTY_ADD)
        self.Bind(wx.EVT_BUTTON, self._OnEditSymbol, id=ID_PROPERTY_EDITSYM)
        self.Bind(wx.EVT_BUTTON, self._OnRemove, id=ID_PROPERTY_REMOVE)
        self.Bind(wx.EVT_BUTTON, self._OnGenClass, id=ID_PROPERTY_GENCLASS)
        self.Bind(wx.EVT_BUTTON, self._OnMoveUp, id=ID_PROPERTY_MOVEUP)
        self.Bind(wx.EVT_BUTTON, self._OnMoveDown, id=ID_PROPERTY_MOVEDOWN)

    def unsubscribe_messages(self):
        """Unsubscribe from all messages."""
        LayerProperties.unsubscribe_messages(self)
        self.layer.Unsubscribe(LAYER_SHAPESTORE_REPLACED,
                               self.layer_shapestore_replaced)

    def layer_shapestore_replaced(self, *args):
        """Subscribed to the map's LAYER_SHAPESTORE_REPLACED message.
        Close self.
        """
        self.Close()

    def EditSymbol(self, row):
        """Open up a dialog where the user can select the properties 
        for a group.
        """
        table = self.classGrid.GetTable()
        prop = table.GetValueAsCustom(row, COL_SYMBOL, None)

        # get a new ClassGroupProperties object and copy the 
        # values over to our current object
        propDlg = SelectPropertiesDialog(self, prop, self.layer.ShapeType())

        self.Enable(False)
        if propDlg.ShowModal() == wx.ID_OK:
            new_prop = propDlg.GetClassGroupProperties()
            table.SetValueAsCustom(row, COL_SYMBOL, None, new_prop)
        self.Enable(True)
        propDlg.Destroy()

    def _SetClassification(self, clazz):
        """Called from the ClassGen dialog when a new classification has
        been created and should be set in the table.
        """
        # FIXME: This could be implemented using a message

        self.fields.SetClientData(self.__cur_field, clazz)
        self.classGrid.GetTable().SetClassification(clazz)

    def __BuildClassification(self, fieldIndex, copyClass=False, force=False):
        """Pack the classification setting into a Classification object.
        Returns (Classification, fieldName) where fieldName is the selected
        field in the table that the classification should be used with.
        """

#       numRows = self.classGrid.GetNumberRows()
#       assert numRows > 0  # there should always be a default row

        if fieldIndex == 0:
            fieldName = None
            fieldType = None
        else:
            fieldName = internal_from_wxstring(self.fields.GetString(fieldIndex))
            fieldType = self.layer.GetFieldType(fieldName)

        clazz = self.fields.GetClientData(fieldIndex)
        if clazz is None or self.classGrid.GetTable().IsModified() or force:
            clazz = self.classGrid.GetTable().GetClassification()
            if copyClass:
                clazz = copy.deepcopy(clazz)

        return clazz, fieldName

    def __SetGridTable(self, fieldIndex, group = None):
        """Set the table with the classification associated with the
        selected field at fieldIndex. Select the specified group
        if group is not None.
        """

        clazz = self.fields.GetClientData(fieldIndex)

        if clazz is None:
            clazz = Classification()
            clazz.SetDefaultGroup(
                ClassGroupDefault(
                    self.layer.GetClassification().
                               GetDefaultGroup().GetProperties()))

        fieldName = internal_from_wxstring(self.fields.GetString(fieldIndex))
        fieldType = self.layer.GetFieldType(fieldName)

        self.classGrid.CreateTable(clazz, fieldType,
                                   self.layer.ShapeType(), group)

    def __SetFieldTypeText(self, fieldIndex):
        """Set the field type string using the data type of the field
        at fieldIndex.
        """
        fieldName = internal_from_wxstring(self.fields.GetString(fieldIndex))
        fieldType = self.layer.GetFieldType(fieldName)

        assert Classifier.type2string.has_key(fieldType)

        text = Classifier.type2string[fieldType]

        self.fieldTypeText.SetLabel(_("Data Type: %s") % text)

    def __SelectField(self, newIndex, oldIndex = -1, group = None):
        """This method assumes that the current selection for the
        combo has already been set by a call to SetSelection().
        """

        assert oldIndex >= -1

        if oldIndex != -1:
            clazz, name = self.__BuildClassification(oldIndex, force = True)
            self.fields.SetClientData(oldIndex, clazz)

        self.__SetGridTable(newIndex, group)

        self.__EnableButtons(EB_SELECT_FIELD)

        self.__SetFieldTypeText(newIndex)

    def __SetTitle(self, title):
        """Set the title of the dialog."""
        if title != "":
            title = ": " + title

        self.SetTitle(_("Layer Properties") + title)

    def _OnEditSymbol(self, event):
        """Open up a dialog for the user to select group properties."""
        sel = self.classGrid.GetCurrentSelection()

        if len(sel) == 1:
            self.EditSymbol(sel[0])

    def _OnFieldSelect(self, event):
        index = self.fields.GetSelection()
        self.__SelectField(index, self.__cur_field)
        self.__cur_field = index

    def OnTry(self, event):
        """Put the data from the table into a new Classification and hand
           it to the layer.
        """

        if self.layer.HasClassification():
            clazz = self.fields.GetClientData(self.__cur_field)

            #
            # only build the classification if there wasn't one to
            # to begin with or it has been modified
            #
            self.classGrid.SaveEditControlValue()
            clazz, name = self.__BuildClassification(self.__cur_field, True)

            self.layer.SetClassificationColumn(name)
            self.layer.SetClassification(clazz)

        self.haveApplied = True

    def OnOK(self, event):
        self.OnTry(event)
        self.Close()

    def OnRevert(self, event):
        """The layer's current classification stays the same."""
        if self.haveApplied and self.layer.HasClassification():
            self.layer.SetClassificationColumn(self.originalClassField)
            self.layer.SetClassification(self.originalClass)

        #self.Close()

    def _OnAdd(self, event):
        self.classGrid.AppendRows()

    def _OnRemove(self, event):
        self.classGrid.DeleteSelectedRows()

    def _OnGenClass(self, event):
        """Open up a dialog for the user to generate classifications."""

        self.genDlg = ClassGenDialog(self, self.layer,
              internal_from_wxstring(self.fields.GetString(self.__cur_field)))

        self.Bind(wx.EVT_CLOSE, self._OnGenDialogClose, self.genDlg)

        self.__EnableButtons(EB_GEN_CLASS)

        self.genDlg.Show()

    def _OnGenDialogClose(self, event):
        """Reenable buttons after the generate classification 
        dialog is closed.
        """
        self.genDlg.Destroy()
        self.genDlg = None
        self.__EnableButtons(EB_GEN_CLASS)

    def _OnMoveUp(self, event):
        """When the user clicks MoveUp, try to move a group up one row."""
        sel = self.classGrid.GetCurrentSelection()

        if len(sel) == 1:
            i = sel[0]
            if i > 1:
                table = self.classGrid.GetTable()
                x = table.GetClassGroup(i - 1)
                y = table.GetClassGroup(i)
                table.SetClassGroup(i - 1, y)
                table.SetClassGroup(i, x)
                self.classGrid.ClearSelection()
                self.classGrid.SelectRow(i - 1)
                self.classGrid.MakeCellVisible(i - 1, 0)

    def _OnMoveDown(self, event):
        """When the user clicks MoveDown, try to move a group down one row."""
        sel = self.classGrid.GetCurrentSelection()

        if len(sel) == 1:
            i = sel[0]
            table = self.classGrid.GetTable()
            if 0 < i < table.GetNumberRows() - 1:
                x = table.GetClassGroup(i)
                y = table.GetClassGroup(i + 1)
                table.SetClassGroup(i, y)
                table.SetClassGroup(i + 1, x)
                self.classGrid.ClearSelection()
                self.classGrid.SelectRow(i + 1)
                self.classGrid.MakeCellVisible(i + 1, 0)

    def _OnTitleChanged(self, event):
        """Update the dialog title when the user changed the layer name."""
        obj = event.GetEventObject()

        self.layer.SetTitle(obj.GetValue())
        self.__SetTitle(self.layer.Title())

        self.__EnableButtons(EB_LAYER_TITLE)

    def __EnableButtons(self, case):
        """Helper method that enables/disables the appropriate buttons
        based on the case provided. Cases are constants beginning with EB_.
        """

        list = {wx.ID_OK                 : True,
                wx.ID_CANCEL             : True,
                ID_PROPERTY_ADD         : True,
                ID_PROPERTY_MOVEUP      : True,
                ID_PROPERTY_MOVEDOWN    : True,
                ID_PROPERTY_REMOVE      : True,
                ID_PROPERTY_SELECT      : True,
                ID_PROPERTY_FIELDTEXT   : True,
                ID_PROPERTY_GENCLASS    : True,
                ID_PROPERTY_EDITSYM     : True}

        if case == EB_LAYER_TITLE:
            if self.layer.Title() == "":
                list[wxID_OK] = False
                list[wxID_CANCEL] = False

        elif case == EB_SELECT_FIELD:
            if self.fields.GetSelection() == 0:
                list[ID_PROPERTY_GENCLASS] = False
                list[ID_PROPERTY_ADD] = False
                list[ID_PROPERTY_MOVEUP] = False
                list[ID_PROPERTY_MOVEDOWN] = False
                list[ID_PROPERTY_REMOVE] = False

        elif case == EB_GEN_CLASS:
            if self.genDlg is not None:
                list[ID_PROPERTY_SELECT] = False
                list[ID_PROPERTY_FIELDTEXT] = False
                list[ID_PROPERTY_GENCLASS] = False

        for id, enable in list.items():
            win = self.FindWindowById(id)
            if win:
                win.Enable(enable)

ID_SELPROP_SPINCTRL_LINEWIDTH = 4002
ID_SELPROP_PREVIEW = 4003
ID_SELPROP_STROKECLR = 4004
ID_SELPROP_FILLCLR = 4005
ID_SELPROP_STROKECLRTRANS = 4006
ID_SELPROP_FILLCLRTRANS = 4007
ID_SELPROP_SPINCTRL_SIZE = 4008

class SelectPropertiesDialog(wx.Dialog):
    """Dialog that allows the user to select group properties."""

    def __init__(self, parent, prop, shapeType):
        """Open the dialog with the initial prop properties and shapeType."""

        wx.Dialog.__init__(self, parent, -1, _("Select Properties"),
                          style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)

        self.prop = ClassGroupProperties(prop)

        topBox = wx.BoxSizer(wx.VERTICAL)

        itemBox = wx.BoxSizer(wx.HORIZONTAL)

        # preview box
        previewBox = wx.BoxSizer(wx.VERTICAL)
        previewBox.Add(wx.StaticText(self, -1, _("Preview:")),
            0, wx.ALIGN_LEFT | wx.ALL, 4)

        self.previewWin = ClassGroupPropertiesCtrl(
            self, ID_SELPROP_PREVIEW, self.prop, shapeType,
            (40, 40), wx.SIMPLE_BORDER)

        self.previewWin.AllowEdit(False)

        previewBox.Add(self.previewWin, 1, wx.GROW | wx.ALL, 4)

        itemBox.Add(previewBox, 1, wx.ALIGN_LEFT | wx.ALL | wx.GROW, 0)

        # control box
        ctrlBox = wx.BoxSizer(wx.VERTICAL)

        lineColorBox = wx.BoxSizer(wx.HORIZONTAL)
        button = wx.Button(self, ID_SELPROP_STROKECLR, _("Change Line Color"))
        button.SetFocus()
        lineColorBox.Add(button, 1, wx.ALL | wx.GROW, 4)
        self.Bind(wx.EVT_BUTTON, self._OnChangeLineColor, id=ID_SELPROP_STROKECLR)

        lineColorBox.Add(
            wx.Button(self, ID_SELPROP_STROKECLRTRANS, _("Transparent")),
            1, wx.ALL | wx.GROW, 4)
        self.Bind(wx.EVT_BUTTON, self._OnChangeLineColorTrans, \
                                   id=ID_SELPROP_STROKECLRTRANS)

        ctrlBox.Add(lineColorBox, 0,
                    wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.GROW, 4)

        if shapeType != SHAPETYPE_ARC:
            fillColorBox = wx.BoxSizer(wx.HORIZONTAL)
            fillColorBox.Add(
                wx.Button(self, ID_SELPROP_FILLCLR, _("Change Fill Color")),
                1, wx.ALL | wx.GROW, 4)
            self.Bind(wx.EVT_BUTTON, self._OnChangeFillColor, id=ID_SELPROP_FILLCLR)
            fillColorBox.Add(
                wx.Button(self, ID_SELPROP_FILLCLRTRANS, _("Transparent")),
                1, wx.ALL | wx.GROW, 4)
            self.Bind(wx.EVT_BUTTON, self._OnChangeFillColorTrans,\
                                        id=ID_SELPROP_FILLCLRTRANS)
            ctrlBox.Add(fillColorBox, 0,
                        wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.GROW, 4)

        # Line width selection
        spinBox = wx.BoxSizer(wx.HORIZONTAL)
        spinBox.Add(wx.StaticText(self, -1, _("Line Width: ")),
                0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 4)
        self.spinCtrl_linewidth = wx.SpinCtrl(self,
                                             ID_SELPROP_SPINCTRL_LINEWIDTH,
                                             min=1, max=10,
                                             value=str(prop.GetLineWidth()),
                                             initial=prop.GetLineWidth())

        self.Bind(wx.EVT_SPINCTRL, self._OnSpinLineWidth, \
                    id=ID_SELPROP_SPINCTRL_LINEWIDTH)

        spinBox.Add(self.spinCtrl_linewidth, 0, wx.ALIGN_LEFT | wx.ALL, 4)
        ctrlBox.Add(spinBox, 0, wx.ALIGN_RIGHT | wx.ALL, 0)

        # Size selection
        if shapeType == SHAPETYPE_POINT:
            spinBox = wx.BoxSizer(wx.HORIZONTAL)
            spinBox.Add(wx.StaticText(self, -1, _("Size: ")),
                        0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 4)
            self.spinCtrl_size = wx.SpinCtrl(self, ID_SELPROP_SPINCTRL_SIZE,
                                            min=1, max=100,
                                            value=str(prop.GetSize()),
                                            initial=prop.GetSize())

            self.Bind(wx.EVT_SPINCTRL, self._OnSpinSize, id=ID_SELPROP_SPINCTRL_SIZE)

            spinBox.Add(self.spinCtrl_size, 0, wx.ALIGN_LEFT | wx.ALL, 4)
            ctrlBox.Add(spinBox, 0, wx.ALIGN_RIGHT | wx.ALL, 0)


        itemBox.Add(ctrlBox, 0, wx.ALIGN_RIGHT | wx.ALL | wx.GROW, 0)
        topBox.Add(itemBox, 1, wx.ALIGN_LEFT | wx.ALL | wx.GROW, 0)

        #
        # Control buttons:
        #
        buttonBox = wx.BoxSizer(wx.HORIZONTAL)
        button_ok = wx.Button(self, wx.ID_OK, _("OK"))
        buttonBox.Add(button_ok, 0, wx.RIGHT|wx.EXPAND, 10)
        buttonBox.Add(wx.Button(self, wx.ID_CANCEL, _("Cancel")),
                      0, wx.RIGHT|wx.EXPAND, 10)
        topBox.Add(buttonBox, 0, wx.ALIGN_RIGHT|wx.BOTTOM|wx.TOP, 10)

        button_ok.SetDefault()

        #EVT_BUTTON(self, wxID_OK, self._OnOK)
        #EVT_BUTTON(self, ID_SELPROP_CANCEL, self._OnCancel)

        self.SetAutoLayout(True)
        self.SetSizer(topBox)
        topBox.Fit(self)
        topBox.SetSizeHints(self)

    def OnOK(self, event):
        self.EndModal(wx.ID_OK)

    def OnCancel(self, event):
        self.EndModal(wx.ID_CANCEL)

    def _OnSpinLineWidth(self, event):
        self.prop.SetLineWidth(self.spinCtrl_linewidth.GetValue())
        self.previewWin.Refresh()

    def _OnSpinSize(self, event):
        self.prop.SetSize(self.spinCtrl_size.GetValue())
        self.previewWin.Refresh()

    def __GetColor(self, cur):
        dialog = ColorDialog(self)
        dialog.SetColor(cur)

        ret = None
        if dialog.ShowModal() == wx.ID_OK:
            ret = dialog.GetColor()

        dialog.Destroy()

        return ret

    def _OnChangeLineColor(self, event):
        clr = self.__GetColor(self.prop.GetLineColor())
        if clr is not None:
            self.prop.SetLineColor(clr)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def _OnChangeLineColorTrans(self, event):
        self.prop.SetLineColor(Transparent)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def _OnChangeFillColor(self, event):
        clr = self.__GetColor(self.prop.GetFill())
        if clr is not None:
            self.prop.SetFill(clr)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def _OnChangeFillColorTrans(self, event):
        self.prop.SetFill(Transparent)
        self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer

    def GetClassGroupProperties(self):
        return self.prop


class ClassDataPreviewWindow(wx.Window):
    """A custom window that draws group properties using the correct shape."""

    def __init__(self, rect, prop, shapeType,
                       parent = None, id = -1, size = wx.DefaultSize):
        """Draws the appropriate shape as specified with shapeType using
        prop properities.
        """
        if parent is not None:
            wx.Window.__init__(self, parent, id, (0, 0), size)
            self.Bind(wx.EVT_PAINT, self._OnPaint)

        self.rect = rect

        self.prop = prop
        self.shapeType = shapeType
        self.previewer = ClassDataPreviewer()

    def GetProperties():
        return self.prop

    def _OnPaint(self, event):
        dc = wx.PaintDC(self)

        # XXX: this doesn't seem to be having an effect:
        dc.DestroyClippingRegion()

        if self.rect is None:
            w, h = self.GetSize()
            rect = wx.Rect(0, 0, w, h)
        else:
            rect = self.rect

        self.previewer.Draw(dc, rect, self.prop, self.shapeType)

class ClassDataPreviewer:
    """Class that actually draws a group property preview."""

    def Draw(self, dc, rect, prop, shapeType):
        """Draw the property.

        returns: (w, h) as adapted extend if the drawing size
        exceeded the given rect. This can only be the case
        for point symbols. If the symbol fits the given rect,
        None is returned.
        """

        assert dc is not None
        assert isinstance(prop, ClassGroupProperties)

        if rect is None:
            x = 0
            y = 0
            w = dc.GetSize().GetWidth()
            h = dc.GetSize().GetHeight()
        else:
            x = rect.GetX()
            y = rect.GetY()
            w = rect.GetWidth()
            h = rect.GetHeight()

        stroke = prop.GetLineColor()
        if stroke is Transparent:
            pen = wx.TRANSPARENT_PEN
        else:
            pen = wx.Pen(Color2wxColour(stroke),
                        prop.GetLineWidth(),
                        wx.SOLID)

        stroke = prop.GetFill()
        if stroke is Transparent:
            brush = wx.TRANSPARENT_BRUSH
        else:
            brush = wx.Brush(Color2wxColour(stroke), wx.SOLID)

        dc.SetPen(pen)
        dc.SetBrush(brush)

        if shapeType == SHAPETYPE_ARC:
            dc.DrawSpline([wx.Point(x, y + h),
                           wx.Point(x + w/2, y + h/4),
                           wx.Point(x + w/2, y + h/4*3),
                           wx.Point(x + w, y)])

        elif shapeType == SHAPETYPE_POINT:

            dc.DrawCircle(x + w/2, y + h/2, prop.GetSize())
            circle_size =  prop.GetSize() * 2 + prop.GetLineWidth() * 2
            new_h = h
            new_w = w
            if h < circle_size: new_h = circle_size
            if w < circle_size: new_w = circle_size
            if new_h > h or new_w > w:
                return (new_w, new_h)

        elif shapeType == SHAPETYPE_POLYGON:
            dc.DrawRectangle(x, y, w, h)

        return None

class ClassRenderer(grid.PyGridCellRenderer):
    """A wrapper class that can be used to draw group properties in a
    grid table.
    """

    def __init__(self, shapeType):
        grid.PyGridCellRenderer.__init__(self)
        self.shapeType = shapeType
        self.previewer = ClassDataPreviewer()

    def Draw(self, grid, attr, dc, rect, row, col, isSelected):
        data = grid.GetTable().GetClassGroup(row)

        dc.SetClippingRegion(rect.GetX(), rect.GetY(),
                             rect.GetWidth(), rect.GetHeight())
        dc.SetPen(wx.Pen(wx.LIGHT_GREY))
        dc.SetBrush(wx.Brush(wx.LIGHT_GREY, wx.SOLID))
        dc.DrawRectangle(rect.GetX(), rect.GetY(),
                         rect.GetWidth(), rect.GetHeight())

        if not isinstance(data, ClassGroupMap):
            new_size = self.previewer.Draw(dc, rect, data.GetProperties(),
                                           self.shapeType)
            if new_size is not None:
                (new_w, new_h) = new_size
                grid.SetRowSize(row, new_h)
                grid.SetColSize(col, new_h)
                grid.ForceRefresh()

                # now that we know the height, redraw everything
                rect.SetHeight(new_h)
                rect.SetWidth(new_w)
                dc.DestroyClippingRegion()
                dc.SetClippingRegion(rect.GetX(), rect.GetY(),
                                     rect.GetWidth(), rect.GetHeight())
                dc.SetPen(wx.Pen(wx.LIGHT_GREY))
                dc.SetBrush(wx.Brush(wx.LIGHT_GREY, wx.SOLID))
                dc.DrawRectangle(rect.GetX(), rect.GetY(),
                                 rect.GetWidth(), rect.GetHeight())
                self.previewer.Draw(dc, rect, data.GetProperties(),
                                    self.shapeType)

        if isSelected:
            dc.SetPen(wx.Pen(wx.BLACK, 1, wx.SOLID))
            dc.SetBrush(wx.TRANSPARENT_BRUSH)

            dc.DrawRectangle(rect.GetX(), rect.GetY(),
                             rect.GetWidth(), rect.GetHeight())

        dc.DestroyClippingRegion()


class ClassGroupPropertiesCtrl(wx.Control):
    """A custom window and control that draw a preview of group properties
    and can open a dialog to modify the properties if the user double-clicks
    it.
    """

    def __init__(self, parent, id, props, shapeType,
                 size = wx.DefaultSize, style = 0):
        wx.Control.__init__(self, parent, id, size = size, style = style)

        self.parent = parent

        self.SetProperties(props)
        self.SetShapeType(shapeType)
        self.AllowEdit(True)

        self.Bind(wx.EVT_PAINT, self._OnPaint)
        self.Bind(wx.EVT_LEFT_DCLICK, self._OnLeftDClick)

        self.previewer = ClassDataPreviewer()

    def _OnPaint(self, event):
        dc = wx.PaintDC(self)

        # XXX: this doesn't seem to be having an effect:
        dc.DestroyClippingRegion()

        w, h = self.GetClientSize()

        self.previewer.Draw(dc,
                            wx.Rect(0, 0, w, h),
                            self.GetProperties(),
                            self.GetShapeType())


    def GetProperties(self):
        return self.props

    def SetProperties(self, props):
        self.props = props
        self.Refresh()

    def GetShapeType(self):
        return self.shapeType

    def SetShapeType(self, shapeType):
        self.shapeType = shapeType
        self.Refresh()

    def AllowEdit(self, allow):
        """Allow/Disallow double-clicking on the control."""
        self.allowEdit = allow

    def DoEdit(self):
        """Open the properties selector dialog."""

        if not self.allowEdit: return

        propDlg = SelectPropertiesDialog(self.parent,
                                         self.GetProperties(),
                                         self.GetShapeType())

        if propDlg.ShowModal() == wx.ID_OK:
            new_prop = propDlg.GetClassGroupProperties()
            self.SetProperties(new_prop)
            self.Refresh()

        propDlg.Destroy()

    def _OnLeftDClick(self, event):
        self.DoEdit()

