# A GUI controller class library for keyframe UI
# 
# v1.0 (Countd360)
# - first release
 
from UnityEngine import Quaternion, Vector3, Vector2, Color, Color32, Rect, GUI, GUIStyle
from UnityEngine import Time, Screen, Input, KeyCode, Event, EventType
from UnityEngine import Texture2D, TextureFormat, TextAnchor, RectOffset, FontStyle, TextClipping
from System import String, Array, Action
from vngameengine import get_engine_id
import copy
import math

# util func
old_engine_color_adjust_power = 0.444444
def unity_adjust_color(color):
    if get_engine_id() == "neov2":
        return color
    else:
        r = math.pow(color.r, old_engine_color_adjust_power)
        g = math.pow(color.g, old_engine_color_adjust_power)
        b = math.pow(color.b, old_engine_color_adjust_power)
        return Color(r, g, b, color.a)

def color_to_color32(color):
    return Color32(round(color.r * 255), round(color.g * 255), round(color.b * 255), round(color.a * 255))

def color32_to_color(color32):
    return Color(color32.r / 255.0, color32.g / 255.0, color32.b / 255.0, color32.a / 255.0)

# Combo List Box
# A list input can select one from dropdown list
class ComboListBox():
    def __init__(self, boxRect, maxDropHeight, dropList = (), selIdx = -1):
        def smallTexture(color):
            texture = Texture2D(2, 2, TextureFormat.RGBA32, False)
            texture.SetPixel(0, 0, color)
            texture.SetPixel(0, 1, color)
            texture.SetPixel(1, 0, color)
            texture.SetPixel(1, 1, color)
            texture.Apply()
            return texture

        self.dropBtnWidth = 18
        self.listItemHeight = 20
        # data
        self.dropList = list(dropList)
        self.selectIndex(selIdx)
        # callback
        self.onSelectChangeCallback = None
        # GUI
        self.resize(boxRect, maxDropHeight)
        # runtime
        self.expanded = False
        self.dropScrollPos = Vector2.zero
        # styles
        self.bgStyle = GUIStyle("box") 
        #self.bgStyle = GUIStyle("textField")
        self.labelStyle = GUIStyle("label")
        self.labelStyle.padding = RectOffset(4, 4, 0, 0)
        self.labelStyle.alignment = TextAnchor.MiddleLeft
        self.labelStyle.wordWrap = False
        self.listItemStyle = GUIStyle("label")
        self.listItemStyle.padding = RectOffset(2, 2, 0, 0)
        self.listItemStyle.alignment = TextAnchor.MiddleLeft
        self.listItemStyle.hover.background = smallTexture(Color.green)

    def resize(self, boxRect, maxDropHeight):
        self.boxRect = boxRect
        self.dropDown = maxDropHeight >= 0
        self.arrowText = u"\u25BC" if self.dropDown else u"\u25B2"
        self.maxDropHeight = abs(maxDropHeight)

        self.textRect = Rect(boxRect.x, boxRect.y, boxRect.width - self.dropBtnWidth, boxRect.height)
        self.dropBtnRect = Rect(self.textRect.xMax, boxRect.y, self.dropBtnWidth, boxRect.height)

    def importListItem(self, itemName, asCurrent = False):
        self.dropList.append(itemName)
        if asCurrent:
            self.selectIndex(len(self.dropList) - 1)

    def selectIndex(self, index):
        if 0 <= index < len(self.dropList):
            self.curSelIdx = index
            self.curText = self.dropList[index]
        else:
            self.curSelIdx = -1
            self.curText = ""

    def mouseHandler(self):
        if Event.current.type == EventType.MouseDown:
            if self.boxRect.Contains(Event.current.mousePosition) and len(self.dropList) > 0:
                self.expanded = not self.expanded
                if self.expanded:
                    neededHeight = len(self.dropList) * self.listItemHeight
                    if neededHeight > self.maxDropHeight:
                        self.useScrollView = True
                        if self.dropDown:
                            self.dropListRect = Rect(self.boxRect.x, self.boxRect.yMax, self.boxRect.width, self.maxDropHeight)
                        else:
                            self.dropListRect = Rect(self.boxRect.x, self.boxRect.y - self.maxDropHeight, self.boxRect.width, self.maxDropHeight)
                        self.dropContentsRect = Rect(0, 0, self.boxRect.width - self.dropBtnWidth, neededHeight)
                    else:
                        self.useScrollView = False
                        if self.dropDown:
                            self.dropListRect = Rect(self.boxRect.x, self.boxRect.yMax, self.boxRect.width, neededHeight)
                        else:
                            self.dropListRect = Rect(self.boxRect.x, self.boxRect.y - neededHeight, self.boxRect.width, neededHeight)
                    #print "expand drop list scroll = %s, rect ="%self.useScrollView, self.dropListRect
            elif self.expanded and not self.dropListRect.Contains(Event.current.mousePosition):
                self.expanded = False

    def paint(self):
        idxChanged = False

        # mouse event
        self.mouseHandler()

        # ui
        GUI.Box(self.boxRect, "", self.bgStyle)
        GUI.Label(self.textRect, self.curText, self.labelStyle)
        GUI.Label(self.dropBtnRect, self.arrowText)
        if self.expanded:
            if self.useScrollView:
                GUI.Box(self.dropListRect, "", self.bgStyle)
                self.dropScrollPos = GUI.BeginScrollView(self.dropListRect, self.dropScrollPos, self.dropContentsRect)
                oColor = GUI.color
                for i in range(len(self.dropList)):
                    bRect = Rect(2, i * self.listItemHeight + 2, self.boxRect.width - 18, self.listItemHeight - 4)
                    if i == self.curSelIdx:
                        GUI.color = Color.green
                    else:
                        GUI.color = oColor
                    if GUI.Button(bRect, self.dropList[i], self.listItemStyle):
                        idxChanged = True
                        self.expanded = False
                        self.selectIndex(i)
                        Event.current.Use()
                        break
                GUI.color = oColor
                GUI.EndScrollView()
            else:
                GUI.Box(self.dropListRect, "", self.bgStyle)
                oColor = GUI.color
                for i in range(len(self.dropList)):
                    bRect = Rect(self.dropListRect.x + 2, self.dropListRect.y + i * self.listItemHeight + 2, self.boxRect.width - 4, self.listItemHeight - 4)
                    if i == self.curSelIdx:
                        GUI.color = Color.green
                    else:
                        GUI.color = oColor
                    if GUI.Button(bRect, self.dropList[i], self.listItemStyle):
                        idxChanged = True
                        self.expanded = False
                        self.selectIndex(i)
                        Event.current.Use()
                        break
                GUI.color = oColor

        # result
        if idxChanged:
            if self.onSelectChangeCallback:
                self.onSelectChangeCallback(self, self.curSelIdx, self.curText)
            return ("onSelectChange", self.curSelIdx, self.curText)
        else:
            return None

# Drop Menu Button
# A button can show a menu in dropdown list
class DropMenuButton():
    def __init__(self, boxRect, maxDropHeight, btnText, dropList = (), selIdx = -1):
        # if btnText == None: use one of the droplist button as default button and display an expend arrow (as ComboListBox)
        # else: click the main button to expand droplist, and no default button nor expand arrow
        # btnText must be set when init

        self.dropBtnWidth = 16
        self.listItemHeight = 20
        # data
        self.menuText = btnText
        self.dropList = list(dropList)
        self.selectIndex(selIdx)
        # callback
        self.onItemClickCallback = None
        # GUI
        self.resize(boxRect, maxDropHeight)
        # runtime
        self.enable = True
        self.expanded = False
        self.dropScrollPos = Vector2.zero
        # styles
        self.bgStyle = GUIStyle("box")
        self.dbStyle = GUIStyle("button")
        self.dbStyle.alignment = TextAnchor.MiddleLeft
        self.dbStyle.padding = RectOffset(1, 0, 0, 0) 

    def resize(self, boxRect, maxDropHeight):
        self.boxRect = boxRect
        self.dropDown = maxDropHeight >= 0
        self.arrowText = u"\u25BC" if self.dropDown else u"\u25B2"
        self.maxDropHeight = abs(maxDropHeight)
        if self.menuText != None:
            self.menuBtnRect = Rect(boxRect.x, boxRect.y, boxRect.width, boxRect.height)
            self.dropBtnRect = None
        else:
            self.menuBtnRect = Rect(boxRect.x, boxRect.y, boxRect.width - self.dropBtnWidth, boxRect.height)
            self.dropBtnRect = Rect(self.menuBtnRect.xMax, boxRect.y, self.dropBtnWidth, boxRect.height)

    def importListItem(self, itemName, asCurrent = False):
        self.dropList.append(itemName)
        if asCurrent:
            self.selectIndex(len(self.dropList) - 1)

    def selectIndex(self, index):
        if self.menuText != None:
            self.curSelIdx = None
            self.curText = self.menuText
        else:
            if 0 <= index < len(self.dropList):
                self.curSelIdx = index
                self.curText = self.dropList[index]
            else:
                self.curSelIdx = -1
                self.curText = ""

    def expandMenu(self):
        if self.expanded: return
        self.expanded = True
        neededHeight = len(self.dropList) * self.listItemHeight
        if neededHeight > self.maxDropHeight:
            self.useScrollView = True
            if self.dropDown:
                self.dropListRect = Rect(self.boxRect.x, self.boxRect.yMax, self.boxRect.width, self.maxDropHeight)
            else:
                self.dropListRect = Rect(self.boxRect.x, self.boxRect.y - self.maxDropHeight, self.boxRect.width, self.maxDropHeight)
            self.dropContentsRect = Rect(0, 0, self.boxRect.width - self.dropBtnWidth, neededHeight)
        else:
            self.useScrollView = False
            if self.dropDown:
                self.dropListRect = Rect(self.boxRect.x, self.boxRect.yMax, self.boxRect.width, neededHeight)
            else:
                self.dropListRect = Rect(self.boxRect.x, self.boxRect.y - neededHeight, self.boxRect.width, neededHeight)
        #print "expand drop list scroll = %s, rect ="%self.useScrollView, self.dropListRect

    def deExpandMenu(self):
        if not self.expanded: return
        self.expanded = False

    def paint(self):
        menuItemClk = None
        # disable mode
        if not self.enable:
            GUI.Button(self.menuBtnRect, "<color=#808080>" + self.curText + "</color>")
            if self.menuText == None:
                GUI.Button(self.dropBtnRect, "<color=#808080>" + self.arrowText + "</color>", self.dbStyle)
            self.deExpandMenu()
            return None
        # menu mode or select-menu mode
        if self.menuText != None:
            dropClk = GUI.Button(self.menuBtnRect, self.curText)
        else:
            if GUI.Button(self.menuBtnRect, self.curText):
                menuItemClk = (self.curSelIdx, self.curText)
            dropClk = GUI.Button(self.dropBtnRect, self.arrowText, self.dbStyle)
        # drop area
        if self.expanded:
            if self.useScrollView:
                GUI.Box(self.dropListRect, "", self.bgStyle)
                self.dropScrollPos = GUI.BeginScrollView(self.dropListRect, self.dropScrollPos, self.dropContentsRect)
                oColor = GUI.color
                for i in range(len(self.dropList)):
                    bRect = Rect(0, i * self.listItemHeight, self.boxRect.width - 15, self.listItemHeight)
                    if i == self.curSelIdx:
                        GUI.color = Color.green
                    else:
                        GUI.color = oColor
                    if GUI.Button(bRect, self.dropList[i]):
                        menuItemClk = (i, self.dropList[i])
                        self.expanded = False
                        self.selectIndex(i)
                        break
                GUI.color = oColor
                GUI.EndScrollView()
            else:
                GUI.Box(self.dropListRect, "", self.bgStyle)
                oColor = GUI.color
                for i in range(len(self.dropList)):
                    bRect = Rect(self.dropListRect.x, self.dropListRect.y + i * self.listItemHeight, self.boxRect.width, self.listItemHeight)
                    if i == self.curSelIdx:
                        GUI.color = Color.green
                    else:
                        GUI.color = oColor
                    if GUI.Button(bRect, self.dropList[i]):
                        menuItemClk = (i, self.dropList[i])
                        self.expanded = False
                        self.selectIndex(i)
                        break
                GUI.color = oColor
        # action
        if dropClk:
            if self.expanded:
                self.deExpandMenu()
            else:
                self.expandMenu()

        # result
        if menuItemClk:
            if self.onItemClickCallback:
                self.onItemClickCallback(self, menuItemClk[0], menuItemClk[1])
            return ("onItemClick", menuItemClk[0], menuItemClk[1])
        else:
            return None
        
