#----------------------------------------------------------------------
# Name:        sized_controls.py
# Purpose:     Implements default, HIG-compliant sizers under the hood
#              and provides a simple interface for customizing those sizers.
#
# Author:      Kevin Ollivier
#
# Created:     26-May-2006
# Copyright:   (c) 2006 Kevin Ollivier
# Licence:     wxWindows license
#----------------------------------------------------------------------

import wx
import wx.lib.scrolledpanel as sp

# For HIG info: links to all the HIGs can be found here: 
# http://en.wikipedia.org/wiki/Human_Interface_Guidelines


# useful defines for sizer prop values

halign = {  "left": wx.ALIGN_LEFT,
            "center": wx.ALIGN_CENTER_HORIZONTAL,
            "centre": wx.ALIGN_CENTRE_HORIZONTAL,
            "right": wx.ALIGN_RIGHT,
         }
        
valign = {  "top": wx.ALIGN_TOP,
            "bottom": wx.ALIGN_BOTTOM,
            "center": wx.ALIGN_CENTER_VERTICAL,
            "centre": wx.ALIGN_CENTRE_VERTICAL,
         }
         
align = {   "center": wx.ALIGN_CENTER,
            "centre": wx.ALIGN_CENTRE, 
        }
        
border = {  "left": wx.LEFT,
            "right": wx.RIGHT,
            "top": wx.TOP,
            "bottom": wx.BOTTOM,
            "all": wx.ALL,
         }
           
minsize = {   "fixed":    wx.FIXED_MINSIZE,
          }
                
misc_flags = {   "expand": wx.EXPAND, }


# My attempt at creating a more intuitive replacement for nesting box sizers
class TableSizer(wx.PySizer):
    def __init__(self, rows=0, cols=0):
        wx.PySizer.__init__(self)
        self.rows = rows
        self.cols = cols
        self.fixed_width = 0
        self.fixed_height = 0
        self.hgrow = 0
        self.vgrow = 0
        
        self.row_widths = []
        self.col_heights = []
        
        # allow us to use 'old-style' proportions when emulating box sizers
        self.isHorizontal = (self.rows == 1 and self.cols == 0)
        self.isVertical = (self.cols == 1 and self.rows == 0)
        
    def CalcNumRowsCols(self):
        numrows = self.rows
        numcols = self.cols
        numchild = len(self.GetChildren())
        
        if numrows == 0 and numcols == 0:
            return 0, 0
        
        if numrows == 0:
            rows, mod = divmod(numchild, self.cols)
            if mod > 0:
                rows += 1
            numrows = rows
            
        if numcols == 0:
            cols, mod = divmod(numchild, self.rows)
            if mod > 0:
                cols += 1
            numcols = cols
            
        return numrows, numcols
        
    def CalcMin(self):
        numrows, numcols = self.CalcNumRowsCols() 
        numchild = len(self.GetChildren())
        
        if numchild == 0:
            return wx.Size(10, 10)
            
        if numrows == 0 and numcols == 0:
            print "TableSizer must have the number of rows or columns set. Cannot continue."
            return wx.Size(10, 10)
       
        self.row_widths = [0 for x in range(0, numrows)]
        self.col_heights = [0 for x in range(0, numcols)]
        currentRow = 0
        currentCol = 0
        counter = 0
        self.hgrow = 0
        self.vgrow = 0

        # get the max row width and max column height
        for item in self.GetChildren():            
            if self.cols != 0:
                currentRow, currentCol = divmod(counter, numcols)
            else:
                currentCol, currentRow = divmod(counter, numrows)
            
            if item.IsShown():
                width, height = item.CalcMin()
                
                if self.isVertical and item.GetProportion() > 0:
                    self.hgrow += item.GetProportion()
                elif self.isHorizontal and item.GetProportion() > 0:
                    self.vgrow += item.GetProportion()
                
                if width > self.row_widths[currentRow]:
                    self.row_widths[currentRow] = width
                
                if height > self.col_heights[currentCol]:
                    self.col_heights[currentCol] = height
            
            counter += 1
                
        minwidth = 0
        for row_width in self.row_widths:
            minwidth += row_width
            
        minheight = 0
        for col_height in self.col_heights:
            minheight += col_height
                
        self.fixed_width = minwidth
        self.fixed_height = minheight
        
        return wx.Size(minwidth, minheight)
        
    def RecalcSizes(self):
        numrows, numcols = self.CalcNumRowsCols()
        numchild = len(self.GetChildren())
        
        if numchild == 0:
            return
        currentRow = 0
        currentCol = 0
        counter = 0
        
        print "cols %d, rows %d" % (self.cols, self.rows)
        print "fixed_height %d, fixed_width %d" % (self.fixed_height, self.fixed_width)
        #print "self.GetSize() = " + `self.GetSize()`
        
        row_widths = [0 for x in range(0, numrows)]
        col_heights = [0 for x in range(0, numcols)]
        item_sizes = [0 for x in range(0, len(self.GetChildren()))]
        grow_sizes = [0 for x in range(0, len(self.GetChildren()))]
        
        curHPos = 0
        curVPos = 0
        curCol = 0
        curRow = 0
        # first, we set sizes for all children, and while doing so, calc
        # the maximum row heights and col widths. Then, afterwards we handle
        # the positioning of the controls                
        
        for item in self.GetChildren():
            if self.cols != 0:
                currentRow, currentCol = divmod(counter, numcols)
            else:
                currentCol, currentRow = divmod(counter, numrows)
            if item.IsShown():
                item_minsize = item.GetMinSizeWithBorder()
                width = item_minsize[0]
                height = item_minsize[1]
                
                print "row_height %d, row_width %d" % (self.col_heights[currentCol], self.row_widths[currentRow])
                growable_width = (self.GetSize()[0]) - width
                growable_height = (self.GetSize()[1]) - height
                
                #if not self.isVertical and not self.isHorizontal:
                #    growable_width = self.GetSize()[0] - self.row_widths[currentRow]
                #    growable_height = self.GetSize()[1] - self.col_heights[currentCol]
                
                #print "grow_height %d, grow_width %d" % (growable_height, growable_width)
                
                item_vgrow = 0
                item_hgrow = 0
                # support wx.EXPAND for box sizers to be compatible
                if item.GetFlag() & wx.EXPAND:
                    if self.isVertical:
                        if self.hgrow > 0 and item.GetProportion() > 0:
                            item_hgrow = (growable_width * item.GetProportion()) / self.hgrow
                        item_vgrow = growable_height 

                    elif self.isHorizontal: 
                        if self.vgrow > 0 and item.GetProportion() > 0:
                            item_vgrow = (growable_height * item.GetProportion()) / self.vgrow
                        item_hgrow = growable_width
                        
                if growable_width > 0 and item.GetHGrow() > 0:
                    item_hgrow = (growable_width * item.GetHGrow()) / 100
                    print "hgrow = %d" % (item_hgrow)

                if growable_height > 0 and item.GetVGrow() > 0:
                    item_vgrow = (growable_height * item.GetVGrow()) / 100
                    print "vgrow = %d" % (item_vgrow)
                
                grow_size = wx.Size(item_hgrow, item_vgrow)
                size = item_minsize #wx.Size(item_minsize[0] + item_hgrow, item_minsize[1] + item_vgrow) 
                if size[0] + grow_size[0] > row_widths[currentRow]:
                    row_widths[currentRow] = size[0] + grow_size[0]
                if size[1] + grow_size[1] > col_heights[currentCol]:
                    col_heights[currentCol] = size[1] + grow_size[1] 
                
                grow_sizes[counter] = grow_size
                item_sizes[counter] = size
            
            counter += 1
        
        counter = 0
        for item in self.GetChildren():
            if self.cols != 0:
                currentRow, currentCol = divmod(counter, numcols)
            else:
                currentCol, currentRow = divmod(counter, numrows)
            
            itempos = self.GetPosition()
            if item.IsShown():
                rowstart = itempos[0] 
                for row in range(0, currentRow):
                    rowstart += row_widths[row]
                    
                colstart = itempos[1]
                for col in range(0, currentCol):
                    #print "numcols = %d, currentCol = %d, col = %d" % (numcols, currentCol, col)
                    colstart += col_heights[col]
                    
                itempos[0] += rowstart
                itempos[1] += colstart
                
                if item.GetFlag() & wx.ALIGN_RIGHT:
                    itempos[0] += (row_widths[currentRow] - item_sizes[counter][0])
                elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL):
                    itempos[0] += (row_widths[currentRow] - item_sizes[counter][0]) / 2
                
                if item.GetFlag() & wx.ALIGN_BOTTOM:
                    itempos[1] += (col_heights[currentCol] - item_sizes[counter][1])
                elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL):
                    itempos[1] += (col_heights[currentCol] - item_sizes[counter][1]) / 2
                
                hgrowth =  (grow_sizes[counter][0] - itempos[0])
                if hgrowth > 0:
                    item_sizes[counter][0] += hgrowth
                
                vgrowth =  (grow_sizes[counter][1] - itempos[1])
                if vgrowth > 0:
                    item_sizes[counter][1] += vgrowth
                #item_sizes[counter][1] -= itempos[1]
                item.SetDimension(itempos, item_sizes[counter])
                    
            counter += 1