# Drag Button
# A button can click and drag on it to change value
class DragButton():
    def __init__(self, id, rect, text):
        self.id = id
        self.text = text
        self.resize(rect)
        # value setting
        self.value = 0
        self.valueMax = None
        self.valueMin = None
        self.valueLoop = False
        self.dragRateX = 1
        self.dragRateY = 0.1
        # callback
        self.onValueChangeCallback = None
        # runtime
        self.onMouseDragging = False
        self.normalStyle = GUIStyle("button")
        self.normalStyle.padding = RectOffset(1, 0, 0, 0)
        self.activeStyle = GUIStyle("button")
        self.activeStyle.padding = RectOffset(1, 0, 0, 0)
        self.activeStyle.normal = self.activeStyle.active

    def resize(self, rect):
        self.boxRect = rect

    def setValue(self, newValue):
        if self.valueLoop and self.valueMax != None and self.valueMin != None:
            self.value = (newValue - self.valueMin) % (self.valueMax - self.valueMin) + self.valueMin
        elif self.valueMax != None and newValue > self.valueMax:
            self.value = self.valueMax
        elif self.valueMin != None and newValue < self.valueMin:
            self.value = self.valueMin
        else:
            self.value = newValue
        return self.value

    def mouseHandler(self):
        def getDelta(currentPos):
            if self.mouseDraggingMode == "xOnly":
                dx = (currentPos.x - self.mouseDraggingStartPoint.x) * self.dragRateX
                return dx + self.mouseDraggingAdjustValue
            if self.mouseDraggingMode == "yOnly":
                dy = - (currentPos.y - self.mouseDraggingStartPoint.y) * self.dragRateY
                return dy + self.mouseDraggingAdjustValue
            else:
                dx = (currentPos.x - self.mouseDraggingStartPoint.x) * self.dragRateX
                dy = - (currentPos.y - self.mouseDraggingStartPoint.y) * self.dragRateY
                return dx + dy

        if Event.current.type == EventType.MouseDown:
            # mouse down
            if self.onMouseDragging:
                self.onMouseDragging = False
            if self.boxRect.Contains(Event.current.mousePosition):
                self.onMouseDragging = True
                self.mouseDraggingStartPoint = Event.current.mousePosition
                self.mouseDraggingStartValue = self.value
                self.mouseDraggingAdjustValue = 0
                if self.dragRateX != 0 and self.dragRateY != 0:
                    self.mouseDraggingMode = "both"
                elif self.dragRateX != 0:
                    self.mouseDraggingMode = "xOnly"
                else:
                    self.mouseDraggingMode = "yOnly"
        if Event.current.type == EventType.MouseUp:
            # mouse up
            if self.onMouseDragging:
                self.onMouseDragging = False
                delta = getDelta(Event.current.mousePosition)
                self.setValue(self.mouseDraggingStartValue + delta)
                if delta != 0:
                    if self.onValueChangeCallback:
                        self.onValueChangeCallback(self, self.value)
                    return ("onDragMoved", self.id, self.value, delta)
        if Event.current.type == EventType.MouseDrag:
            # mouse drag
            if self.onMouseDragging:
                Event.current.Use()
                # decide mode
                if self.mouseDraggingMode == "both":
                    dx = abs(Event.current.mousePosition.x - self.mouseDraggingStartPoint.x)
                    dy = abs(Event.current.mousePosition.y - self.mouseDraggingStartPoint.y)
                    if dx > 5 and dy <= 5:
                        self.mouseDraggingMode = "xOnly"
                        self.mouseDraggingAdjustValue = - (Event.current.mousePosition.y - self.mouseDraggingStartPoint.y) * self.dragRateY
                    elif dy > 5 and dx <= 5:
                        self.mouseDraggingMode = "yOnly"
                        self.mouseDraggingAdjustValue = (Event.current.mousePosition.x - self.mouseDraggingStartPoint.x) * self.dragRateX
                    elif dx > 5 and dy > 5:
                        if dx > dy:
                            self.mouseDraggingMode = "xOnly"
                            self.mouseDraggingAdjustValue = - (Event.current.mousePosition.y - self.mouseDraggingStartPoint.y) * self.dragRateY
                        elif dy > dx:
                            self.mouseDraggingMode = "yOnly"
                            self.mouseDraggingAdjustValue = (Event.current.mousePosition.x - self.mouseDraggingStartPoint.x) * self.dragRateX
                # check delta
                delta = getDelta(Event.current.mousePosition)
                self.setValue(self.mouseDraggingStartValue + delta)
                if delta != 0:
                    if self.onValueChangeCallback:
                        self.onValueChangeCallback(self, self.value)
                    return ("onDragMoving", self.id, self.value, delta)
        return None
    
    def paint(self):
        if self.onMouseDragging:
            clk = GUI.Button(self.boxRect, self.text, self.activeStyle)
        else:
            clk = GUI.Button(self.boxRect, self.text, self.normalStyle)
        return clk

# Drag Input Unit
# A input unit contants drag button and text input
class DragInputUnit():
    def __init__(self, rect, text, vStyle, buttonSize = None):
        # set button size
        if buttonSize == None:
            self.vBtnWidth = rect.width / 2 - 2
            self.vBtnHeight = rect.height / 2 - 2
        elif vStyle:
            self.vBtnWidth = rect.width / 2 - 2
            self.vBtnHeight = buttonSize
        else:
            self.vBtnWidth = buttonSize
            self.vBtnHeight = rect.height / 2 - 2

        # setting
        self.text = text
        self.drgButton = DragButton(text, Rect(0, 0, self.vBtnWidth, self.vBtnHeight), text)
        self.resize(rect, vStyle)
        self.enable = True

        # callback
        self.onInputChangeCallback = None       # function(self, self.value)

        # runtime
        self.dragMode = True
        
        # format and value
        self.setValueFormat()
        self.setValue(0)

        # styles
        self.color = Color.white
        self.disableColor = Color(0.5, 0.5, 0.5, 1)
        self.labelStyle = GUIStyle("label")
        self.labelStyle.alignment = TextAnchor.LowerCenter
        self.textStyle = GUIStyle("textField")
        self.textStyle.alignment = TextAnchor.LowerCenter
        self.textStyle.fontSize = 12


    def resize(self, rect, vStyle):
        self.unitRect = Rect(rect.x, rect.y, rect.width, rect.height)
        self.vStyle = vStyle

        # size drag button
        if vStyle:
            bRect = Rect(rect.x + 1, rect.y + 1, rect.width - 2, self.vBtnHeight)
            self.labelRect = Rect(rect.x, rect.y + self.vBtnHeight + 1, rect.width, rect.height - self.vBtnHeight - 1)
            self.textRect  = Rect(rect.x, rect.y + self.vBtnHeight + 1, rect.width, rect.height - self.vBtnHeight - 1)
            self.confirmBtnRect = Rect(rect.x + 1, rect.y + 1, rect.width / 2 - 1, self.vBtnHeight)
            self.cancelBtnRect = Rect(rect.x + 1 + rect.width / 2 - 1, rect.y + 1, rect.width / 2 - 1, self.vBtnHeight)
        else:
            bRect = Rect(rect.x + 1, rect.y + 1, self.vBtnWidth, rect.height - 2)
            self.labelRect = Rect(rect.x + self.vBtnWidth, rect.y, rect.width - self.vBtnWidth, rect.height)
            self.textRect  = Rect(rect.x + self.vBtnWidth, rect.y, rect.width - self.vBtnWidth, rect.height)
            self.confirmBtnRect = Rect(rect.x + 1, rect.y + 1, self.vBtnWidth / 2 - 1, rect.height - 2)
            self.cancelBtnRect = Rect(rect.x + 1 + self.vBtnWidth / 2 - 1, rect.y + 1, self.vBtnWidth / 2 - 1, rect.height - 2)
            #self.textRect  = Rect(rect.x + 1, rect.y, rect.width - self.vBtnWidth, rect.height)
            #self.confirmBtnRect = Rect(rect.x + self.textRect.width + 1, rect.y + 1, self.vBtnWidth / 2 - 1, rect.height - 2)
            #self.cancelBtnRect = Rect(rect.x + self.textRect.width + self.vBtnWidth / 2, rect.y + 1, self.vBtnWidth / 2 - 1, rect.height - 2)
        self.drgButton.resize(bRect)

    def setValueFormat(self, minValue = None, maxValue = None, valueLoop = False, vFormat = "%.2f", maxLength = 7):
        self.drgButton.valueMin = minValue
        self.drgButton.valueMax = maxValue
        self.drgButton.valueLoop = valueLoop
        self.valueFormat = vFormat
        self.maxLength = maxLength

    def setDragRate(self, rateX, rateY):
        self.drgButton.dragRateX = rateX
        self.drgButton.dragRateY = rateY

    def setText(self, text):
        self.text = text
        self.drgButton.text = text

    def setValue(self, value):
        self.value = self.drgButton.setValue(value)
        if self.dragMode:
            self.valueText = (self.valueFormat%self.value)[:self.maxLength]

    def paint(self):
        # event
        if self.enable:
            if self.dragMode:
                if Event.current.type == EventType.MouseDown:
                    if self.labelRect.Contains(Event.current.mousePosition):
                        self.dragMode = False
                        return False
            else:
                if Event.current.type == EventType.KeyDown:
                    if Event.current.keyCode == KeyCode.Return or Event.current.keyCode == KeyCode.KeypadEnter:
                        try:
                            v = float(self.valueText)
                            self.value = self.drgButton.setValue(v)
                            self.valueText = (self.valueFormat%self.value)[:self.maxLength]
                            self.dragMode = True
                            if self.onInputChangeCallback:
                                self.onInputChangeCallback(self, self.value, None)
                            return True
                        except:
                            return False
            if self.dragMode:
                ev = self.drgButton.mouseHandler()
                if ev and ev[0] == "onDragMoving":
                    self.value = ev[2]
                    delta = ev[3]
                    self.valueText = (self.valueFormat%self.value)[:self.maxLength]
                    if self.onInputChangeCallback:
                        self.onInputChangeCallback(self, self.value, delta)
                    return True
        # label or text
        oColor = GUI.color
        GUI.color = self.color if self.enable else self.disableColor
        if self.dragMode or not self.enable:
            GUI.Box(self.unitRect, "", self.textStyle)
            GUI.Label(self.labelRect, self.valueText, self.labelStyle)
        else:
            GUI.Box(self.unitRect, "", self.textStyle)
            self.valueText = GUI.TextField(self.textRect, self.valueText, self.labelStyle)
        # drag or confirm/cancel button
        if self.dragMode or not self.enable:
            self.drgButton.paint()
        else:
            GUI.color = Color.green
            if GUI.Button(self.confirmBtnRect, u"\u2713"):
                try:
                    v = float(self.valueText)
                    self.value = self.drgButton.setValue(v)
                    self.valueText = (self.valueFormat%self.value)[:self.maxLength]
                    self.dragMode = True
                    if self.onInputChangeCallback:
                        self.onInputChangeCallback(self, self.value, None)
                    GUI.color = oColor
                    return True
                except:
                    pass
            GUI.color = Color.red
            if GUI.Button(self.cancelBtnRect, u"\u2715"):
                self.valueText = (self.valueFormat%self.value)[:self.maxLength]
                self.dragMode = True
        GUI.color = oColor
        return False