def GetDefaultBorder(self):
    border = 4
    if wx.Platform == "__WXMAC__":
        border = 6 
    elif wx.Platform == "__WXMSW__":
        # MSW HIGs use dialog units, not pixels
        pnt = self.ConvertDialogPointToPixels(wx.Point(4, 4))
        border = pnt[0] // 2
    elif wx.Platform == "__WXGTK__":
        border = 3 

    return border

def SetDefaultSizerProps(self):
    item = self.GetParent().GetSizer().GetItem(self)    
    item.SetProportion(0)
    item.SetFlag(wx.ALL)
    item.SetBorder(self.GetDefaultHIGBorder())
        
def GetSizerProps(self):
    """
    Returns a dictionary of prop name + value
    """

    props = {}
    item = self.GetParent().GetSizer().GetItem(self)
    if item is None:
        return None
    
    props['proportion'] = item.GetProportion()
    flags = item.GetFlag()

    if flags & border['all'] == border['all']:
        props['border'] = (['all'], item.GetBorder())
    else:
        borders = []
        for key in border:
            if flags & border[key]:
                borders.append(key)      
            
        props['border'] = (borders, item.GetBorder())
    
    if flags & align['center'] == align['center']:
        props['align'] = 'center'
    else:
        for key in halign:
            if flags & halign[key]:
                props['halign'] = key

        for key in valign:
            if flags & valign[key]:
                props['valign'] = key
    
    for key in minsize:
        if flags & minsize[key]:
            props['minsize'] = key 
    
    for key in misc_flags:
        if flags & misc_flags[key]:
            props[key] = "true"
            
    return props