# Curve Editor
# A Beizer curve Editor, capable for multi-data and multi-section
class CurveEditor():
    CURVE_LINEAR = (0.2, 0.2, 0.8, 0.8)
    CURVE_PRESET_DATA = [
        ("Linear", (0.2, 0.2, 0.8, 0.8)),
        ("F-S", (0.1, 0.4, 0.6, 0.9)),
        ("S-F", (0.4, 0.1, 0.9, 0.6)),
        ("F-S-F", (0.0, 0.4, 1.0, 0.6)),
        ("S-F-S", (0.4, 0.0, 0.6, 1.0)),
    ]
    MULTI_CURVE_LIMIT = 30
    DEFAULT_COLOR = {
        "Background"    : Color(0.2, 0.2, 0.2, 1), 
        "Editable Curve": Color.yellow, 
        "Readonly Curve": Color.cyan, 
        "Start Handle"  : Color.cyan, 
        "End Handle"    : Color.magenta
    }

    @staticmethod
    def cubicBezierInterpolate(curve, x):
        def sampleBezier(h1, h2, t):
            zero = Vector2.zero
            full = Vector2(1.0, 1.0)
            v1 = Vector2.Lerp(zero, h1, t)
            v2 = Vector2.Lerp(h1, h2, t)
            v3 = Vector2.Lerp(h2, full, t)
            v4 = Vector2.Lerp(v1, v2, t)
            v5 = Vector2.Lerp(v2, v3, t)
            v6 = Vector2.Lerp(v4, v5, t)
            return v6

        # cubic bezier iterploate function
        handle1 = Vector2(curve[0], curve[1])
        handle2 = Vector2(curve[2], curve[3])
        xtgt = x
        x0 = 0.0
        x1 = 1.0
        c = 0
        #print "try to find point at %.3f"%x
        while True:
            v = sampleBezier(handle1, handle2, x)
            c += 1
            #print "sample #%d x=%.3f get point (%.3f, %.3f)"%(c, x, v.x, v.y)
            if c >= 10: break
            if math.fabs(xtgt - v.x) < 0.001: break
            if v.x > xtgt:
                x1 = x
                x = (x1 + x0) / 2
                #print "   over! -> set seek range to %.3f to %.3f"%(x0, x1)
            else:
                x0 = x
                x = (x1 + x0) / 2
                #print "   less! -> set seek range to %.3f to %.3f"%(x0, x1)
        return v.y

    def __init__(self, x, y, size, sections):
        # sections
        if sections and len(sections) > 0:
            self.sections = sections
            self.curSectionIdx = -1
        else:
            self.sections = None
        # ui
        self.curveRect = Rect(0, 0, 0, 0)
        self.startHandlerRect = None
        self.endHandlerRect = None
        self.setupColor()
        self.resize(x, y, size)
        # data
        self.curveDataSet = []
        # callback
        self.onCurveUpdateCallback = None
        # runtime
        self.targetCurve = []
        self.editCurveData = None
        self.copyTargetData = None
        self.clipboard = None
        self.draggingHandle = None
        self.enable = True

    def setupColor(self):
        # change class member to setup input, call repaint after this
        self.colorCurveBackground = unity_adjust_color(CurveEditor.DEFAULT_COLOR["Background"])
        self.colorCurveEditable = unity_adjust_color(CurveEditor.DEFAULT_COLOR["Editable Curve"])
        self.colorCurveReadonly = unity_adjust_color(CurveEditor.DEFAULT_COLOR["Readonly Curve"])
        self.colorHandleStart = unity_adjust_color(CurveEditor.DEFAULT_COLOR["Start Handle"])
        self.colorHandleEnd = unity_adjust_color(CurveEditor.DEFAULT_COLOR["End Handle"])

    def changeColor(self, key, color, autoAdj=False):
        self.changeColors(((key, color),), autoAdj)

    def changeColors(self, colorInfos, autoAdj=False):
        for key, color in colorInfos:
            if autoAdj:
                color = unity_adjust_color(color)
            if key == "Background":
                self.colorCurveBackground = color
                self.baseNeedRepaint = True
            elif key == "Editable Curve":
                self.colorCurveEditable = color
                self.curveNeedRepaint = True
            elif key == "Readonly Curve":
                self.colorCurveReadonly = color
                self.curveNeedRepaint = True
            elif key == "Start Handle":
                self.colorHandleStart = color
                self.curveNeedRepaint = True
            elif key == "End Handle":
                self.colorHandleEnd = color
                self.curveNeedRepaint = True

    @property
    def CurrentColors(self):
        return {
            "Background": self.colorCurveBackground,
            "Editable Curve": self.colorCurveEditable,
            "Readonly Curve": self.colorCurveReadonly,
            "Start Handle": self.colorHandleStart,
            "End Handle": self.colorHandleEnd
        }

    def resize(self, x, y, size):
        # section select button
        if self.sections:
            self.sectionListBox = ComboListBox(Rect(x, y, size, 20), size)
            self.sectionListBox.importListItem("ALL")
            for sec in self.sections:
                self.sectionListBox.importListItem(sec)
            self.sectionListBox.selectIndex(0)
            y += 22
        # curve area
        self.curveRect.Set(x, y, size, size)
        self.curveTexture = Texture2D(size, size, TextureFormat.RGBA32, False)
        self.repaintBase()
        # func buttons
        self.copyBtnRect = Rect(x, y + size + 2, size / 2 - 1, 20)
        self.pasteBtnRect = Rect(x + size / 2 + 1, y + size + 2, size / 2 - 1, 20)
        self.presetBtnRect = Rect(x, y + size + 24, size, 20)
        # preset
        self.presetMenu = DropMenuButton(self.presetBtnRect, -(size + 20), None, [a[0] for a in CurveEditor.CURVE_PRESET_DATA], 0)
        self.presetMenu.onItemClickCallback = self.applyPreset

    def repaintBase(self):
        w = self.curveTexture.width
        h = self.curveTexture.height
        array = []
        for y0 in range(h):
            for x0 in range(w):
                if x0 == 0 or y0 == 0 or x0 == w-1 or y0 == h-1:
                    array.append(Color.white)
                else:
                    array.append(self.colorCurveBackground)
        self.curveBasePixelArray = array
        self.baseNeedRepaint = False
        self.curveNeedRepaint = True

    def clearCurveData(self):
        self.curveDataSet = []
        self.targetCurve = []
        self.editCurveData = None
        self.copyTargetData = None
        self.curveNeedRepaint = True

    def importCurveData(self, data, refresh = True):
        if self.sections:
            # with section, data should be ([x1, y1, x2, y2], [x1, y1, x2, y2], ..., [x1, y1, x2, y2])
            if len(data) != len(self.sections):
                raise Exception("CurveEditor: Error in curve data format for section editor, not enough section")
            for sd in data:
                if len(sd) != 4:
                    raise Exception("CurveEditor: Error in curve data format for section editor")
            self.curveDataSet.append(data)
        else:
            # no section, data should be [x1, y1, x2, y2]
            if len(data) != 4 or isinstance(data[0], list):
                raise Exception("CurveEditor: Error in curve data format for non-section editor")
            self.curveDataSet.append(data)
        if refresh:
            self.selectTargetCurve()
            self.curveNeedRepaint = True

    def activeSection(self, index):
        if self.sections:
            if 0 <= index < len(self.sections):
                self.curSectionIdx = index
            else:
                self.curSectionIdx = -1
            self.sectionListBox.selectIndex(self.curSectionIdx + 1)
            self.selectTargetCurve()
            self.curveNeedRepaint = True

    def selectTargetCurve(self):
        # select targets
        self.targetCurve = []
        for ds in self.curveDataSet:
            if self.sections:
                if self.curSectionIdx == -1:
                    for c in ds:
                        self.targetCurve.append(c)
                else:
                    self.targetCurve.append(ds[self.curSectionIdx])
            else:
                self.targetCurve.append(ds)
        # check edit condition
        self.checkEditCondition()
        # check copy condition
        self.checkCopyCondition()

    def checkEditCondition(self):
        if len(self.targetCurve):
            # to enable edit mode, all curve in target must be the same
            self.editCurveData = copy.copy(self.targetCurve[0])
            for i in range(1, len(self.targetCurve)):
                if self.editCurveData != self.targetCurve[i]:
                    self.editCurveData = None
                    break
        else:
            self.editCurveData = None

    def checkCopyCondition(self):
        self.copyTargetData = None
        if self.editCurveData:
            self.copyTargetData = self.editCurveData
        elif self.sections and self.curSectionIdx == -1:
            if len(self.curveDataSet) == 1:
                self.copyTargetData = self.curveDataSet[0]
            elif len(self.curveDataSet) > 1:
                self.copyTargetData = self.curveDataSet[0]
                for i in range(1, len(self.curveDataSet)):
                    if self.copyTargetData != self.curveDataSet[i]:
                        self.copyTargetData = None
                        break
    
    def updateTargetCurve(self):
        for c in self.targetCurve:
            c[0] = self.editCurveData[0]
            c[1] = self.editCurveData[1]
            c[2] = self.editCurveData[2]
            c[3] = self.editCurveData[3]
        self.curveNeedRepaint = True

    def pasteCurveData(self):
        if self.sections:
            copyMultiSection = isinstance(self.clipboard[0], list)
            if self.curSectionIdx == -1:
                # multi sections
                if copyMultiSection:
                    for cs in self.curveDataSet:
                        for sec in range(len(self.sections)):
                            cs[sec][0] = self.clipboard[sec][0]
                            cs[sec][1] = self.clipboard[sec][1]
                            cs[sec][2] = self.clipboard[sec][2]
                            cs[sec][3] = self.clipboard[sec][3]
                else:
                    for cs in self.curveDataSet:
                        for sec in range(len(self.sections)):
                            cs[sec][0] = self.clipboard[0]
                            cs[sec][1] = self.clipboard[1]
                            cs[sec][2] = self.clipboard[2]
                            cs[sec][3] = self.clipboard[3]
            else:
                # single section
                sec = self.curSectionIdx
                if copyMultiSection:
                    for cs in self.curveDataSet:
                        cs[sec][0] = self.clipboard[sec][0]
                        cs[sec][1] = self.clipboard[sec][1]
                        cs[sec][2] = self.clipboard[sec][2]
                        cs[sec][3] = self.clipboard[sec][3]
                else:
                    for cs in self.curveDataSet:
                        cs[sec][0] = self.clipboard[0]
                        cs[sec][1] = self.clipboard[1]
                        cs[sec][2] = self.clipboard[2]
                        cs[sec][3] = self.clipboard[3]
        else:
            # non-section
            for c in self.curveDataSet:
                c[0] = self.clipboard[0]
                c[1] = self.clipboard[1]
                c[2] = self.clipboard[2]
                c[3] = self.clipboard[3]
        self.checkEditCondition()
        self.curveNeedRepaint = True

    def mouseHandler(self):
        def vlimit(v):
            if v > 1.0:
                return 1.0
            elif v < 0.0:
                return 0.0
            else:
                return v
        if Event.current.type == EventType.MouseDown:
            self.draggingHandle = None
            # mouse down
            if self.startHandlerRect and self.startHandlerRect.Contains(Event.current.mousePosition):
                self.draggingHandle = "s"
                self.draggingOrgPosition = (self.editCurveData[0], self.editCurveData[1])
                self.mouseDraggingStartPoint = Event.current.mousePosition
            if self.endHandlerRect and self.endHandlerRect.Contains(Event.current.mousePosition):
                self.draggingHandle = "e"
                self.draggingOrgPosition = (self.editCurveData[2], self.editCurveData[3])
                self.mouseDraggingStartPoint = Event.current.mousePosition
            if not self.draggingHandle and self.curveRect.Contains(Event.current.mousePosition):
                self.draggingHandle = "n"
        if Event.current.type == EventType.MouseUp:
            # mouse up
            self.draggingHandle = None
        if Event.current.type == EventType.MouseDrag:
            # mouse drag
            if self.draggingHandle:
                Event.current.Use()
                if self.draggingHandle == "n":
                    return False
                dx = Event.current.mousePosition.x - self.mouseDraggingStartPoint.x
                dy = - Event.current.mousePosition.y + self.mouseDraggingStartPoint.y
                if self.draggingHandle == "s":
                    self.editCurveData[0] = vlimit(self.draggingOrgPosition[0] + float(dx) / self.curveRect.width)
                    self.editCurveData[1] = vlimit(self.draggingOrgPosition[1] + float(dy) / self.curveRect.height)
                else:
                    self.editCurveData[2] = vlimit(self.draggingOrgPosition[0] + float(dx) / self.curveRect.width)
                    self.editCurveData[3] = vlimit(self.draggingOrgPosition[1] + float(dy) / self.curveRect.height)
                return True
        return False

    def applyPreset(self, tgt, presetIndex, presetName):
        if presetIndex >= len(CurveEditor.CURVE_PRESET_DATA):
            self.editCurveData = list(CurveEditor.CURVE_LINEAR)
        else:
            self.editCurveData = list(CurveEditor.CURVE_PRESET_DATA[presetIndex][1])

    def paint(self):
        def drawPoint(x, y, color):
            if x < 0: x = 0
            if x > self.curveRect.width-1: x = self.curveRect.width-1
            if y < 0: y = 0
            if y > self.curveRect.height-1: y = self.curveRect.height-1
            self.curveTexture.SetPixel(x, y, color)
        def drawLine(x0, y0, x1, y1, color):
            dx = (x1 - x0) if x1 >= x0 else (x0 - x1)
            dy = (y1 - y0) if y1 >= y0 else (y0 - y1)
            if dx == dy == 0:
                drawPoint(x0, y0, color)
            elif dx >= dy:
                for x in range(x0, x1) if x1 > x0 else range(x1, x0):
                    y = (y1 - y0)*(x - x0)/(x1 - x0) + y0
                    drawPoint(x, y, color)
            else:
                for y in range(y0, y1) if y1 > y0 else range(y1, y0):
                    x = (x1 - x0)*(y - y0)/(y1 - y0) + x0
                    drawPoint(x, y, color)
        def drawHandler(x0, y0, x, y, color):
            drawLine(x0, y0, x, y, color)
            drawPoint(x-1, y-1, color)
            drawPoint(x, y-1, color)
            drawPoint(x+1, y-1, color)
            drawPoint(x-1, y, color)
            drawPoint(x, y, color)
            drawPoint(x+1, y, color)
            drawPoint(x-1, y+1, color)
            drawPoint(x, y+1, color)
            drawPoint(x+1, y+1, color)
            drawPoint(x-2, y, color)
            drawPoint(x+2, y, color)
            drawPoint(x, y-2, color)
            drawPoint(x, y+2, color)
            return Rect(self.curveRect.xMin + x - 5, self.curveRect.yMax - y - 5, 11, 11)
        def drawCurve(curveData, color):
            xl = 1
            yl = 1
            for x in range(1, self.curveRect.width-1):
                y = CurveEditor.cubicBezierInterpolate(curveData, float(x)/self.curveRect.width)
                y = int(round(y*self.curveRect.height))
                drawLine(xl, yl, x, y, color)
                xl = x
                yl = y
        def repaintCurve():
            # base
            self.curveTexture.SetPixels(Array[Color](self.curveBasePixelArray))
            # draw data
            if self.editCurveData:
                drawCurve(self.editCurveData, self.colorCurveEditable)
                h1x = int(round(self.curveRect.width  * self.editCurveData[0]))
                h1y = int(round(self.curveRect.height * self.editCurveData[1]))
                h2x = int(round(self.curveRect.width  * self.editCurveData[2]))
                h2y = int(round(self.curveRect.height * self.editCurveData[3]))
                self.startHandlerRect = drawHandler(0, 0, h1x, h1y, self.colorHandleStart)
                self.endHandlerRect = drawHandler(self.curveRect.width, self.curveRect.height, h2x, h2y, self.colorHandleEnd)
            else:
                drawnBuffer = []
                drawnCount = 0
                for c in self.targetCurve:
                    if c not in drawnBuffer:
                        drawnBuffer.append(c)
                        drawCurve(c, self.colorCurveReadonly)
                        drawnCount += 1
                        if drawnCount >= CurveEditor.MULTI_CURVE_LIMIT:
                            break
                self.startHandlerRect = None
                self.endHandlerRect = None
            # repaint done
            self.curveTexture.Apply()
            self.curveNeedRepaint = False

        # handle evnet
        cUpdate = False
        pUpdate = False
        if self.enable and not (self.sections and self.sectionListBox.expanded):
            cUpdate = self.mouseHandler()

        # draw base
        if self.baseNeedRepaint:
            self.repaintBase()

        # draw curve
        if self.curveNeedRepaint:
            repaintCurve()
        GUI.DrawTexture(self.curveRect, self.curveTexture)

        # multi sections
        if self.sections:
            ev = self.sectionListBox.paint()
            if ev and ev[0] == "onSelectChange":
                self.activeSection(ev[1] - 1)

        # func buttons
        if not self.presetMenu.expanded:
            allowCopy = self.enable and self.copyTargetData != None
            if GUI.Button(self.copyBtnRect, "Copy" if allowCopy else "<color=#808080>Copy</color>") and allowCopy:
                self.clipboard = copy.deepcopy(self.copyTargetData)
            allowPaste = self.enable and self.clipboard != None and len(self.curveDataSet) > 0
            if GUI.Button(self.pasteBtnRect, "Paste" if allowPaste else "<color=#808080>Paste</color>") and allowPaste:
                self.pasteCurveData()
                pUpdate = True
        allowPreset = self.enable and len(self.targetCurve) > 0
        self.presetMenu.enable = allowPreset
        if self.presetMenu.paint():
            cUpdate = True
        
        # update
        if cUpdate:
            self.updateTargetCurve()
        if cUpdate or pUpdate:
            self.checkCopyCondition()
            if self.onCurveUpdateCallback:
                self.onCurveUpdateCallback(self)
            return ("onCurveUpdate")
        else:
            return None
        
# Keyframe Button
# A little button to present one keyframe
class KFButton():
    WIDTH = 11
    HEIGHT = 11
    DEFAULT_COLOR = {
        "Icon Keyframe"         : Color.white,
        "Icon Keyframe Active"  : Color(0.878, 0.125, 0.251, 1),
        "Icon Group"            : Color(0.500, 0.500, 0.500, 1),
        "Icon Group Active"     : Color(0.500, 0.000, 0.000, 1),
        "Icon Edge Normal"      : Color(0.627, 0.627, 0, 1),
        "Icon Edge Light"       : Color(1, 1, 0, 1),
        "Icon Edge Dark"        : Color(0.376, 0.251, 0.251, 1),
    }
    FRAMENORMALCOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Keyframe"])
    FRAMEACTIVECOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Keyframe Active"])
    GROUPNORMALCOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Group"])
    GROUPACTIVECOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Group Active"])
    BORDERNORMALCOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Edge Normal"])
    BORDERLIGHTCOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Edge Light"])
    BORDERDARKCOLOR = unity_adjust_color(DEFAULT_COLOR["Icon Edge Dark"])
    TEXTURE_FRAMENORMAL = None
    TEXTURE_FRAMEACTIVE = None
    TEXTURE_GROUPNORMAL = None
    TEXTURE_GROUPACTIVE = None

    @staticmethod
    def InitButtonTexture(forceRedraw = False):
        if not forceRedraw and KFButton.TEXTURE_FRAMENORMAL and KFButton.TEXTURE_FRAMEACTIVE and KFButton.TEXTURE_GROUPNORMAL and KFButton.TEXTURE_GROUPACTIVE:
            return

        def BuildButtonBitArray(fColor):
            bitArray = []
            for cy in range(KFButton.HEIGHT):
                for cx in range(KFButton.WIDTH):
                    px = cx - 5
                    py = cy - 5
                    pd = abs(px) + abs(py)
                    # get color
                    if pd < 5:
                        pc = fColor
                    elif pd > 5:
                        pc = Color.clear
                    else:
                        if px <= 0 and py >= 0:
                            pc = KFButton.BORDERLIGHTCOLOR
                        elif px > 0 and py < 0:
                            pc = KFButton.BORDERDARKCOLOR
                        else:
                            pc = KFButton.BORDERNORMALCOLOR
                    bitArray.append(pc)
            return bitArray

        # frame normal
        if KFButton.TEXTURE_FRAMENORMAL == None:
            KFButton.TEXTURE_FRAMENORMAL = Texture2D(KFButton.WIDTH, KFButton.HEIGHT, TextureFormat.RGBA32, False)
        KFButton.TEXTURE_FRAMENORMAL.SetPixels(Array[Color](BuildButtonBitArray(KFButton.FRAMENORMALCOLOR)))
        KFButton.TEXTURE_FRAMENORMAL.Apply()
        # frame active
        if KFButton.TEXTURE_FRAMEACTIVE == None:
            KFButton.TEXTURE_FRAMEACTIVE = Texture2D(KFButton.WIDTH, KFButton.HEIGHT, TextureFormat.RGBA32, False)
        KFButton.TEXTURE_FRAMEACTIVE.SetPixels(Array[Color](BuildButtonBitArray(KFButton.FRAMEACTIVECOLOR)))
        KFButton.TEXTURE_FRAMEACTIVE.Apply()
        # group normal
        if KFButton.TEXTURE_GROUPNORMAL == None:
            KFButton.TEXTURE_GROUPNORMAL = Texture2D(KFButton.WIDTH, KFButton.HEIGHT, TextureFormat.RGBA32, False)
        KFButton.TEXTURE_GROUPNORMAL.SetPixels(Array[Color](BuildButtonBitArray(KFButton.GROUPNORMALCOLOR)))
        KFButton.TEXTURE_GROUPNORMAL.Apply()
        # group active
        if KFButton.TEXTURE_GROUPACTIVE == None:
            KFButton.TEXTURE_GROUPACTIVE = Texture2D(KFButton.WIDTH, KFButton.HEIGHT, TextureFormat.RGBA32, False)
        KFButton.TEXTURE_GROUPACTIVE.SetPixels(Array[Color](BuildButtonBitArray(KFButton.GROUPACTIVECOLOR)))
        KFButton.TEXTURE_GROUPACTIVE.Apply()

    def __init__(self, grouped):
        KFButton.InitButtonTexture()
        self.selected = False
        self.grouped = grouped
        self.canvasRect = Rect(0, 0, KFButton.WIDTH, KFButton.HEIGHT)
        self.chooseTexture()
    
    def chooseTexture(self):
        if self.grouped:
            if self.selected:
                self.canvasTexture =  KFButton.TEXTURE_GROUPACTIVE
            else:
                self.canvasTexture =  KFButton.TEXTURE_GROUPNORMAL
        else:
            if self.selected:
                self.canvasTexture =  KFButton.TEXTURE_FRAMEACTIVE
            else:
                self.canvasTexture =  KFButton.TEXTURE_FRAMENORMAL

    def select(self):
        if not self.selected:
            self.selected = True
            self.chooseTexture()

    def deselect(self):
        if self.selected:
            self.selected = False
            self.chooseTexture()

    def toggle(self):
        self.selected = not self.selected
        self.chooseTexture()

    def paint(self):
        # GUI paint
        GUI.DrawTexture(self.canvasRect, self.canvasTexture)