def SetSizerProp(self, prop, value):
    """
    Sets a sizer property
    
    :param prop: valid strings are "proportion", "hgrow", "vgrow",
        "align", "halign", "valign", "border", "minsize" and "expand"
    :param value: corresponding value for the prop
    """
    
    lprop = prop.lower()
    sizer = self.GetParent().GetSizer()
    item = sizer.GetItem(self) 
    flag = item.GetFlag()
    if lprop == "proportion":
        item.SetProportion(int(value))
    elif lprop == "hgrow":
        item.SetHGrow(int(value))
    elif lprop == "vgrow":
        item.SetVGrow(int(value))
    elif lprop == "align":
        flag = flag | align[value]
    elif lprop == "halign":
        flag = flag | halign[value]
    elif lprop == "valign":
        flag = flag | valign[value]
    # elif lprop == "border":
    #     # this arg takes a tuple (dir, pixels)
    #     dirs, amount = value
    #     if dirs == "all":
    #         dirs = ["all"]
    #     for dir in dirs:
    #         flag = flag | border[dir]
    #     item.SetBorder(amount)
    elif lprop == "border":
        # this arg takes a tuple (dir, pixels)
        dirs, amount = value
        if dirs == "all":
            dirs = ["all"]
        else:
            flag &= ~(wx.ALL)
        for dir in dirs:
            flag = flag | border[dir]
        item.SetBorder(amount)
    elif lprop == "minsize":
        flag = flag | minsize[value]
    elif lprop in misc_flags:
        if not value or str(value) == "" or str(value).lower() == "false":
            flag = flag &~ misc_flags[lprop]
        else:
            flag = flag | misc_flags[lprop]
    
    # auto-adjust growable rows/columns if expand or proportion is set
    # on a sizer item in a FlexGridSizer
    if lprop in ["expand", "proportion"] and isinstance(sizer, wx.FlexGridSizer):
        cols = sizer.GetCols()
        rows = sizer.GetRows()
        # FIXME: I'd like to get the item index in the sizer instead, but
        # doing sizer.GetChildren.index(item) always gives an error
        itemnum = self.GetParent().GetChildren().index(self)
                
        col = 0
        row = 0
        if cols == 0:
            col, row = divmod( itemnum, rows )
        else:
            row, col = divmod( itemnum, cols )
        
        if lprop == "expand" and not sizer.IsColGrowable(col):
            sizer.AddGrowableCol(col)
        elif lprop == "proportion" and int(value) != 0 and not sizer.IsRowGrowable(row):
            sizer.AddGrowableRow(row)

    item.SetFlag(flag)

def SetSizerProps(self, props={}, **kwargs):
    """
    Allows to set multiple sizer properties

    :param props: a dictionary of prop name + value
    :param kwargs: key words can be used for properties, e.g. expand=True
    """

    allprops = {}
    allprops.update(props)
    allprops.update(kwargs)
    
    for prop in allprops:
        self.SetSizerProp(prop, allprops[prop])
        
def GetDialogBorder(self):
    border = 6
    if wx.Platform == "__WXMAC__" or wx.Platform == "__WXGTK__":
        border = 12
    elif wx.Platform == "__WXMSW__":
        pnt = self.ConvertDialogPointToPixels(wx.Point(7, 7))
        border = pnt[0]
    
    return border

def SetHGrow(self, proportion):
    data = self.GetUserData()
    if "HGrow" in data:
        data["HGrow"] = proportion
        self.SetUserData(data) 
    
def GetHGrow(self):
    if self.GetUserData() and "HGrow" in self.GetUserData():
        return self.GetUserData()["HGrow"]
    else:
        return 0
        
def SetVGrow(self, proportion):
    data = self.GetUserData()
    if "VGrow" in data:
        data["VGrow"] = proportion
        self.SetUserData(data) 
    
def GetVGrow(self):
    if self.GetUserData() and "VGrow" in self.GetUserData():
        return self.GetUserData()["VGrow"]
    else:
        return 0

def GetDefaultPanelBorder(self):
    # child controls will handle their borders, so don't pad the panel.
    return 0

# Why, Python?! Why do you make it so easy?! ;-)    
wx.Dialog.GetDialogBorder = GetDialogBorder
wx.Panel.GetDefaultHIGBorder = GetDefaultPanelBorder
wx.Notebook.GetDefaultHIGBorder = GetDefaultPanelBorder
wx.SplitterWindow.GetDefaultHIGBorder = GetDefaultPanelBorder

wx.Window.GetDefaultHIGBorder = GetDefaultBorder
wx.Window.SetDefaultSizerProps = SetDefaultSizerProps
wx.Window.SetSizerProp = SetSizerProp
wx.Window.SetSizerProps = SetSizerProps
wx.Window.GetSizerProps = GetSizerProps

wx.SizerItem.SetHGrow = SetHGrow
wx.SizerItem.GetHGrow = GetHGrow
wx.SizerItem.SetVGrow = SetVGrow
wx.SizerItem.GetVGrow = GetVGrow