# Keyframe box
# A grid box to draw keyframes buttons
class KFBox():
    ROWHEIGHT = 14
    COLUMNWIDTH = 13
    DEFAULT_COLOR = {
        "Frame Line Normal"     : Color(0.294, 0.294, 0.294, 1),
        "Frame Line Every 5"    : Color(0.588, 0.588, 1.000, 1),
        "Frame Line Highlight"  : Color(0.588, 1.000, 0.588, 1),
        "Frame Row Border"      : Color(0.627, 0.255, 0.051, 1),
        "Frame Odd Normal"      : Color(0.216, 0.216, 0.235, 1),
        "Frame Even Normal"     : Color(0.075, 0.075, 0.075, 1),
        "Frame Odd Highlight"   : Color(0.412, 0.216, 0.235, 1),
        "Frame Even Highlight"  : Color(0.329, 0.075, 0.075, 1),
        "Frame Selecting Box"   : Color.gray,
    }

    @property
    def columnTo(self):
        return self.columnFrom + self.columnNumber - 1
    @property
    def rowTo(self):
        return self.rowFrom + self.rowNumber - 1

    def __init__(self, x, y, columns, rows):
        # setting
        self.columnFrom = 0
        self.rowFrom = 0
        self.columnNumber = 0
        self.rowNumber = 0
        self.highlightColumn = 0
        self.highlightRows = []
        self.arrangedNames = []

        # keyframe buffer
        self.clearKeyframe()

        # runtime - GUI
        self.dataMode32 = get_engine_id() == "neov2"
        self.setupColor()
        self.resize(x, y, columns, rows)

        # runtime - Mouse
        self.onMouseDragging = False
        self.onMouseDraggingMoveKeyframe = False
        self.mouseDraggingStartPoint = None
        self.mouseDraggingBox = None
        self.mouseDraggingBoxTexture = None
        self.mouseDraggingBoxUpdate = False

    def setupColor(self):
        self.colorRowBorder = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Row Border"])
        self.colorRowEvenNormal = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Even Normal"]) # even normal
        self.colorRowEvenHighlight = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Even Highlight"]) # even highlight
        self.colorRowOddNormal = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Odd Normal"]) # odd normal
        self.colorRowOddHighlight = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Odd Highlight"])  # odd highlight
        self.colorLineNormal = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Line Normal"]) # normal
        self.colorLine5 = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Line Every 5"]) # 5
        self.colorLineHighlight = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Line Highlight"]) # highlight
        self.colorSelectingBox = unity_adjust_color(KFBox.DEFAULT_COLOR["Frame Selecting Box"])
        # change to Color32
        self.setupColor32()

    def setupColor32(self):
        # change to Color32
        self.colorRowBorder32 = color_to_color32(self.colorRowBorder)
        self.colorRowBackground32 = (
            color_to_color32(self.colorRowEvenNormal), # even normal
            color_to_color32(self.colorRowEvenHighlight), # even highlight
            color_to_color32(self.colorRowOddNormal), # odd normal
            color_to_color32(self.colorRowOddHighlight), # odd highlight
        )
        self.colorColumnLine32 = (
            color_to_color32(self.colorLineNormal), # normal
            color_to_color32(self.colorLine5), # 5 
            color_to_color32(self.colorLineHighlight), # height
        )

    def clearKeyframe(self):
        self.keyframes = {}
        self.groupFrames = {}
        self.selectedKeyframes = set()
        self.visiableFrames = set()

    def appendKeyframe(self, bKey, col, frameObj):
        # save keyframe info
        kfb = KFButton(False)
        kfb.key = bKey
        kfb.column = col
        kfb.frameObj = frameObj
        self.linkGroupFrame(kfb)
        # register
        if bKey in self.keyframes:
            self.keyframes[bKey][col] = kfb
        else:
            self.keyframes[bKey] = {col: kfb}

    def deleteKeyframe(self, bKey, col):
        # need arrange buttons and clear selection manually
        kfb = self.keyframes[bKey][col]
        #self.deselectButton(kfb)
        self.unlinkGroupFrame(kfb)
        del self.keyframes[bKey][col]

    def updateKeyframeObject(self, bKey, col, frameObj):
        self.keyframes[bKey][col].frameObj = frameObj

    def linkGroupFrame(self, kfBtn):
        def newGroupBtn(k, c):
            gfb = KFButton(True)
            gfb.key = k
            gfb.column = c
            gfb.child = set()
            return gfb
        # normalize key
        sidx = kfBtn.key.index("#")
        gKey = kfBtn.key[:sidx + 1]
        # get or add target group btn
        col = kfBtn.column
        if gKey in self.groupFrames:
            if col in self.groupFrames[gKey]:
                tgtGroupBtn = self.groupFrames[gKey][col]
            else:
                tgtGroupBtn = newGroupBtn(gKey, col)
                self.groupFrames[gKey][col] = tgtGroupBtn
        else:
            tgtGroupBtn = newGroupBtn(gKey, col)
            self.groupFrames[gKey] = {col: tgtGroupBtn}
        # link
        tgtGroupBtn.child.add(kfBtn)
        kfBtn.group =  tgtGroupBtn

    def unlinkGroupFrame(self, kfBtn):
        # remove keyframe <--> group link
        gBtn = kfBtn.group
        kfBtn.group = None
        gBtn.child.remove(kfBtn)
        # check group exists
        if len(gBtn.child) == 0:
            del self.groupFrames[gBtn.key][gBtn.column]
            return
        # check group select state
        selected = [bkf for bkf in gBtn.child if bkf.selected]
        if len(selected) > 0:
            gBtn.select()
        else:
            gBtn.deselect()

    def arrangeKeyframe(self, arrInfo = None):
        def addButton(btn, cIdx, rIdx):
            btn.canvasRect.x = (cIdx - self.columnFrom) * KFBox.COLUMNWIDTH + 3 + self.boxRect.x
            btn.canvasRect.y = (rIdx - self.rowFrom) * KFBox.ROWHEIGHT + 2 + self.boxRect.y
            self.visiableFrames.add(btn)
        def addRowButtons(tgtBuffer, bKey, rIdx):
            if bKey in tgtBuffer:
                for cIdx in range(self.columnFrom, self.columnTo + 1):
                    if cIdx in tgtBuffer[bKey]:
                        addButton(tgtBuffer[bKey][cIdx], cIdx, rIdx)
        # update arr info
        if arrInfo == None:
            arrInfo = self.arrangedNames
        else:
            self.arrangedNames = arrInfo
        # arrange all keyframes
        self.visiableFrames.clear()
        for rowIdx in range(self.rowFrom, self.rowTo + 1):
            if rowIdx >= len(arrInfo):
                break
            fn = arrInfo[rowIdx]
            if fn[1]:
                if not fn[2]:
                    # group frame
                    addRowButtons(self.groupFrames, fn[3], rowIdx)
            else:
                # keyframe
                addRowButtons(self.keyframes, fn[3], rowIdx)

    def resize(self, x, y, colNum, rowNum):
        # column and row
        self.columnNumber = colNum
        self.rowNumber = rowNum
        # box texture
        self.boxRect = Rect(x, y, colNum * KFBox.COLUMNWIDTH + 4, rowNum * KFBox.ROWHEIGHT + 1)
        self.boxTexture = Texture2D(self.boxRect.width, self.boxRect.height, TextureFormat.RGBA32, False)
        self.repaintBase()
        self.arrangeKeyframe()

    def repaintBase(self):
        w = self.boxTexture.width
        h = self.boxTexture.height
        # setup info
        self.boxColumnInfo = []
        cIndex = self.columnFrom
        for x in range(w):
            if x > 7 and ((x - 8) % KFBox.COLUMNWIDTH) == 0:
                if cIndex == self.highlightColumn:
                    ci = 2
                elif cIndex % 5 == 0:
                    ci = 1
                else:
                    ci = 0
                self.boxColumnInfo.append([cIndex, x, ci])
                cIndex += 1
        self.boxColumnSet = set([ci[1] for ci in self.boxColumnInfo])
        self.boxRowInfo = []
        boxInterRow = set()
        rIndex = self.rowFrom
        for r in range(self.rowNumber):
            y = ((self.rowNumber - r - 1) * KFBox.ROWHEIGHT + 1) * w
            ci = r % 2 * 2 + (1 if rIndex in self.highlightRows else 0)
            self.boxRowInfo.append([rIndex, y, ci])
            rIndex += 1
            boxInterRow.add(r * KFBox.ROWHEIGHT)
        boxInterRow.add(h-1)
        # draw base
        if self.dataMode32:
            data = self.boxTexture.GetRawTextureData[Color32]()
            # draw row border
            for rby in boxInterRow:
                index = rby * w
                for x in range(w):
                    if x not in self.boxColumnSet:
                        data[index] = self.colorRowBorder32
                    index += 1
            # draw row
            for (rn, ry, ci) in self.boxRowInfo:
                color = self.colorRowBackground32[ci]
                index = ry
                for y in range(KFBox.ROWHEIGHT-1):
                    for x in range(w):
                        if x not in self.boxColumnSet:
                            data[index] = color
                        index += 1
            # draw column
            for (cn, cx, ci) in self.boxColumnInfo:
                color = self.colorColumnLine32[ci]
                index = cx
                for y in range(h):
                    data[index] = color
                    index += w
            # done
            self.boxTexture.Apply()
        else:
            bytedata = self.boxTexture.GetRawTextureData()
            # draw row border
            for rby in boxInterRow:
                index = rby * w * 4
                for x in range(w):
                    if x not in self.boxColumnSet:
                        bytedata[index] = self.colorRowBorder32.r
                        bytedata[index+1] = self.colorRowBorder32.g
                        bytedata[index+2] = self.colorRowBorder32.b
                        bytedata[index+3] = self.colorRowBorder32.a
                    index += 4
            # draw row
            for (rn, ry, ci) in self.boxRowInfo:
                color = self.colorRowBackground32[ci]
                index = ry * 4
                for y in range(KFBox.ROWHEIGHT-1):
                    for x in range(w):
                        if x not in self.boxColumnSet:
                            bytedata[index] = color.r
                            bytedata[index+1] = color.g
                            bytedata[index+2] = color.b
                            bytedata[index+3] = color.a
                        index += 4
            # draw column
            for (cn, cx, ci) in self.boxColumnInfo:
                color = self.colorColumnLine32[ci]
                index = cx * 4
                for y in range(h):
                    bytedata[index] = color.r
                    bytedata[index+1] = color.g
                    bytedata[index+2] = color.b
                    bytedata[index+3] = color.a
                    index += w * 4
            # done
            self.boxTexture.LoadRawTextureData(bytedata)
            self.boxTexture.Apply()
        # Done
        self.baseNeedRepaint = False

    def drawColumns(self, drawInfo):
        w = self.boxTexture.width
        h = self.boxTexture.height
        if self.dataMode32:
            data = self.boxTexture.GetRawTextureData[Color32]()
            for (cn, cx, ci) in drawInfo:
                color = self.colorColumnLine32[ci]
                for y in range(h):
                    data[cx] = color
                    cx += w
            self.boxTexture.Apply()
        else:
            data = self.boxTexture.GetRawTextureData()
            w *= 4
            for (cn, cx, ci) in drawInfo:
                color = self.colorColumnLine32[ci]
                index = cx * 4
                for y in range(h):
                    data[index] = color.r
                    data[index+1] = color.g
                    data[index+2] = color.b
                    data[index+3] = color.a
                    index += w
            self.boxTexture.LoadRawTextureData(data)
            self.boxTexture.Apply()

    def drawRows(self, drawInfo):
        w = self.boxTexture.width
        h = self.boxTexture.height
        if self.dataMode32:
            data = self.boxTexture.GetRawTextureData[Color32]()
            for (rn, ry, ci) in drawInfo:
                color = self.colorRowBackground32[ci]
                for y in range(KFBox.ROWHEIGHT-1):
                    for x in range(w):
                        if x not in self.boxColumnSet:
                            data[ry] = color
                        ry += 1
            self.boxTexture.Apply()
        else:
            data = self.boxTexture.GetRawTextureData()
            for (rn, ry, ci) in drawInfo:
                color = self.colorRowBackground32[ci]
                index = ry * 4
                for y in range(KFBox.ROWHEIGHT-1):
                    for x in range(w):
                        if x not in self.boxColumnSet:
                            data[index] = color.r
                            data[index+1] = color.g
                            data[index+2] = color.b
                            data[index+3] = color.a
                        index += 4
            self.boxTexture.LoadRawTextureData(data)
            self.boxTexture.Apply()

    def scroll(self, col, row):
        if col == self.columnFrom and row == self.rowFrom:
            return
        # horizontal
        if col != self.columnFrom:
            self.columnFrom = col
            redrawInfo = []
            for cInfo in self.boxColumnInfo:
                cInfo[0] = col
                if col == self.highlightColumn:
                    ci = 2
                elif col % 5 == 0:
                    ci = 1
                else:
                    ci = 0
                if ci != cInfo[2]:
                    cInfo[2] = ci
                    redrawInfo.append(cInfo)
                col += 1
            if len(redrawInfo):
                self.drawColumns(redrawInfo)
        # vertical
        if row != self.rowFrom:
            self.rowFrom = row
            redrawInfo = []
            for i in range(self.rowNumber):
                rInfo = self.boxRowInfo[i]
                rInfo[0] = row
                ci = i % 2 * 2 + (1 if rInfo[0] in self.highlightRows else 0)
                if ci != rInfo[2]:
                    rInfo[2] = ci
                    redrawInfo.append(rInfo)
                row += 1
            if len(redrawInfo):
                self.drawRows(redrawInfo)
        # arrange
        self.arrangeKeyframe()

    def setHighlightColumn(self, col):
        if self.highlightColumn != col:
            oldHighlightCol = self.highlightColumn
            self.highlightColumn = col
            # check and repaint column
            redrawInfo = []
            if self.columnFrom <= col <= self.columnTo:
                cIndex = col - self.columnFrom
                cInfo = self.boxColumnInfo[cIndex]
                cInfo[2] = 2
                redrawInfo.append(cInfo)
            if self.columnFrom <= oldHighlightCol <= self.columnTo:
                cIndex = oldHighlightCol - self.columnFrom
                cInfo = self.boxColumnInfo[cIndex]
                cInfo[2] = 0 if cInfo[0] % 5 else 1
                redrawInfo.append(cInfo)
            if len(redrawInfo):
                self.drawColumns(redrawInfo)

    def setHighlightRows(self, rows):
        if self.highlightRows != rows:
            oldHighlightRows = self.highlightRows
            self.highlightRows = rows
            redrawInfo = []
            for i in range(self.rowNumber):
                rInfo = self.boxRowInfo[i]
                rInfo[2] = i % 2 * 2 + (1 if rInfo[0] in self.highlightRows else 0) 
                if (rInfo[0] in oldHighlightRows and rInfo[0] not in rows) or (rInfo[0] not in oldHighlightRows and rInfo[0] in rows):
                    redrawInfo.append(rInfo)
            if len(redrawInfo):
                self.drawRows(redrawInfo)

    def selectButtonByCood(self, bKey, col):
        if bKey in self.keyframes:
            if col in self.keyframes[bKey]:
                self.selectButton(self.keyframes[bKey][col])
                return
        raise Exception("KFBox>selectButton FAILED: Button %s@%d not found!"%(bKey, col))

    def selectButton(self, tgtBtn):
        if tgtBtn.selected:
            return False
        tgtBtn.select()
        if tgtBtn.grouped:
            # group btn
            for kfb in tgtBtn.child:
                self.selectedKeyframes.add(kfb)
                kfb.select()
        else:
            # keyframe btn
            self.selectedKeyframes.add(tgtBtn)
            tgtBtn.group.select()
        return True

    def deselectButton(self, tgtBtn):
        if not tgtBtn.selected:
            return False
        tgtBtn.deselect()
        if tgtBtn.grouped:
            # group btn
            for kfb in tgtBtn.child:
                if kfb.selected:
                    self.selectedKeyframes.remove(kfb)
                    kfb.deselect()
        else:
            # keyframe btn
            self.selectedKeyframes.remove(tgtBtn)
            anySelect = False
            for kfb in tgtBtn.group.child:
                if kfb.selected:
                    anySelect = True
                    break
            if not anySelect:
                tgtBtn.group.deselect()
        return True

    def deselectAllButton(self):
        if len(self.selectedKeyframes) > 0:
            for kfb in self.selectedKeyframes:
                kfb.deselect()
                kfb.group.deselect()
            self.selectedKeyframes.clear()
            return True
        else:
            return False

    def toggleButton(self, tgtBtn):
        if tgtBtn.selected:
            return self.deselectButton(tgtBtn)
        else:
            return self.selectButton(tgtBtn)

    def offsetKeyframePrepare(self, startColumn):
        self.onMouseDraggingMoveStartColumn = startColumn
        self.offsetInfoTemp = {}
        for kfb in self.selectedKeyframes:
            columnList = sorted(self.keyframes[kfb.key].keys())
            curIdx = columnList.index(kfb.column)
            # search minus limit
            minusLimit = 0
            sIdx = curIdx
            mid = 0
            while sIdx > 0:
                sIdx -= 1
                tkfb = self.keyframes[kfb.key][columnList[sIdx]]
                if tkfb in self.selectedKeyframes:
                    mid += 1
                else:
                    minusLimit = columnList[sIdx] + 1
                    break
            minusLimit += mid
            # search plus limt
            plusLimit = -1
            sIdx = curIdx
            mid = 0
            while sIdx < len(columnList) - 1:
                sIdx += 1
                tkfb = self.keyframes[kfb.key][columnList[sIdx]]
                if tkfb in self.selectedKeyframes:
                    mid += 1
                else:
                    plusLimit = columnList[sIdx] - 1
                    break
            if plusLimit != -1:
                plusLimit -= mid
            self.offsetInfoTemp[kfb] = [kfb.column, 0, minusLimit, plusLimit]
            #print "bone %s, start at %d, minuslimit = %d, pluslimit = %d"%(kfb.key, kfb.column, minusLimit, plusLimit)

    def offsetKeyframeDragging(self, currentColumn):
        if self.onMouseDraggingMoveStartColumn == currentColumn:
            return
        # offset keyframes
        delta = currentColumn - self.onMouseDraggingMoveStartColumn
        toMove = {}
        for kfb in self.offsetInfoTemp.keys():
            info = self.offsetInfoTemp[kfb]
            info[1] += delta
            res = info[0] + info[1]
            if res < info[2]: res = info[2]
            if info[3] != -1 and res > info[3]: res = info[3]
            if res != kfb.column:
                toMove[kfb] = res
        # do moving
        for kfb in toMove.keys():
            self.keyframes[kfb.key].pop(kfb.column)
            self.unlinkGroupFrame(kfb)
            kfb.column = toMove[kfb]
        for kfb in toMove.keys():
            self.keyframes[kfb.key][kfb.column] = kfb
            self.linkGroupFrame(kfb)
            kfb.group.select()
        self.arrangeKeyframe()
        # update start column
        self.onMouseDraggingMoveStartColumn = currentColumn

    def offsetKeyframeResult(self):
        offsetResult = set()
        for kfb in self.offsetInfoTemp.keys():
            oldColum = self.offsetInfoTemp[kfb][0]
            if kfb.column != oldColum:
                offsetResult.add((kfb.key, oldColum, kfb.column, kfb.frameObj))
                #print "move result for %s: %d -> %d"%(kfb.key, oldColum, kfb.column)
        del self.onMouseDraggingMoveStartColumn
        del self.offsetInfoTemp
        return offsetResult

    def mouseHandler(self):
        selectChange = False
        if Event.current.type == EventType.MouseDown:
            # mouse down
            if self.boxRect.Contains(Event.current.mousePosition):
                self.onMouseDragging = True
                self.mouseDraggingStartPoint = Event.current.mousePosition
                self.mouseDraggingBox = None
                #print "[KFBOX] mouse down @ %s"%str(Event.current.mousePosition)
                # kf hit check
                hit = None
                for kfb in self.visiableFrames:
                    if kfb.canvasRect.Contains(Event.current.mousePosition):
                        hit = kfb
                        break
                if hit:
                    if Event.current.shift:
                        # click with shift
                        selectChange = self.toggleButton(hit)
                    else:
                        # simple click
                        if hit.selected:    # start keyframe drag
                            self.onMouseDraggingMoveKeyframe = True
                            self.offsetKeyframePrepare(hit.column)
                        else:   # select clicked one
                            self.deselectAllButton()
                            selectChange = self.selectButton(hit)
                else:   # deselect all if not holding shift 
                    if not Event.current.shift:
                        selectChange = self.deselectAllButton()
            else:
                pass
                #self.onMouseDragging = False
                #self.onMouseDraggingMoveKeyframe = False
        if Event.current.type == EventType.MouseUp:
            # mouse up
            self.onMouseDragging = False
            if self.onMouseDraggingMoveKeyframe:
                self.onMouseDraggingMoveKeyframe = False
                res = self.offsetKeyframeResult()
                if len(res) > 0:
                    return ("onKeyframeMove", res)
        if Event.current.type == EventType.MouseDrag:
            # mouse drag
            if self.onMouseDragging:
                Event.current.Use()
                if self.onMouseDraggingMoveKeyframe:
                    # dragging move keyframe
                    limitx = Event.current.mousePosition.x
                    #if limitx > self.boxRect.xMax: limitx = self.boxRect.xMax
                    #if limitx < self.boxRect.xMin: limitx = self.boxRect.xMin
                    currentMouseColumn = int((limitx - self.boxRect.x) / KFBox.COLUMNWIDTH) + self.columnFrom
                    self.offsetKeyframeDragging(currentMouseColumn)
                else:
                    # box selecting
                    limitx = Event.current.mousePosition.x
                    if limitx > self.boxRect.xMax: limitx = self.boxRect.xMax
                    if limitx < self.boxRect.xMin: limitx = self.boxRect.xMin
                    if limitx == self.mouseDraggingStartPoint.x: limitx = self.mouseDraggingStartPoint.x + 1
                    limity = Event.current.mousePosition.y
                    if limity > self.boxRect.yMax: limity = self.boxRect.yMax
                    if limity < self.boxRect.yMin: limity = self.boxRect.yMin
                    if limity == self.mouseDraggingStartPoint.y: limity = self.mouseDraggingStartPoint.y + 1
                    newRect = Rect(min(self.mouseDraggingStartPoint.x, limitx),
                                min(self.mouseDraggingStartPoint.y, limity),
                                abs(self.mouseDraggingStartPoint.x - limitx),
                                abs(self.mouseDraggingStartPoint.y - limity)
                                )
                    if self.mouseDraggingBox != newRect:
                        self.mouseDraggingBox = newRect
                        self.mouseDraggingBoxUpdate = True
                    if self.mouseDraggingBoxUpdate:
                        for kfb in self.visiableFrames:
                            if kfb.canvasRect.Overlaps(self.mouseDraggingBox):
                                selectChange |= self.selectButton(kfb)
                            else:
                                if not Event.current.shift:
                                    selectChange |= self.deselectButton(kfb)
        # done
        if selectChange:
            return ("onKeyframeSelect", self.selectedKeyframes)
        else:
            return None

    def paint(self, enable = True):
        def paintMouseDragBox():
            w = self.mouseDraggingBox.width
            h = self.mouseDraggingBox.height
            #if w == 0 or h == 0: return
            if not self.mouseDraggingBoxTexture or self.mouseDraggingBoxUpdate:
                self.mouseDraggingBoxTexture = Texture2D(w, h, TextureFormat.RGBA32, False)
                pixels = []
                for y in range(h):
                    for x in range(w):
                        if (x == 0 or x == w-1) and y % 2 == 0:
                            pixels.append(self.colorSelectingBox)
                        elif (y == 0 or y == h-1) and x % 2 == 0:
                            pixels.append(self.colorSelectingBox)
                        else:
                            pixels.append(Color.clear)
                self.mouseDraggingBoxTexture.SetPixels(Array[Color](pixels))
                self.mouseDraggingBoxTexture.Apply()
                self.mouseDraggingBoxUpdate = False
            GUI.DrawTexture(self.mouseDraggingBox, self.mouseDraggingBoxTexture)

        # box
        if self.baseNeedRepaint:
            self.repaintBase()
        GUI.DrawTexture(self.boxRect, self.boxTexture)

        # keyframes
        for kfb in self.visiableFrames:
            kfb.paint()

        # drag box
        if self.onMouseDragging and self.mouseDraggingBox and not self.onMouseDraggingMoveKeyframe:
            paintMouseDragBox()