class SizedParent:
    def AddChild(self, child):
        # Note: The wx.LogNull is used here to suppress a log message
        # on wxMSW that happens because when AddChild is called the
        # widget's hwnd hasn't been set yet, so the GetWindowRect that
        # happens as a result of sizer.Add (in wxSizerItem::SetWindow)
        # fails.  A better fix would be to defer this code somehow
        # until after the child widget is fully constructed.
        sizer = self.GetSizer()
        nolog = wx.LogNull()
        item = sizer.Add(child)
        del nolog
        item.SetUserData({"HGrow":0, "VGrow":0})
        
        # Note: One problem is that the child class given to AddChild
        # is the underlying wxWidgets control, not its Python subclass. So if 
        # you derive your own class, and override that class' GetDefaultBorder(), 
        # etc. methods, it will have no effect.  
        child.SetDefaultSizerProps()
        
    def GetSizerType(self):
        return self.sizerType
    
    def SetSizerType(self, type, options={}):
        """
        Sets the sizer type and automatically re-assign any children
        to it.
        
        :param type: sizer type, valid values are "horizontal", "vertical",
            "form", "table" and "grid"
        :param options: dictionary of options depending on type
        """

        sizer = None
        self.sizerType = type
        if type == "horizontal":
            sizer = wx.BoxSizer(wx.HORIZONTAL) # TableSizer(0, 1) 
        
        elif type == "vertical":
            sizer = wx.BoxSizer(wx.VERTICAL) # TableSizer(1, 0)
        
        elif type == "form":
            #sizer = TableSizer(2, 0)
            sizer = wx.FlexGridSizer(0, 2, 0, 0)
            #sizer.AddGrowableCol(1)
            
        elif type == "table":
            rows = cols = 0
            if options.has_key('rows'):
                rows = int(options['rows'])

            if options.has_key('cols'):
                cols = int(options['cols'])
        
            sizer = TableSizer(rows, cols)
        
        elif type == "grid":
            sizer = wx.FlexGridSizer(0, 0, 0, 0)
            if options.has_key('rows'):
                sizer.SetRows(int(options['rows']))
            else:
                sizer.SetRows(0)
            if options.has_key('cols'):
                sizer.SetCols(int(options['cols']))
            else:
                sizer.SetCols(0)
    
            if options.has_key('growable_row'):
                row, proportion = options['growable_row']
                sizer.SetGrowableRow(row, proportion)
            
            if options.has_key('growable_col'):
                col, proportion = options['growable_col']
                sizer.SetGrowableCol(col, proportion)
            
            if options.has_key('hgap'):
                sizer.SetHGap(options['hgap'])
                
            if options.has_key('vgap'):
                sizer.SetVGap(options['vgap'])
        if sizer:
            self._SetNewSizer(sizer)
                
    def _DetachFromSizer(self, sizer):
        props = {}
        for child in self.GetChildren():
            # On the Mac the scrollbars and corner gripper of a
            # ScrolledWindow will be in the list of children, but
            # should not be managed by a sizer.  So if there is a
            # child that is not in a sizer make sure we don't track
            # info for it nor add it to the next sizer.
            csp = child.GetSizerProps()
            if csp is not None:
                props[child.GetId()] = csp
                self.GetSizer().Detach(child)

        return props

    def _AddToNewSizer(self, sizer, props):
        for child in self.GetChildren():
            csp = props.get(child.GetId(), None)
            # See Mac comment above.
            if csp is not None:
                self.GetSizer().Add(child)
                child.SetSizerProps(csp)


class SizedPanel(wx.PyPanel, SizedParent):
    def __init__(self, *args, **kwargs):
        """
        A sized panel
        
        Controls added to it will automatically be added to its sizer.
        
        Usage:
        'self' is a SizedPanel instance
        
        self.SetSizerType("horizontal")
        
        b1 = wx.Button(self, wx.ID_ANY)
        t1 = wx.TextCtrl(self, -1)
        t1.SetSizerProps(expand=True)
        """

        wx.PyPanel.__init__(self, *args, **kwargs)
        sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0)
        self.SetSizer(sizer)
        self.sizerType = "vertical"

    def AddChild(self, child):
        """
        Called automatically by wx, do not call it from user code
        """

        if wx.VERSION < (2,8):
            wx.PyPanel.base_AddChild(self, child)
        else:
            wx.PyPanel.AddChild(self, child)

        SizedParent.AddChild(self, child)

    def _SetNewSizer(self, sizer):
        props = self._DetachFromSizer(sizer)
        wx.PyPanel.SetSizer(self, sizer)
        self._AddToNewSizer(sizer, props)