# Keyframe Name List
# A name list with group support, work as y-axis
class KFNameList():
    # color
    DEFAULT_COLOR = {
        "Name Text Normal"      : Color.black, 
        "Name Text Group"       : Color(0.000, 0.000, 0.500, 1), 
        "Name Text Highlight"   : Color(1.000, 0.200, 0.200, 1), 
        "Name Row Odd"          : Color(0.592, 0.427, 0.263, 1),
        "Name Row Even"         : Color(0.651, 0.486, 0.322, 1),
        "Name Row Border"       : Color(0.392, 0.000, 0.000, 1),
        "Name Column Border"    : Color(0.075, 0.075, 0.075, 1), 
    }

    @property
    def rowTo(self):
        return self.rowFrom + self.rowNumber - 1

    def __init__(self, x, y, width, rowNum):
        # setting
        self.rowFrom = 0
        self.rowNumber = 0
        self.width = 0
        self.showGroup = True
        self.clearName()

        # runtime - GUI
        self.setupColor()
        self.resize(x, y, width, rowNum)
        self.labelStyle = GUIStyle("label")
        self.labelStyle.richText = True
        self.labelStyle.fontSize = 12
        #self.labelStyle.fontStyle = FontStyle.Bold
        self.labelStyle.padding = RectOffset(0, 0, 0, 0)
        self.labelStyle.normal.textColor = self.colorTextNormal
        #print self.labelStyle.CalcSize(GUIContent("abceJjgG"))
        #print "Font name: " + self.labelStyle.font.name
        self.markStyle = GUIStyle("label")
        self.markStyle.fontSize = 12
        self.markStyle.padding = RectOffset(0, 0, 0, 0)
        self.markStyle.normal.textColor = self.colorTextGroup
        self.markStyle.alignment = TextAnchor.MiddleCenter

    def setupColor(self):
        self.colorRowBorder = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Row Border"])
        self.colorRowBgOdd = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Row Odd"])
        self.colorRowBgEven = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Row Even"])
        self.colorColumnBorder = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Column Border"])
        self.colorTextNormal = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Text Normal"])
        self.colorTextGroup = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Text Group"])
        self.colorTextHighlight = unity_adjust_color(KFNameList.DEFAULT_COLOR["Name Text Highlight"])

    def clearName(self):
        self.groupList = []
        self.groupExpand = {}
        self.groupContents = {}
        self.dispNameDic = {}
        self.fullGroupNameKeys = []
        self.arrangedNames = []
        self.highlightKeys = []
        self.highlightIndex = []
        self.hiddenNames = set()

    def clearHighlight(self):
        self.highlightKeys = []
        self.highlightIndex = []

    def setHighlight(self, newKeys):
        if self.highlightKeys != newKeys:
            self.highlightKeys = newKeys
            self.getHighlightRowIndex()
        return self.highlightIndex

    def appendName(self, groupName, frameName):
        if groupName in self.groupList:
            if frameName not in self.groupContents[groupName]:
                self.groupContents[groupName].append(frameName)
        else:
            self.groupList.append(groupName)
            self.groupExpand[groupName] = True
            self.groupContents[groupName] = [frameName,]
        self.dispNameDic[frameName] = frameName

    def arrangeName(self):
        self.fullGroupNameKeys = []    # full list of key "groupName#bonename"
        self.arrangedNames = []   # list of (name, isGroup, isExpand, key)
        for gn in self.groupList:
            # full
            self.fullGroupNameKeys.append(gn + "#")
            visibleNames = 0
            for bn in self.groupContents[gn]:
                key = gn + "#" + bn
                self.fullGroupNameKeys.append(key)
                if key not in self.hiddenNames and bn not in self.hiddenNames:
                    visibleNames += 1
            # arranged
            if self.showGroup and visibleNames:
                self.arrangedNames.append((gn, True, self.groupExpand[gn], gn + "#"))
            if self.groupExpand[gn]:
                for bn in self.groupContents[gn]:
                    key = gn + "#" + bn
                    if key not in self.hiddenNames and bn not in self.hiddenNames:
                        self.arrangedNames.append((self.dispNameDic[bn], False, False, key))
        self.getHighlightRowIndex()
        return len(self.arrangedNames)

    def getGroup(self, name):
        for group in self.groupContents.keys():
            if name in self.groupContents[group]:
                return group
        raise Exception("KFNameList.getGroup ERROR! Unknown name '%s'!"%name)

    def rename(self, frameName, newDispName):
        groupName = self.getGroup(frameName)
        self.rename(groupName, frameName, newDispName)

    def rename(self, groupName, frameName, newDispName):
        if frameName in self.groupContents[groupName]:
            # rename
            self.dispNameDic[frameName] = newDispName
            # replace in arrangedNames
            key = groupName + "#" + frameName
            for idx in range(len(self.arrangedNames)):
                if self.arrangedNames[idx][3] == key:
                    self.arrangedNames[idx] = (newDispName, self.arrangedNames[idx][1], self.arrangedNames[idx][2], key)
                    return
        print "KFNameList.rename FAILED! %s#%s not found!"%(groupName, oldName)

    def hideName(self, frameName, hide):
        groupName = self.getGroup(frameName)
        self.hideName(groupName, frameName, hide)

    def hideName(self, groupName, frameName, hide):
        if frameName in self.groupContents[groupName]:
            key = groupName + "#" + frameName
            if hide:
                self.hiddenNames.add(key)
            else:
                self.hiddenNames.discard(key)
            if hide and key in self.highlightKeys: # un-highlight when hide
                self.highlightKeys.remove(key)
                self.getHighlightRowIndex()
            self.arrangeName()
        else:
            print "KFNameList.hideName FAILED! %s#%s not found!"%(groupName, frameName)

    def getHighlightRowIndex(self):
        self.highlightIndex = []
        for fn in self.arrangedNames:
            if fn[1]:
                if fn[2]: # expanded group
                    onSel = False
                else:
                    onSel = len([c for c in self.highlightKeys if c.startswith(fn[3])]) > 0
            else:
                onSel = fn[3] in self.highlightKeys
            if onSel:
                self.highlightIndex.append(self.arrangedNames.index(fn))
        return self.highlightIndex

    def resize(self, x, y, width, rowNum):
        self.width = width
        self.rowNumber = rowNum
        # GUI
        self.boxRect = Rect(x, y, width, rowNum * KFBox.ROWHEIGHT + 1)
        self.repaintBase()

    def repaintBase(self):
        def initBasePixels(w, h):
            array = []
            for y in range(h):
                if y % KFBox.ROWHEIGHT == 0:
                    pc = self.colorRowBorder
                else:
                    if int(y / KFBox.ROWHEIGHT) % 2 != 0:
                        pc = self.colorRowBgOdd
                    else:
                        pc = self.colorRowBgEven
                array.append(self.colorColumnBorder)
                for x in range(1, w - 1):
                    array.append(pc)
                array.append(self.colorColumnBorder)
            return array
        self.boxTexture = Texture2D(self.boxRect.width, self.boxRect.height, TextureFormat.RGBA32, False)
        self.boxBasePixelArray = initBasePixels(self.boxRect.width, self.boxRect.height)
        self.boxTexture.SetPixels(Array[Color](self.boxBasePixelArray))
        self.boxTexture.Apply()
        self.baseNeedRepaint = False

    def scroll(self, row):
        if row == self.rowFrom:
            return
        # change setting
        self.rowFrom = row

    def mouseHandler(self):
        if Event.current.type == EventType.MouseDown:
            # mouse down
            if self.boxRect.Contains(Event.current.mousePosition):
                yoff = Event.current.mousePosition.y - self.boxRect.y
                ni = int(yoff / KFBox.ROWHEIGHT) + self.rowFrom
                if ni >= len(self.arrangedNames):
                    return None
                fn = self.arrangedNames[ni]
                if fn[1] and Event.current.mousePosition.x <= self.boxRect.x + 10:
                    # expand click
                    self.groupExpand[fn[0]] = not fn[2]
                    self.arrangeName()
                    return ("onExpanedGroup", self.arrangedNames, self.highlightIndex)
                else:
                    if fn[1]:
                        # group select
                        if Event.current.shift:
                            for bn in self.groupContents[fn[0]]:
                                bk = fn[0] + "#" + bn
                                if bk in self.hiddenNames or bn in self.hiddenNames:
                                    continue
                                if bk in self.highlightKeys:
                                    self.highlightKeys.remove(bk)
                                else:
                                    self.highlightKeys.append(bk)
                        else:
                            self.highlightKeys = [fn[0] + "#" + bn for bn in self.groupContents[fn[0]] if bn not in self.hiddenNames and (fn[0] + "#" + bn) not in self.hiddenNames]
                    else:
                        # bone select
                        if Event.current.shift:
                            if fn[3] in self.highlightKeys:
                                self.highlightKeys.remove(fn[3])
                            else:
                                self.highlightKeys.append(fn[3])
                        else:
                            self.highlightKeys = [fn[3]]
                    self.getHighlightRowIndex()
                    return ("onRowSelect", self.highlightIndex)
        return None

    def paint(self, enable = True):
        # draw box
        if self.baseNeedRepaint:
            self.repaintBase()
        GUI.DrawTexture(self.boxRect, self.boxTexture)

        # names
        for n in range(self.rowNumber):
            ni = n + self.rowFrom
            if ni >= len(self.arrangedNames):
                break
            fn = self.arrangedNames[ni]
            gRect = Rect(self.boxRect.x + 1, self.boxRect.y + n * KFBox.ROWHEIGHT, 10, KFBox.ROWHEIGHT)
            nRect = Rect(self.boxRect.x + 11, self.boxRect.y + n * KFBox.ROWHEIGHT, self.width - 11, KFBox.ROWHEIGHT)
            if fn[1]: # group name
                if fn[2]: # expanded group
                    GUI.Label(gRect, "--", self.markStyle)
                    onSel = False
                else:
                    GUI.Label(gRect, "+", self.markStyle)
                    onSel = len([c for c in self.highlightKeys if c.startswith(fn[3])]) > 0
            else:
                onSel = fn[3] in self.highlightKeys
            if ni in self.highlightIndex:
                self.labelStyle.normal.textColor = self.colorTextHighlight
                GUI.Label(nRect, fn[0], self.labelStyle)
            else:
                self.labelStyle.normal.textColor = self.colorTextGroup if fn[1] else self.colorTextNormal
                GUI.Label(nRect, fn[0], self.labelStyle)

# Keyframe No. header
# A frameNo indicator, work as x-axis
class KFNoHeader():
    # color
    DEFAULT_COLOR = {
        "Header Text Normal"    : Color.black,
        "Header Text Highlight" : Color(0.588, 1.000, 0.588, 1),
        "Header Border"         : Color(0.075, 0.075, 0.075, 1),
        "Header Background"     : Color(0.651, 0.486, 0.322, 1),
    }

    def __init__(self, x, y, colNum, height):
        # setting
        self.columnFrom = 0
        self.columnNumber = 0
        self.highlightColumn = 0
        self.height = 0
        # runtime - GUI
        self.setupColor()
        self.resize(x, y, colNum, height)
        self.labelStyle = GUIStyle("label")
        self.labelStyle.richText = True
        self.labelStyle.alignment = TextAnchor.LowerLeft
        self.labelStyle.fontSize = 12
        self.labelStyle.fontStyle = FontStyle.Bold
        self.labelStyle.padding = RectOffset(0, 0, 0, 0)
        self.labelStyle.normal.textColor = self.colorTextNormal

    def setupColor(self):
        self.colorTextNormal = unity_adjust_color(KFNoHeader.DEFAULT_COLOR["Header Text Normal"])
        self.colorTextHighlight = unity_adjust_color(KFNoHeader.DEFAULT_COLOR["Header Text Highlight"])
        self.colorHeaderBorder = unity_adjust_color(KFNoHeader.DEFAULT_COLOR["Header Border"])
        self.colorHeaderBackground = unity_adjust_color(KFNoHeader.DEFAULT_COLOR["Header Background"])

    def resize(self, x, y, colNum, height):
        self.columnNumber = colNum
        self.height = height
        # GUI
        self.boxRect = Rect(x-2, y, colNum * KFBox.COLUMNWIDTH + 7, height)
        self.repaintBase()

    def repaintBase(self):
        def initBasePixels(w, h):
            array = []
            for y in range(h):
                array.append(self.colorHeaderBorder)
                for x in range(1, w):
                    array.append(self.colorHeaderBackground)
            return array
        self.boxTexture = Texture2D(self.boxRect.width, self.boxRect.height, TextureFormat.RGBA32, False)
        self.boxBasePixelArray = initBasePixels(self.boxRect.width, self.boxRect.height)
        self.boxTexture.SetPixels(Array[Color](self.boxBasePixelArray))
        self.boxTexture.Apply()
        self.baseNeedRepaint = False

    def scroll(self, col):
        if col == self.columnFrom:
            return
        # change setting
        self.columnFrom = col

    def mouseHandler(self):
        if Event.current.type == EventType.MouseDown:
            # mouse down
            if self.boxRect.Contains(Event.current.mousePosition):
                #print "[KFNO] mouse down @ %s"%str(Event.current.mousePosition)
                xoffset = Event.current.mousePosition.x - self.boxRect.x
                col = int(xoffset / KFBox.COLUMNWIDTH) + self.columnFrom
                #print " -- hit x-offset= %d, column = %d"%(xoffset, col)
                self.highlightColumn = col
                return ("onFrameChange", col)
        return None

    def paint(self, enable = True):
        # box
        if self.baseNeedRepaint:
            self.repaintBase()
        GUI.DrawTexture(self.boxRect, self.boxTexture)

        cn = None
        # frame no every 5 frames
        self.labelStyle.normal.textColor = self.colorTextNormal
        for n in range(self.columnNumber):
            ni = n + self.columnFrom
            if ni == self.highlightColumn:
                cn = n
                continue
            elif ni % 5 == 0:
                fntxt = str(ni)
            else:
                continue
            txtRect = Rect(self.boxRect.x + n * KFBox.COLUMNWIDTH + 7, self.boxRect.y + 1, KFBox.COLUMNWIDTH * 5, self.height)
            #if txtRect.xMax > self.boxRect.xMax:
            #    txtRect.xMax = self.boxRect.xMax
            GUI.Label(txtRect, fntxt, self.labelStyle)

        # current frame
        self.labelStyle.normal.textColor = self.colorTextHighlight
        if enable and cn != None:
            fntxt = str(self.highlightColumn) #"<color=#96ff96>%d</color>"%self.highlightColumn
            txtRect = Rect(self.boxRect.x + cn * KFBox.COLUMNWIDTH + 7, self.boxRect.y + 1, KFBox.COLUMNWIDTH * 5, self.height)
            GUI.Label(txtRect, fntxt, self.labelStyle)