class SizedScrolledPanel(sp.ScrolledPanel, SizedParent):
    def __init__(self, *args, **kwargs):
        """A sized scrolled panel
        
        Controls added to it will automatically be added to its sizer.
        
        Usage:
        'self' is a SizedScrolledPanel instance
        
        self.SetSizerType("horizontal")
        
        b1 = wx.Button(self, wx.ID_ANY)
        t1 = wx.TextCtrl(self, -1)
        t1.SetSizerProps(expand=True)
        """

        sp.ScrolledPanel.__init__(self, *args, **kwargs)
        sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0)
        self.SetSizer(sizer)
        self.sizerType = "vertical"
        self.SetupScrolling()

    def AddChild(self, child):
        """
        Called automatically by wx, should not be called from user code
        """

        if wx.VERSION < (2,8):
            sp.ScrolledPanel.base_AddChild(self, child)
        else:
            sp.ScrolledPanel.AddChild(self, child)

        SizedParent.AddChild(self, child)

    def _SetNewSizer(self, sizer):
        props = self._DetachFromSizer(sizer)
        sp.ScrolledPanel.SetSizer(self, sizer)
        self._AddToNewSizer(sizer, props)


class SizedDialog(wx.Dialog):
    def __init__(self, *args, **kwargs):    
        """A sized dialog
        
        Controls added to its content pane will automatically be added to 
        the panes sizer.
        
        Usage:
        'self' is a SizedDialog instance
        
        pane = self.GetContentsPane()
        pane.SetSizerType("horizontal")
        
        b1 = wx.Button(pane, wx.ID_ANY)
        t1 = wx.TextCtrl(pane, wx.ID_ANY)
        t1.SetSizerProps(expand=True)
        """

        wx.Dialog.__init__(self, *args, **kwargs)
        
        self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY)
        
        self.borderLen = 12
        self.mainPanel = SizedPanel(self, -1)
        
        mysizer = wx.BoxSizer(wx.VERTICAL)
        mysizer.Add(self.mainPanel, 1, wx.EXPAND | wx.ALL, self.GetDialogBorder())
        self.SetSizer(mysizer)
        
        self.SetAutoLayout(True)
        
    def GetContentsPane(self):
        """
        Return the pane to add controls too
        """
        return self.mainPanel
        
    def SetButtonSizer(self, sizer):
        self.GetSizer().Add(sizer, 0, wx.EXPAND | wx.BOTTOM | wx.RIGHT, self.GetDialogBorder())
        
        # Temporary hack to fix button ordering problems.
        cancel = wx.FindWindowById(wx.ID_CANCEL, parent=self)
        no = wx.FindWindowById(wx.ID_NO, parent=self)
        if no and cancel:
            cancel.MoveAfterInTabOrder(no)
            
class SizedFrame(wx.Frame):
    def __init__(self, *args, **kwargs):    
        """
        A sized frame
        
        Controls added to its content pane will automatically be added to 
        the panes sizer.
        
        Usage:
        'self' is a SizedFrame instance
        
        pane = self.GetContentsPane()
        pane.SetSizerType("horizontal")
        
        b1 = wx.Button(pane, wx.ID_ANY)
        t1 = wx.TextCtrl(pane, -1)
        t1.SetSizerProps(expand=True)
        """
        wx.Frame.__init__(self, *args, **kwargs)
        
        self.borderLen = 12
        # this probably isn't needed, but I thought it would help to make it consistent
        # with SizedDialog, and creating a panel to hold things is often good practice.
        self.mainPanel = SizedPanel(self, -1)
        
        mysizer = wx.BoxSizer(wx.VERTICAL)
        mysizer.Add(self.mainPanel, 1, wx.EXPAND)
        self.SetSizer(mysizer)
        
        self.SetAutoLayout(True)
        
    def GetContentsPane(self):
        """
        Return the pane to add controls too
        """
        return self.mainPanel