# Keyframe Viewer
# A view contants name list, frame indicator, keyframe box and something else
class KeyframeViewer():
    DEFAULT_COLOR = {
        # NameList
        "Name Text Normal"      : KFNameList.DEFAULT_COLOR["Name Text Normal"], 
        "Name Text Group"       : KFNameList.DEFAULT_COLOR["Name Text Group"], 
        "Name Text Highlight"   : KFNameList.DEFAULT_COLOR["Name Text Highlight"], 
        "Name Row Odd"          : KFNameList.DEFAULT_COLOR["Name Row Odd"],
        "Name Row Even"         : KFNameList.DEFAULT_COLOR["Name Row Even"],
        "Name Row Border"       : KFNameList.DEFAULT_COLOR["Name Row Border"],
        "Name Column Border"    : KFNameList.DEFAULT_COLOR["Name Column Border"], 
        # Header
        "Header Text Normal"    : KFNoHeader.DEFAULT_COLOR["Header Text Normal"],
        "Header Text Highlight" : KFNoHeader.DEFAULT_COLOR["Header Text Highlight"],
        "Header Border"         : KFNoHeader.DEFAULT_COLOR["Header Border"],
        "Header Background"     : KFNoHeader.DEFAULT_COLOR["Header Background"],
        # Frame
        "Frame Row Border"      : KFBox.DEFAULT_COLOR["Frame Row Border"],
        "Frame Even Normal"     : KFBox.DEFAULT_COLOR["Frame Even Normal"],
        "Frame Even Highlight"  : KFBox.DEFAULT_COLOR["Frame Even Highlight"],
        "Frame Odd Normal"      : KFBox.DEFAULT_COLOR["Frame Odd Normal"],
        "Frame Odd Highlight"   : KFBox.DEFAULT_COLOR["Frame Odd Highlight"],
        "Frame Line Normal"     : KFBox.DEFAULT_COLOR["Frame Line Normal"],
        "Frame Line Every 5"    : KFBox.DEFAULT_COLOR["Frame Line Every 5"],
        "Frame Line Highlight"  : KFBox.DEFAULT_COLOR["Frame Line Highlight"],
        "Frame Selecting Box"   : KFBox.DEFAULT_COLOR["Frame Selecting Box"],
        # Icon
        "Icon Keyframe"         : KFButton.DEFAULT_COLOR["Icon Keyframe"],
        "Icon Keyframe Active"  : KFButton.DEFAULT_COLOR["Icon Keyframe Active"],
        "Icon Group"            : KFButton.DEFAULT_COLOR["Icon Group"],
        "Icon Group Active"     : KFButton.DEFAULT_COLOR["Icon Group Active"],
        "Icon Edge Normal"      : KFButton.DEFAULT_COLOR["Icon Edge Normal"],
        "Icon Edge Light"       : KFButton.DEFAULT_COLOR["Icon Edge Light"],
        "Icon Edge Dark"        : KFButton.DEFAULT_COLOR["Icon Edge Dark"],
    }

    @property
    def CurrentColors(self):
        return {
        # NameList
        "Name Text Normal"      : self.keyframeNameList.colorTextNormal, 
        "Name Text Group"       : self.keyframeNameList.colorTextGroup, 
        "Name Text Highlight"   : self.keyframeNameList.colorTextHighlight, 
        "Name Row Odd"          : self.keyframeNameList.colorRowBgOdd,
        "Name Row Even"         : self.keyframeNameList.colorRowBgEven,
        "Name Row Border"       : self.keyframeNameList.colorRowBorder,
        "Name Column Border"    : self.keyframeNameList.colorColumnBorder, 
        # Header
        "Header Text Normal"    : self.keyframeNo.colorTextNormal,
        "Header Text Highlight" : self.keyframeNo.colorTextHighlight,
        "Header Border"         : self.keyframeNo.colorHeaderBorder,
        "Header Background"     : self.keyframeNo.colorHeaderBackground,
        # Frame
        "Frame Row Border"      : self.keyframeBox.colorRowBorder,
        "Frame Even Normal"     : self.keyframeBox.colorRowEvenNormal,
        "Frame Even Highlight"  : self.keyframeBox.colorRowEvenHighlight,
        "Frame Odd Normal"      : self.keyframeBox.colorRowOddNormal,
        "Frame Odd Highlight"   : self.keyframeBox.colorRowOddHighlight,
        "Frame Line Normal"     : self.keyframeBox.colorLineNormal,
        "Frame Line Every 5"    : self.keyframeBox.colorLine5,
        "Frame Line Highlight"  : self.keyframeBox.colorLineHighlight,
        "Frame Selecting Box"   : self.keyframeBox.colorSelectingBox,
        # Icon
        "Icon Keyframe"         : KFButton.FRAMENORMALCOLOR,
        "Icon Keyframe Active"  : KFButton.FRAMEACTIVECOLOR,
        "Icon Group"            : KFButton.GROUPNORMALCOLOR,
        "Icon Group Active"     : KFButton.GROUPACTIVECOLOR,
        "Icon Edge Normal"      : KFButton.BORDERNORMALCOLOR,
        "Icon Edge Light"       : KFButton.BORDERLIGHTCOLOR,
        "Icon Edge Dark"        : KFButton.BORDERDARKCOLOR,
        }


    def __init__(self, rect):
        # ui setting
        self.frameNameWidth = 100
        self.frameNoHeight = 15
        self.scrollSpace = 15
        self.viewRect = rect
        self.cursorAlwaysFollow = False
        self.enable = True

        # event callback
        self.onFrameChangeCallback = None
        self.onRowSelecCallback = None
        self.onExpanedGroupCallback = None
        self.onKeyframeMoveCallback = None
        self.onKeyframeSelectCallback = None

        # keyframe
        self.keyframeBuffer = {}

        # ui runtime
        self.rowFrom = 0
        self.rowTotal = 0
        self.colFrom = 0
        self.colTotal = 0
        self.lastFrameColumn = 0
        self.colNum = int((rect.width - self.frameNameWidth - self.scrollSpace) / KFBox.COLUMNWIDTH)
        self.rowNum = int((rect.height - self.frameNoHeight - self.scrollSpace) / KFBox.ROWHEIGHT)
        self.frameIconChangeColor = False

        # ui current
        self.curFrame = 0
        self.defaultScroll = "v"
        self.curNameWidth = rect.width - (self.colNum * KFBox.COLUMNWIDTH + 4) - self.scrollSpace
        self.curNoHeight = rect.height - (self.rowNum * KFBox.ROWHEIGHT + 1) - self.scrollSpace
        
        # keyframe box
        self.keyframeBox = KFBox(rect.x + self.curNameWidth, rect.y + self.curNoHeight, self.colNum, self.rowNum)
        # frame no line
        self.keyframeNo = KFNoHeader(rect.x + self.curNameWidth, rect.y, self.colNum, self.curNoHeight)
        # frame name column
        self.keyframeNameList = KFNameList(rect.x, rect.y + self.curNoHeight, self.curNameWidth, self.rowNum)
        # H-scroller
        self.hPrevBtnRect = Rect(self.keyframeBox.boxRect.xMin, self.keyframeBox.boxRect.yMax + 1, 20, self.scrollSpace)
        self.hScrollRect = Rect(self.hPrevBtnRect.xMax + 1, self.hPrevBtnRect.yMin, self.keyframeBox.boxRect.width - 42, self.scrollSpace - 1)
        self.hNextBtnRect = Rect(self.hScrollRect.xMax + 1, self.hScrollRect.yMin, 20, self.hPrevBtnRect.height)
        # V-scroller
        self.vScrollRect = Rect(self.keyframeBox.boxRect.xMax + 1, self.keyframeBox.boxRect.yMin, self.scrollSpace - 1, self.keyframeBox.boxRect.height)
        # current button
        self.currentBtnRect = Rect(rect.x, self.keyframeBox.boxRect.yMax + 1, self.curNameWidth - 1 - 1 - 20, self.scrollSpace)
        # scroll dir button
        self.hScrDirBtnRect = Rect(self.hNextBtnRect.xMax + 1, self.hScrollRect.yMin, self.scrollSpace, self.hPrevBtnRect.height)
        # name resize button
        nameResizeBtnRect = Rect(rect.x + self.curNameWidth - 1 - 20, self.keyframeBox.boxRect.yMax + 1, 20, self.scrollSpace)
        self.nameResizeBtn = DragButton("nameresize", nameResizeBtnRect, u"\u2194")
        self.nameResizeBtn.value = self.frameNameWidth
        self.nameResizeBtn.valueMin = self.frameNameWidth / 2
        self.nameResizeBtn.valueMax = rect.width / 2
        self.nameResizeBtn.dragRateY = 0

    def changeColor(self, key, color, autoAdj=False):
        self.changeColors(((key, color),), autoAdj)

    def changeColors(self, colorInfos, autoAdj=False):
        namelistRepaint = False
        frameNoRepaint = False
        keyframeRepaint = False
        for key, color in colorInfos:
            if autoAdj:
                color = unity_adjust_color(color)
            # NameList
            if key == "Name Text Normal":
                self.keyframeNameList.colorTextNormal = color
            elif key == "Name Text Group":
                self.keyframeNameList.colorTextGroup = color
            elif key == "Name Text Highlight":
                self.keyframeNameList.colorTextHighlight = color
            elif key == "Name Row Odd":
                self.keyframeNameList.colorRowBgOdd = color
                namelistRepaint = True
            elif key == "Name Row Even":
                self.keyframeNameList.colorRowBgEven = color
                namelistRepaint = True
            elif key == "Name Row Border":
                self.keyframeNameList.colorRowBorder = color
                namelistRepaint = True
            elif key == "Name Column Border":
                self.keyframeNameList.colorColumnBorder = color
                namelistRepaint = True
            # Header
            elif key == "Header Text Normal":
                self.keyframeNo.colorTextNormal = color
            elif key == "Header Text Highlight":
                self.keyframeNo.colorTextHighlight = color
            elif key == "Header Border":
                self.keyframeNo.colorHeaderBorder = color
                frameNoRepaint = True
            elif key == "Header Background":
                self.keyframeNo.colorHeaderBackground = color
                frameNoRepaint = True
            # Frame
            elif key == "Frame Row Border": 
                self.keyframeBox.colorRowBorder = color
                keyframeRepaint = True
            elif key == "Frame Even Normal": 
                self.keyframeBox.colorRowEvenNormal = color
                keyframeRepaint = True
            elif key == "Frame Even Highlight": 
                self.keyframeBox.colorRowEvenHighlight = color
                keyframeRepaint = True
            elif key == "Frame Odd Normal": 
                self.keyframeBox.colorRowOddNormal = color
                keyframeRepaint = True
            elif key == "Frame Odd Highlight": 
                self.keyframeBox.colorRowOddHighlight = color
                keyframeRepaint = True
            elif key == "Frame Line Normal": 
                self.keyframeBox.colorLineNormal = color
                keyframeRepaint = True
            elif key == "Frame Line Every 5": 
                self.keyframeBox.colorLine5 = color
                keyframeRepaint = True
            elif key == "Frame Line Highlight": 
                self.keyframeBox.colorLineHighlight = color
                keyframeRepaint = True
            elif key == "Frame Selecting Box": 
                self.keyframeBox.colorSelectingBox = color
            # Icon
            elif key == "Icon Keyframe": 
                KFButton.FRAMENORMALCOLOR = color
                self.frameIconChangeColor = True
            elif key == "Icon Keyframe Active": 
                KFButton.FRAMEACTIVECOLOR = color
                self.frameIconChangeColor = True
            elif key == "Icon Group": 
                KFButton.GROUPNORMALCOLOR = color
                self.frameIconChangeColor = True
            elif key == "Icon Group Active": 
                KFButton.GROUPACTIVECOLOR = color
                self.frameIconChangeColor = True
            elif key == "Icon Edge Normal": 
                KFButton.BORDERNORMALCOLOR = color
                self.frameIconChangeColor = True
            elif key == "Icon Edge Light": 
                KFButton.BORDERLIGHTCOLOR = color
                self.frameIconChangeColor = True
            elif key == "Icon Edge Dark": 
                KFButton.BORDERDARKCOLOR = color
                self.frameIconChangeColor = True

        if namelistRepaint:
            self.keyframeNameList.baseNeedRepaint = True
        if frameNoRepaint:
            self.keyframeNo.baseNeedRepaint = True
        if keyframeRepaint:
            self.keyframeBox.setupColor32()
            self.keyframeBox.baseNeedRepaint = True

    def resize(self, rect, nameWidth):
        # update base infomation
        if rect != None and self.viewRect != rect:
            self.viewRect = rect
        else:
            rect = self.viewRect
        if nameWidth != None and self.frameNameWidth != nameWidth:
            self.frameNameWidth = nameWidth
        # update runtime
        self.colNum = int((rect.width - self.frameNameWidth - self.scrollSpace) / KFBox.COLUMNWIDTH)
        self.rowNum = int((rect.height - self.frameNoHeight - self.scrollSpace) / KFBox.ROWHEIGHT)
        self.curNameWidth = rect.width - (self.colNum * KFBox.COLUMNWIDTH + 4) - self.scrollSpace
        self.curNoHeight = rect.height - (self.rowNum * KFBox.ROWHEIGHT + 1) - self.scrollSpace
        # update inner controllers
        self.keyframeBox.resize(rect.x + self.curNameWidth, rect.y + self.curNoHeight, self.colNum, self.rowNum)
        self.keyframeNo.resize(rect.x + self.curNameWidth, rect.y, self.colNum, self.curNoHeight)
        self.keyframeNameList.resize(rect.x, rect.y + self.curNoHeight, self.curNameWidth, self.rowNum)
        self.hPrevBtnRect.Set(self.keyframeBox.boxRect.xMin, self.keyframeBox.boxRect.yMax + 1, 20, self.scrollSpace)
        self.hScrollRect.Set(self.hPrevBtnRect.xMax + 1, self.hPrevBtnRect.yMin, self.keyframeBox.boxRect.width - 42, self.scrollSpace - 1)
        self.hNextBtnRect.Set(self.hScrollRect.xMax + 1, self.hScrollRect.yMin, 20, self.hPrevBtnRect.height)
        self.vScrollRect.Set(self.keyframeBox.boxRect.xMax + 1, self.keyframeBox.boxRect.yMin, self.scrollSpace - 1, self.keyframeBox.boxRect.height)
        self.currentBtnRect.Set(rect.x, self.keyframeBox.boxRect.yMax + 1, self.curNameWidth - 1 - 1 - 20, self.scrollSpace)
        self.hScrDirBtnRect.Set(self.hNextBtnRect.xMax + 1, self.hScrollRect.yMin, self.scrollSpace, self.hPrevBtnRect.height)
        self.nameResizeBtn.resize(Rect(rect.x + self.curNameWidth - 1 - 20, self.keyframeBox.boxRect.yMax + 1, 20, self.scrollSpace))

    def setCurrentFrame(self, newCol, autoScrollTo = True):
        # update highlight column
        self.curFrame = newCol
        self.keyframeNo.highlightColumn = newCol
        self.keyframeBox.setHighlightColumn(newCol)
        if autoScrollTo:
            self.scrollToCurrentFrame()

    def setEnable(self, enable):
        if enable and not self.enable:
            self.enable = True
            self.keyframeNo.highlightColumn = self.curFrame
            self.keyframeBox.setHighlightColumn(self.curFrame)
            self.scrollToCurrentFrame()
        elif not enable and self.enable:
            self.enable = False
            self.keyframeNo.highlightColumn = -1
            self.keyframeBox.setHighlightColumn(-1)

    def scrollH(self, tgtCol):
        if self.colFrom == tgtCol:
            return
        self.colFrom = tgtCol
        if self.colFrom + self.colNum <= self.lastFrameColumn:
            self.colTotal = self.lastFrameColumn
        else:
            self.colTotal = self.colFrom + self.colNum
        if self.colFrom < 0:
            self.colFrom = 0
        # parts
        self.keyframeBox.scroll(self.colFrom, self.rowFrom)
        self.keyframeNo.scroll(self.colFrom)

    def scrollV(self, tgtRow):
        if self.rowFrom == tgtRow:
            return
        self.rowFrom = tgtRow
        if self.rowFrom > self.rowTotal - self.rowNum:
            self.rowFrom = self.rowTotal - self.rowNum
        if self.rowFrom < 0:
            self.rowFrom = 0
        # parts
        self.keyframeBox.scroll(self.colFrom, self.rowFrom)
        self.keyframeNameList.scroll(self.rowFrom)

    def scrollToCurrentFrame(self):
        if self.curFrame > int(self.colNum * 0.6):
            if self.cursorAlwaysFollow:
                self.scrollH(self.curFrame - int(self.colNum * 0.6))
            else:
                if self.curFrame < self.colFrom + 3:
                    self.scrollH(self.curFrame - int(self.colNum * 0.6))
                if self.curFrame > self.colFrom + self.colNum - 3:
                    self.scrollH(self.curFrame - int(self.colNum * 0.4))
        else:
            self.scrollH(0)

    def eventHandler(self):
        # scroll
        if self.viewRect.Contains(Event.current.mousePosition) and Event.current.type == EventType.ScrollWheel:
            if Event.current.delta.y > 0:
                if self.defaultScroll == "v":
                    if Event.current.shift:
                        self.scrollH(self.colFrom + 1)
                    else:
                        self.scrollV(self.rowFrom + 1)
                else:
                    if Event.current.shift:
                        self.scrollV(self.rowFrom + 1)
                    else:
                        self.scrollH(self.colFrom + 1)
            if Event.current.delta.y < 0:
                if self.defaultScroll == "v":
                    if Event.current.shift:
                        self.scrollH(self.colFrom - 1)
                    else:
                        self.scrollV(self.rowFrom - 1)
                else:
                    if Event.current.shift:
                        self.scrollV(self.rowFrom - 1)
                    else:
                        self.scrollH(self.colFrom - 1)
        # key
        #if Event.current.isKey:
            #print Event.current
            #Event.current.Use()
        # sub parts
        if Event.current.isMouse:
            # header
            ev = self.keyframeNo.mouseHandler()
            if ev and ev[0] == "onFrameChange":
                self.onFrameChange(ev[1])
            # namelist
            ev = self.keyframeNameList.mouseHandler()
            if ev and ev[0] == "onRowSelect":
                self.onRowSelect(ev[1])
            if ev and ev[0] == "onExpanedGroup":
                self.onExpanedGroup(ev[1], ev[2])
            # box
            ev = self.keyframeBox.mouseHandler()
            if ev and ev[0] == "onKeyframeMove":
                self.onKeyframeMove(ev[1])
            if ev and ev[0] == "onKeyframeSelect":
                self.onKeyframeSelect(ev[1])
            # resize
            ev = self.nameResizeBtn.mouseHandler()
            if ev and ev[0] == "onDragMoving":
                self.resize(None, int(ev[2]))

    def onFrameChange(self, newCol):
        # update highlight column
        self.curFrame = newCol
        self.keyframeBox.setHighlightColumn(newCol)
        self.scrollToCurrentFrame()
        # callback
        if self.onFrameChangeCallback:
            self.onFrameChangeCallback(self)

    def onRowSelect(self, selRowIndex):
        self.keyframeBox.setHighlightRows(selRowIndex)
        # callback
        if self.onRowSelecCallback:
            self.onRowSelecCallback(self)

    def onExpanedGroup(self, arrInfo, selRowIndex):
        self.keyframeBox.arrangeKeyframe(arrInfo)
        self.keyframeBox.setHighlightRows(selRowIndex)
        self.rowTotal = len(arrInfo)
        # callback
        if self.onExpanedGroupCallback:
            self.onExpanedGroupCallback(self)

    def onKeyframeMove(self, keyMoveInfo):
        # move keyframes
        for kmi in keyMoveInfo:
            del self.keyframeBuffer[kmi[0]][kmi[1]]
        for kmi in keyMoveInfo:
            self.keyframeBuffer[kmi[0]][kmi[2]] = kmi[3]
        # callback
        if self.onKeyframeMoveCallback:
            self.onKeyframeMoveCallback(self, keyMoveInfo)

    def onKeyframeSelect(self, keySelectInfo):
        # callback
        if self.onKeyframeSelectCallback:
            self.onKeyframeSelectCallback(self)

    def paint(self):
        # event
        if self.enable:
            self.eventHandler()

        # change frame icon
        if self.frameIconChangeColor:
            self.frameIconChangeColor = False
            KFButton.InitButtonTexture(True)

        # keyframe box
        self.keyframeBox.paint(self.enable)
        # frame no line
        self.keyframeNo.paint(self.enable)
        # frame name column
        self.keyframeNameList.paint(self.enable)
        # H-scroller
        color = GUI.color
        if (self.defaultScroll == "h" and not Event.current.shift) or (self.defaultScroll == "v" and Event.current.shift):
            GUI.color = Color.green
        hPos = GUI.HorizontalScrollbar(self.hScrollRect, self.colFrom, self.colNum, 0, self.colTotal)
        if int(hPos) != self.colFrom:
            self.scrollH(int(hPos))
        # V-scroller
        if (self.defaultScroll == "v" and not Event.current.shift) or (self.defaultScroll == "h" and Event.current.shift):
            GUI.color = Color.green
        else:
            GUI.color = color
        vPos = GUI.VerticalScrollbar(self.vScrollRect, self.rowFrom, self.rowNum, 0, self.rowTotal)
        if int(vPos) != self.rowFrom:
            self.scrollV(int(vPos))
        GUI.color = color
        # scroll button
        if GUI.Button(self.hPrevBtnRect, "<"):
            self.scrollH(self.colFrom - 1)
        if GUI.Button(self.hNextBtnRect, ">"):
            self.scrollH(self.colFrom + 1)
        # current button
        if GUI.Button(self.currentBtnRect, "current"):
            self.scrollToCurrentFrame()
        # scroll direction
        if GUI.Button(self.hScrDirBtnRect, "|" if self.defaultScroll == "v" else "---"):
            if self.defaultScroll == "v":
                self.defaultScroll = "h"
            else:
                self.defaultScroll = "v"
        # name resize button
        self.nameResizeBtn.paint()
        GUI.color = color

    def clearKeyframe(self):
        #print "[KEYFRAMEVIEW] cleanup for keyframe import"
        self.keyframeBuffer = {}

        self.keyframeNameList.clearName()
        self.keyframeBox.clearKeyframe()

    def importFrameName(self, groupName, frameName):
        # add to name list
        self.keyframeNameList.appendName(groupName, frameName)
        # prepare keyframe
        bKey = groupName + "#" + frameName
        if bKey not in self.keyframeBuffer:
            self.keyframeBuffer[bKey] = {}

    def importKeyframe(self, groupName, frameName, frameNo, keyframe):
        # store in keyframe buffer
        bKey = groupName + "#" + frameName
        if bKey in self.keyframeBuffer:
            self.keyframeBuffer[bKey][frameNo] = keyframe
        else:
            self.keyframeBuffer[bKey] = {frameNo: keyframe}
        # add to name list
        self.keyframeNameList.appendName(groupName, frameName)

    def arrangeImportedKeyframes(self):
        # arrange frame names
        self.rowTotal = self.keyframeNameList.arrangeName()
        # arrange keyframes
        self.colTotal = 0
        fmTotal = 0
        for bKey in self.keyframeBuffer.keys():
            for colIdx in self.keyframeBuffer[bKey].keys():
                if colIdx > self.colTotal:
                    self.colTotal = colIdx
                self.keyframeBox.appendKeyframe(bKey, colIdx, self.keyframeBuffer[bKey][colIdx])
                fmTotal += 1
        self.keyframeBox.arrangeKeyframe(self.keyframeNameList.arrangedNames)
        # setup last frame
        self.colTotal += 1
        self.lastFrameColumn = self.colTotal
        print "[KEYFRAMEVIEW] imported %d keyframes, last frame @%d"%(fmTotal, self.lastFrameColumn - 1)

    def registerKeyframe(self, groupName, frameName, frameNo, keyframe):
        # store in keyframe buffer
        bKey = groupName + "#" + frameName
        if bKey not in self.keyframeNameList.fullGroupNameKeys:
            raise Exception("registerKeyframe CANNOT add frame to new group or bone!")
        if bKey in self.keyframeBuffer:
            newFlg = frameNo not in self.keyframeBuffer[bKey]
            self.keyframeBuffer[bKey][frameNo] = keyframe
        else:
            newFlg = True
            self.keyframeBuffer[bKey] = {frameNo: keyframe}
        # update keyframes
        if newFlg:
            self.keyframeBox.appendKeyframe(bKey, frameNo, keyframe)
        else:
            self.keyframeBox.updateKeyframeObject(bKey, frameNo, keyframe)
        # update max column
        if newFlg and frameNo >= self.colTotal:
            self.lastFrameColumn = self.colTotal = frameNo + 1
        # Notice: should at least call self.keyframeBox.arrangeKeyframe after all done
        return newFlg

    def refreshKeyframe(self):
        # refresh name list first
        self.rowTotal = self.keyframeNameList.arrangeName()
        # refresh frame box
        self.keyframeBox.arrangeKeyframe(self.keyframeNameList.arrangedNames)

    def selectBonesByName(self, selBones):
        selKeys = [self.keyframeNameList.getGroup(b) + "#" + b for b in selBones]
        self.selectBonesByKey(selKeys)

    def selectBonesByKey(self, selKeys):
        selRowIndex = self.keyframeNameList.setHighlight(selKeys)
        self.keyframeBox.setHighlightRows(selRowIndex)

    def selectKeyframes(self, toSelectKeyframeInfo):
        self.keyframeBox.deselectAllButton()
        for bKey, col in toSelectKeyframeInfo:
            self.keyframeBox.selectButtonByCood(bKey, col)

    def deleteKeyframes(self, toDeleteKeyframeInfo):
        for bKey, col in toDeleteKeyframeInfo:
            self.keyframeBox.deleteKeyframe(bKey, col)
            del self.keyframeBuffer[bKey][col]
        self.keyframeBox.selectedKeyframes.clear()

