#===============================================================================================
# Key frame based animator framework 2
# this is a new version of vnanime, more powerful and useful
# v2.0 (countd360)
# - the initial version
# v2.1 (countd360)
# - bug fix
# - implemented Shift and Ctrl sub functions
# - implemented *Add* and *resort* in detail view
# - right click to register keyframe
# v2.2 (countd360)
# - support copy-to operation
# - support clip copy
# - support shortcuts
# v2.3 (countd360)
# - support color customize
# - support clip v1 copy to clip v2
# - support value editing
# - bug fix
# v2.4 (countd360)
# - add seekNormalized()
# - support re-assign when copy clip v2
# - skip unsupported properties when copy keyframes between object
# - auto add FK/IK subkeys
# - bug fix
# v2.4.1 (countd360)
# - adapt to new customwindow skin
# - fix pre-load error
# v2.5 (Countd360)
# - support for vnphy action/trigger
# - support garbage collect control
# - add videoexport mode in setting
# - move setting from scenedata to gdata
#===============================================================================================

import time
import math
import traceback
import copy
from os import path
from UnityEngine import Quaternion, Vector3, Vector2, Color, Rect, GameObject, Transform
from UnityEngine import GUI, GUILayout, GUIStyle, GUIContent, GUIUtility, TextAnchor, RectOffset
from UnityEngine import Time, Screen, Input, KeyCode, Event, EventType
from System import String, Array, Action, GC
from vngameengine import HSNeoOCI, HSNeoOCIChar, HSNeoOCIProp, HSNeoOCIFolder, HSNeoOCILight, HSNeoOCIRoute, HSNeoOCIText, GData
from vnframe import paramInterpolater, script2string, scriptHelperGUIMessage, scriptCopy, dumpToJsonString, loadFromJsonString, VNCheckGarbageCollect, VNEnableGarbageCollect, VNDoGarbageCollectNow
import unity_util

class Keyframe2:
    def __init__(self, fNo=0, fValue=None, fCurve=None):
        self.frameNo = fNo
        self.value = fValue
        self.curve = fCurve # [x1, x2, y1, y2] format, same as clip v1 but diff with vmd

    def copy(self):
        nf = Keyframe2(self.frameNo, scriptCopy(self.value))
        if self.curve:
            nf.curve = copy.copy(self.curve)
        return nf

    def __str__(self):
        if self.curve == None:
            return "[%X]@%d, v=%s"%(id(self), self.frameNo, self.value)
        else:
            return "[%X]@%d, v=%s, c=[%.2f,%.2f,%.2f,%.2f]"%(id(self), self.frameNo, self.value, self.curve[0], self.curve[1], self.curve[2], self.curve[3])

    @property
    def IsLinear(self):
        if self.curve:
            return self.curve[0] == self.curve[2] and self.curve[1] == self.curve[3]
        else:
            return None

    def cubicBezierInterpolate2(self, x):
        def sampleBezier(handle1, handle2, t):
            zero = Vector2.zero
            full = Vector2(1.0, 1.0)
            v1 = Vector2.Lerp(zero, handle1, t)
            v2 = Vector2.Lerp(handle1, handle2, t)
            v3 = Vector2.Lerp(handle2, full, t)
            v4 = Vector2.Lerp(v1, v2, t)
            v5 = Vector2.Lerp(v2, v3, t)
            v6 = Vector2.Lerp(v4, v5, t)
            return v6 
        # check
        cchk = self.IsLinear
        if cchk == None:
            return None
        elif cchk:
            return x
        # interploate
        handle1 = Vector2(self.curve[0], self.curve[2])
        handle2 = Vector2(self.curve[1], self.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


class Keyframe2Bundle:
    def __init__(self, fBundle=None):
        self.frameNo = None if fBundle == None else fBundle.values()[0].frameNo  # choose one frameNo, all should be the same
        self.bundle = fBundle
        self.curve = None #if fBundle == None else fBundle.values()[0].curve  # choose one curve

    def copy(self):
        nf = Keyframe2Bundle(self.bundle)
        return nf

    def __str__(self):
        return "[%X]@%d, bundle:%s"%(id(self), self.frameNo, [str(kfk)+":"+str(self.bundle[kfk]) for kfk in self.bundle])


class KeyframeSet:
    def __init__(self):
        #setting
        self.updateBeforeFirst = None
        self.updateAfterLast = None
        self.interpolateType = None
        # data
        self.keyframes = {}
        self.keyframeNos = []
        self.workingIndex = None

    @property
    def HasKeyframe(self):
        return len(self.keyframes) > 0

    @property
    def KeyframeCount(self):
        return len(self.keyframes)

    @property
    def FirstFrameNo(self):
        return self.keyframeNos[0] if self.HasKeyframe else 0

    @property
    def LastFrameNo(self):
        return self.keyframeNos[-1] if self.HasKeyframe else 0

    def clearData(self):
        self.keyframes = {}
        self.keyframeNos = []
        self.workingIndex = None

    def copyData(self):
        newSet = KeyframeSet()
        # setting
        newSet.updateBeforeFirst = self.updateBeforeFirst
        newSet.updateAfterLast = self.updateAfterLast
        newSet.interpolateType = self.interpolateType
        # data
        for frameNo in self.keyframeNos:
            kf = self.keyframes[frameNo]
            newSet.keyframes[frameNo] = kf.copy()
        newSet.keyframeNos = sorted(newSet.keyframes.keys())
        newSet.buildWorkingIndex()
        return newSet

    def exportData(self):
        keyframeData = []
        for frameNo in self.keyframeNos:
            kf = self.keyframes[frameNo]
            kfdata = [kf.frameNo, kf.value, kf.curve]
            keyframeData.append(kfdata)
        return keyframeData

    def importData(self, keyframeData):
        self.keyframes = {}
        for kfdata in keyframeData:
            kf = Keyframe2(kfdata[0], kfdata[1], kfdata[2])
            self.keyframes[kfdata[0]] = kf
        self.keyframeNos = sorted(self.keyframes.keys())
        self.buildWorkingIndex()

    def appendData(self, keyframeData):
        for kfdata in keyframeData:
            kf = Keyframe2(kfdata[0], kfdata[1], kfdata[2])
            self.keyframes[kfdata[0]] = kf
        self.keyframeNos = sorted(self.keyframes.keys())
        self.buildWorkingIndex()

    def getKeyframe(self, frameNo):
        if frameNo in self.keyframes:
            return self.keyframes[frameNo]
        else:
            return None

    def getKeyframeList(self):
        return [self.keyframes[i] for i in self.keyframeNos]

    def newKeyframe(self, newFrame):
        if newFrame.frameNo not in self.keyframes:
            # only add new keyframe
            self.keyframes[newFrame.frameNo] = newFrame
            self.keyframeNos = sorted(self.keyframes.keys())
            self.buildWorkingIndex()

    def updateKeyframes(self, keyframes):
        hasnew = False
        for kf in keyframes:
            if kf.frameNo not in self.keyframes:
                hasnew = True
            # update or new
            self.keyframes[kf.frameNo] = kf
        if hasnew:
            # rebuild setting
            self.keyframeNos = sorted(self.keyframes.keys())
            self.buildWorkingIndex()

    def moveKeyframes(self, moveInfos):
        haschg = False
        setToMove = moveInfos
        while len(setToMove) > 0:
            haschg_inloop = False
            remainToMove = set()
            for (fromFrame, toFrame) in setToMove:
                if fromFrame not in self.keyframes:
                    print("WARNING in move keyframe: from frame not valid! Ignored!")
                    continue
                if toFrame in self.keyframes:
                    remainToMove.add((fromFrame, toFrame))
                    continue
                kf = self.keyframes.pop(fromFrame)
                kf.frameNo = toFrame
                self.keyframes[toFrame] = kf
                haschg = True
                haschg_inloop = True
            if not haschg_inloop:
                if len(remainToMove):
                    print("WARNING in move keyframe: following remove set not performed! \n  " + str(remainToMove))
                break
            else:
                setToMove = remainToMove
        # rebuild index
        if haschg:
            self.keyframeNos = sorted(self.keyframes.keys())
            self.buildWorkingIndex()

    def deleteKeyframes(self, keyframeNos):
        haschg = False
        for kfno in keyframeNos:
            if kfno in self.keyframes:
                del self.keyframes[kfno]
                self.keyframeNos.remove(kfno)
                haschg = True
            else:
                #print("WARNING in delete keyframe: target frameNo not existed! Ignored!")
                pass
        # rebuild index
        if haschg:
            self.buildWorkingIndex()

    def buildWorkingIndex(self):
        if len(self.keyframes):
            wi = GData()
            wi.prevIndex = 0
            wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
            wi.nextIndex = 1 if len(self.keyframes) > 1 else 0
            wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
            wi.firstFrameNo = self.keyframeNos[0]
            wi.lastFrameNo = self.keyframeNos[-1]
            wi.prevUpdate = -1
            self.workingIndex = wi
        else:
            self.workingIndex = None
    
    def resetWorkingIndex(self):
        if self.workingIndex != None:
            wi = self.workingIndex
            wi.prevIndex = 0
            wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
            wi.nextIndex = 1 if len(self.keyframes) > 1 else 0
            wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
            wi.firstFrameNo = self.keyframeNos[0]
            wi.lastFrameNo = self.keyframeNos[-1]
            wi.prevUpdate = -1

    def getFrame(self, tgtFrameNo):
        # work index mode
        if self.workingIndex == None:
            return None
        wi = self.workingIndex
        # trigger type interpolate
        if self.interpolateType == "T":
            # play the same frame
            if wi.prevUpdate == tgtFrameNo and wi.prevUpdate == wi.prevFrameNo:
                #print("T-Type keyframe: play the same frame %d"%wi.prevFrameNo)
                return self.keyframes[wi.prevFrameNo]
            # index search
            if tgtFrameNo in self.keyframes:
                wi.prevIndex = self.keyframeNos.index(tgtFrameNo)
                wi.nextIndex = wi.prevIndex + 1 if len(self.keyframes) > wi.prevIndex + 1 else wi.prevIndex
            elif tgtFrameNo <= wi.firstFrameNo:
                wi.prevIndex = 0
                wi.nextIndex = wi.prevIndex + 1 if len(self.keyframes) > wi.prevIndex + 1 else wi.prevIndex
            elif tgtFrameNo >= wi.lastFrameNo:
                wi.prevIndex = len(self.keyframes) - 1
                wi.nextIndex = wi.prevIndex
            elif tgtFrameNo < wi.prevFrameNo:
                for i in range(len(self.keyframes)):
                    if self.keyframeNos[i] > tgtFrameNo:
                        wi.prevIndex = i - 1
                        wi.nextIndex = i
                        break
            else:
                while tgtFrameNo > wi.nextFrameNo:
                    wi.prevIndex = wi.nextIndex
                    wi.nextIndex = wi.prevIndex + 1 if len(self.keyframes) > wi.prevIndex + 1 else wi.prevIndex
                    wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
            wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
            wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
            # trigger frame
            if wi.prevUpdate < wi.prevFrameNo and tgtFrameNo >= wi.prevFrameNo:
                #print("T-Type keyframe: play the frame %d, trigger %f -> %f"%(wi.prevFrameNo, wi.prevUpdate, tgtFrameNo))
                tfk = self.keyframes[wi.prevFrameNo]
            else:
                tfk = None
            wi.prevUpdate = tgtFrameNo
            return tfk
        # Auto, Dis or Con type interpolate
        if tgtFrameNo in self.keyframes:
            wi.prevIndex = self.keyframeNos.index(tgtFrameNo)
            wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
            wi.nextIndex = wi.prevIndex + 1 if len(self.keyframes) > wi.prevIndex + 1 else wi.prevIndex
            wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
            return self.keyframes[wi.prevFrameNo]
        elif wi.firstFrameNo > tgtFrameNo:
            # target frame is before first keyframe
            wi.prevIndex = 0
            wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
            wi.nextIndex = 1 if len(self.keyframes) > 1 else 0
            wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
            if self.updateBeforeFirst:
                return self.keyframes[wi.firstFrameNo]
            else:
                return None
        elif wi.lastFrameNo < tgtFrameNo:
            # target frame is after last keyframe
            if self.updateAfterLast:
                return self.keyframes[wi.lastFrameNo]
            else:
                return None
        else:
            # firstFrameNo < tgtFrameNo < lastFrameNo
            if wi.prevFrameNo < tgtFrameNo < wi.nextFrameNo:
                # inside current range
                prevFrame = self.keyframes[wi.prevFrameNo]
                nextFrame = self.keyframes[wi.nextFrameNo]
            elif tgtFrameNo > wi.nextFrameNo:
                # seek foreward
                while 1:
                    wi.prevIndex += 1
                    wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
                    wi.nextIndex += 1
                    wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
                    if wi.prevFrameNo < tgtFrameNo < wi.nextFrameNo:
                        prevFrame = self.keyframes[wi.prevFrameNo]
                        nextFrame = self.keyframes[wi.nextFrameNo]
                        break
            elif tgtFrameNo < wi.prevFrameNo:
                # seek backward
                for i in range(len(self.keyframes)):
                    if self.keyframeNos[i] > tgtFrameNo:
                        wi.prevIndex = i - 1
                        wi.prevFrameNo = self.keyframeNos[wi.prevIndex]
                        wi.nextIndex = i
                        wi.nextFrameNo = self.keyframeNos[wi.nextIndex]
                        prevFrame = self.keyframes[wi.prevFrameNo]
                        nextFrame = self.keyframes[wi.nextFrameNo]
                        break
                #print "Seek backward for #%f: [%d]#%f - [%d]#%f"%(tgtFrameNo, wi.prevIndex, wi.prevFrameNo, wi.nextIndex, wi.nextFrameNo)
            else:
                # tgtFrameNo == wi.prevFrameNo:
                raise Exception("Impossible")

            if self.interpolateType == "D":
                # no interpolate
                return prevFrame
            elif self.interpolateType == "A" and wi.prevFrameNo + 1 == wi.nextFrameNo:
                # skip interpolate for consecutive keyframe
                return prevFrame
            else:
                # do interpolate
                tframe = float(tgtFrameNo - wi.prevFrameNo) / float(wi.nextFrameNo - wi.prevFrameNo)
                tx = nextFrame.cubicBezierInterpolate2(tframe)
                ivalue = paramInterpolater(prevFrame.value, nextFrame.value, tx)
                return Keyframe2(tgtFrameNo, ivalue)


class KeyframeController:
    VALID_UPDATE_TYPE = ("auto", "diff", "force")
    VALID_INTERPOLATE_TYPE = ("A", "C", "D", "T")
    VALID_CONTINUOUS_INTER_COUNT = 2

    def __init__(self, name, targetObj, actInfo):
        # setting
        self.name = name
        self.targetObj = targetObj
        self.updateFunc = actInfo[0]
        self.interpolatable = actInfo[1]

        if len(actInfo) >= 3 and actInfo[2] in KeyframeController.VALID_UPDATE_TYPE:
            self.updateType = actInfo[2]
        else:
            self.updateType = "auto"
        if len(actInfo) >= 4 and actInfo[3] in KeyframeController.VALID_INTERPOLATE_TYPE:
            self.interpolateType = actInfo[3]
        else:
            self.interpolateType = "A" if self.interpolatable else "D"
        self.updateBeforeFirst = True
        self.updateAfterLast = True
        # data
        self.keyframeSets = None
        self.keyframeSetKeys = None
        # runtime
        self.lastValue = None

    def __str__(self):
        return "keyframe ctrl: %s"%(self.name)

    @property
    def HasKeyframe(self):
        if self.keyframeSets:
            for kfset in self.keyframeSets:
                if kfset.HasKeyframe:
                    return True
        return False

    @property
    def KeyframeCount(self):
        c = 0
        for kfset in self.keyframeSets:
            c += kfset.KeyframeCount
        return c

    @property
    def FirstFrameNo(self):
        f = 0
        if self.keyframeSets:
            for kfset in self.keyframeSets:
                if kfset.FirstFrameNo < f:
                    f = kfset.FirstFrameNo
        return f

    @property
    def LastFrameNo(self):
        l = 0
        if self.keyframeSets:
            for kfset in self.keyframeSets:
                if kfset.LastFrameNo > l:
                    l = kfset.LastFrameNo
        return l

    @property
    def SingleSet(self):
        return self.keyframeSetKeys == None

    def clearKeyframes(self):
        for kfset in self.keyframeSets:
            kfset.clearData()

    def copy(self):
        newCtrl = KeyframeController(self.name, self.targetObj, (self.updateFunc, self.interpolatable))

        newCtrl.updateType = self.updateType
        newCtrl.interpolateType = self.interpolateType
        newCtrl.updateBeforeFirst = self.updateBeforeFirst
        newCtrl.updateAfterLast = self.updateAfterLast
        # data
        newCtrl.keyframeSets = []
        for kfSet in self.keyframeSets:
            newCtrl.keyframeSets.append(kfSet.copyData())
        newCtrl.keyframeSetKeys = None if self.SingleSet else copy.deepcopy(self.keyframeSetKeys)
        # runtime
        self.lastValue = None
        return newCtrl

    def exportSetting(self, keysFilter=None):
        setting = {}
        # general setting
        setting["updateType"] = self.updateType
        setting["updateBeforeFirst"] = self.updateBeforeFirst
        setting["updateAfterLast"] = self.updateAfterLast
        setting["interpolateType"] = self.interpolateType
        # contents
        setting["SingleSet"] = self.SingleSet
        if self.SingleSet:
            setting["keyframeSetKeys"] = None
            setting["keyframeData"] = [self.keyframeSets[0].exportData()]
        else:
            setting["keyframeSetKeys"] = []
            setting["keyframeData"] = []
            for subkey, subset in self.getKeySetPair:
                if keysFilter != None and subkey not in keysFilter:
                    continue
                setting["keyframeSetKeys"].append(subkey)
                setting["keyframeData"].append(subset.exportData())
        return setting

    def importSetting(self, setting):
        # notice: runtime setting leaves no change
        self.updateType = setting["updateType"]
        self.updateBeforeFirst = setting["updateBeforeFirst"]
        self.updateAfterLast = setting["updateAfterLast"]
        if "interpolateType" in setting:
            self.interpolateType = setting["interpolateType"]
        elif "interpolateConsecutive" in setting: # 2.0 data
            self.interpolateType = "D" if not self.interpolatable else ("C" if setting["interpolateConsecutive"] else "A")
        else:
            self.interpolateType = "A" if self.interpolatable else "D"
        # overwrite old keyframe sets
        if setting["SingleSet"]:
            keys = None
            setCount = 1
        else:
            keys = setting["keyframeSetKeys"]
            setCount = len(keys)
        self.createKeyframeSet(keys)
        # import data
        for i in range(setCount):
            self.keyframeSets[i].importData(setting["keyframeData"][i])

    def appendSetting(self, setting):
        # notice: runtime setting leaves no change
        self.updateType = setting["updateType"]
        self.updateBeforeFirst = setting["updateBeforeFirst"]
        self.updateAfterLast = setting["updateAfterLast"]
        if "interpolateType" in setting:
            self.interpolateType = setting["interpolateType"]
        elif "interpolateConsecutive" in setting: # 2.0 data
            self.interpolateType = "D" if not self.interpolatable else ("C" if setting["interpolateConsecutive"] else "A")
        else:
            self.interpolateType = "A" if self.interpolatable else "D"
        # add keyframe sets
        if setting["SingleSet"]:
            self.keyframeSets[0].appendData(setting["keyframeData"][0])
        else:
            for i in range(len(setting["keyframeSetKeys"])):
                key = setting["keyframeSetKeys"][i]
                if key in self.keyframeSetKeys:
                    tgtindex = self.keyframeSetKeys.index(key)
                    self.keyframeSets[tgtindex].appendData(setting["keyframeData"][i])
                else:
                    newKfSet = KeyframeSet()
                    newKfSet.importData(setting["keyframeData"][i])
                    self.keyframeSetKeys.append(key)
                    self.keyframeSets.append(newKfSet)

    def createKeyframeSet(self, keys):
        if keys == None:
            self.keyframeSets = [KeyframeSet()]
            self.keyframeSetKeys = None
        else:
            self.keyframeSets = []
            self.keyframeSetKeys = []
            for key in keys:
                self.keyframeSets.append(KeyframeSet())
                self.keyframeSetKeys.append(key)
        # align setting
        for kfset in self.keyframeSets:
            kfset.updateBeforeFirst = self.updateBeforeFirst
            kfset.updateAfterLast = self.updateAfterLast
            kfset.interpolateType = self.interpolateType

    def insertKeyframeSet(self, key):
        self.keyframeSetKeys.append(key)
        self.keyframeSetKeys.sort()
        newIdx = self.keyframeSetKeys.index(key)
        self.keyframeSets.insert(newIdx, KeyframeSet())

    def deleteKeyframeSet(self, key):
        if key in self.keyframeSetKeys:
            idx = self.keyframeSetKeys.index(key)
            del self.keyframeSetKeys[idx]
            del self.keyframeSets[idx]

    def updateSetting(self, updBeforeFirst, updAfterLast, intpoType):
        if updBeforeFirst != None:
            self.updateBeforeFirst = updBeforeFirst
        if updAfterLast != None:
            self.updateAfterLast = updAfterLast
        if intpoType in self.VALID_INTERPOLATE_TYPE:
            if self.interpolatable:
                if intpoType in KeyframeController.VALID_INTERPOLATE_TYPE:
                    self.interpolateType = intpoType
                else:
                    self.interpolateType = KeyframeController.VALID_INTERPOLATE_TYPE[0]
            else:
                if intpoType in KeyframeController.VALID_INTERPOLATE_TYPE[KeyframeController.VALID_CONTINUOUS_INTER_COUNT:]:
                    self.interpolateType = intpoType
                else:
                    self.interpolataType = KeyframeController.VALID_INTERPOLATE_TYPE[KeyframeController.VALID_CONTINUOUS_INTER_COUNT]
        for kfset in self.keyframeSets:
            kfset.updateBeforeFirst = self.updateBeforeFirst
            kfset.updateAfterLast = self.updateAfterLast
            kfset.interpolateType = self.interpolateType

    def getKeyframe(self, key, frameNo):
        if self.SingleSet:
            return self.keyframeSets[0].getKeyframe(frameNo)
        else:
            if key == None:
                kfs = {}
                for i in range(len(self.keyframeSets)):
                    kf = self.keyframeSets[i].getKeyframe(frameNo)
                    if kf != None: kfs[self.keyframeSetKeys[i]] = kf
                if len(kfs):
                    return Keyframe2Bundle(kfs)
                else:
                    return None
            elif key in self.keyframeSetKeys:
                return self.keyframeSets[self.keyframeSetKeys.index(key)].getKeyframe(frameNo)
            else:
                raise Exception("Invalid keyframeSet key %s for keyframe controller %s"%(str(key), self.name))

    def registerKeyframe(self, frameNo, newValue):
        def createKF(tgtKfSet, nValue):
            newkf = tgtKfSet.getKeyframe(frameNo)
            if newkf:
                newkf.value = nValue
            else:
                newkf = Keyframe2(frameNo, nValue, [0.2, 0.8, 0.2, 0.8] if self.interpolatable else None)
                tgtKfSet.newKeyframe(newkf)
            return newkf

        if self.SingleSet:
            tgtKeyframeSet = self.keyframeSets[0]
            self.lastValue = newValue
            return createKF(tgtKeyframeSet, newValue)
        elif isinstance(newValue, dict) and len(newValue) > 0:
            newkfs = {}
            for key in newValue:
                if key not in self.keyframeSetKeys:
                    self.insertKeyframeSet(key)
                tgtKeyframeSet = self.keyframeSets[self.keyframeSetKeys.index(key)]
                newkfs[key] = createKF(tgtKeyframeSet, newValue[key])
            self.lastValue = newValue
            return Keyframe2Bundle(newkfs)
        else:
            return None

    def updateKeyframes(self, keyframes):
        if self.SingleSet:
            self.keyframeSets[0].updateKeyframes(keyframes)
        else:
            regroup = {}
            for kfbundle in keyframes:
                for key in kfbundle.bundle:
                    kf = kfbundle.bundle[key]
                    if key in regroup:
                        regroup[key].add(kf)
                    else:
                        regroup[key] = {kf}
            for key in regroup:
                if key in self.keyframeSetKeys:
                    tgtKeyframeSet = self.keyframeSets[self.keyframeSetKeys.index(key)]
                else:
                    continue
                    #tgtKeyframeSet = KeyframeSet()
                    #self.keyframeSets.append(tgtKeyframeSet)
                    #self.keyframeSetKeys.append(key)
                tgtKeyframeSet.updateKeyframes(regroup[key])

    def moveKeyframes(self, keyframeMoveInfos):
        # keyframeMoveInfos may be a set of (from, to) for non-key specify, or {key: {(from, to)}} for keyed
        if isinstance(keyframeMoveInfos, dict) and not self.SingleSet:
            for key in keyframeMoveInfos:
                if key in self.keyframeSetKeys:
                    self.keyframeSets[self.keyframeSetKeys.index(key)].moveKeyframes(keyframeMoveInfos[key])
        else:
            for kfset in self.keyframeSets:
                kfset.moveKeyframes(keyframeMoveInfos)

    def deleteKeyframes(self, key, keyframeNos):
        if self.SingleSet:
            self.keyframeSets[0].deleteKeyframes(keyframeNos)
        else:
            if key == None:
                for kfset in self.keyframeSets:
                    kfset.deleteKeyframes(keyframeNos)
            elif key in self.keyframeSetKeys:
                self.keyframeSets[self.keyframeSetKeys.index(key)].deleteKeyframes(keyframeNos)
            else:
                raise Exception("Invalid keyframeSet key %s for keyframe controller %s"%(str(key), self.name))

    def getKeyframeList(self):
        if self.SingleSet and self.keyframeSets != None:
            kfset = self.keyframeSets[0]
            return kfset.getKeyframeList()
        elif not self.SingleSet:
            kfnoset = set()
            for kfset in self.keyframeSets:
                kfnoset = kfnoset.union(set(kfset.keyframeNos))
            kflist = []
            for frameNo in sorted(list(kfnoset)):
                kflist.append(self.getKeyframe(None, frameNo))
            return kflist
        else:
            return []

    def getKeyframeNoList(self):
        if self.SingleSet and self.keyframeSets != None:
            kfset = self.keyframeSets[0]
            return kfset.keyframeNos
        elif not self.SingleSet:
            kfnoset = set()
            for kfset in self.keyframeSets:
                kfnoset = kfnoset.union(kfset.keyframeNos)
            return sorted(list(kfnoset))
        else:
            return []

    @property
    def getKeySetPair(self):
        if self.SingleSet:
            return [(None, self.keyframeSets[0])]
        else:
            return [(self.keyframeSetKeys[i], self.keyframeSets[i]) for i in range(len(self.keyframeSetKeys))]

    def getKeyframeSetByKey(self, key):
        if not self.SingleSet and key in self.keyframeSetKeys:
            return self.keyframeSets[self.keyframeSetKeys.index(key)]
        else:
            return None
        
    def resetWorkingIndex(self):
        for kfSet in self.keyframeSets:
            kfSet.resetWorkingIndex()

    def getFrameValue(self, tgtFrameNo):
        # get current value
        if self.SingleSet:
            frm = self.keyframeSets[0].getFrame(tgtFrameNo)
            if frm:
                return frm.value
        else:
            value = {}
            for i in range(len(self.keyframeSetKeys)):
                frm = self.keyframeSets[i].getFrame(tgtFrameNo)
                if frm:
                    value[self.keyframeSetKeys[i]] = frm.value
            if len(value):
                return value
        # No value get from keyframe
        return None

    def setFrame(self, tgtFrameNo, referValue):
        # check setup
        if not self.targetObj or not self.updateFunc:
            raise Exception("KeyframeController <%s> miss target infomation, cannot set frame!"%self.name)
        # get current value
        frmValue = self.getFrameValue(tgtFrameNo)
        if frmValue == None:
            return
        # check update type
        if self.updateType == "auto" and frmValue == self.lastValue:
            #if self.name == "cam_rot":
            #    def strq(q):
            #        return "(%.4f, %.4f, %.4f)"%(q.eulerAngles[0], q.eulerAngles[1], q.eulerAngles[2])
            #    print("skip cam=rot upd %s -> %s, equal=%s, same=%s"%(strq(self.lastValue), strq(frmValue), frmValue == self.lastValue, KeyframeController.IsSameValue(frmValue, self.lastValue)))
            return
        if self.updateType == "diff" and frmValue == referValue:
            return
        # update
        self.updateFunc(self.targetObj, frmValue)
        self.lastValue = frmValue

    def statusDiffReport(self, tgtFrameNo, referValue):
        report = {}
        # get current value
        frmValue = self.getFrameValue(tgtFrameNo)
        # check current value
        if frmValue != None:
            if self.SingleSet:
                report[self.name] = "Same" if KeyframeController.IsSameValue(referValue, frmValue) else "Diff"
            else:
                sameCount = 0
                for key in self.keyframeSetKeys:
                    if key in referValue and key in frmValue:
                        if KeyframeController.IsSameValue(referValue[key], frmValue[key]): # referValue[key] == frmValue[key]:
                            report[key] = "Same"
                            sameCount += 1
                        else:
                            report[key] = "Diff"
                    else:
                        report[key] = "NewValue"
                if sameCount == len(report):
                    report[self.name] = "Same"
                elif sameCount == 0:
                    report[self.name] = "Diff"
                else:
                    report[self.name] = "PartSame"
        else:
            if self.SingleSet:
                report[self.name] = "NewValue"
            else:
                report[self.name] = "NewValue" if len(referValue) != 0 else "Same"
                for key in self.keyframeSetKeys:
                    if key in referValue:
                        report[key] = "NewValue"
        return report

    @staticmethod
    def IsSameValue(value1, value2, mdiff = 0.001):
        # ignore little difference
        try:
            if isinstance(value1, list) or isinstance(value1, tuple):
                for i in range(len(value1)):
                    if not KeyframeController.IsSameValue(value1[i], value2[i]):
                        return False
                return True
            elif isinstance(value1, dict):
                for subKey in value1.keys():
                    if not KeyframeController.IsSameValue(value1[subKey], value2[subKey]):
                        return False
                return True
            elif isinstance(value1, Vector3):
                if not isinstance(value2, Vector3):
                    value2 = Vector3(value2[0], value2[1], value2[2])
                return Vector3.Distance(value1, value2) < mdiff
            elif isinstance(value1, Vector2):
                if not isinstance(value2, Vector2):
                    value2 = Vector2(value2[0], value2[1])
                return Vector2.Distance(value1, value2) < mdiff
            elif isinstance(value1, Color):
                if not isinstance(value2, Color):
                    value2 = Color(value2[0], value2[1], value2[2], value2[3])
                return abs(value1.r - value2.r) < mdiff and abs(value1.g - value2.g) < mdiff and abs(value1.b - value2.b) < mdiff and abs(value1.a - value2.a) < mdiff
            elif isinstance(value1, Quaternion):
                if not isinstance(value2, Quaternion):
                    value2 = Quaternion(value2[0], value2[1], value2[2], value2[3])
                #def strq(q):
                #    return "(%.4f, %.4f, %.4f)"%(q.eulerAngles[0], q.eulerAngles[1], q.eulerAngles[2])
                #print("IsSameValue compare %s vs %s"%(strq(value1), strq(value2)))
                #print(" Angle: %f < %f = %s"%(Quaternion.Angle(value1, value2), mdiff, Quaternion.Angle(value1, value2) < mdiff))
                return Quaternion.Angle(value1, value2) < mdiff or Vector3.Distance(value1.eulerAngles, value2.eulerAngles) < mdiff
            elif isinstance(value1, float):
                return abs(value1 - value2) < mdiff
            else:
                return value1 == value2
        except:
            print("Compare value error!?")
            traceback.print_exc()
            return False


class KeyframeCtrlSet:
    SUB_KEY_REQ = {
        "actor": {"ik_set2", "fk_set2"},
        "prop": {"fk_set2", "videostate"},
    }

    def __init__(self, tgtObj, tgtId, tgtType):
        self.targetObj = tgtObj
        self.targetId = tgtId
        self.targetType = tgtType
        self.keyframeCtrls = []
        self.updateNeedDiff = False

    @property
    def HasKeyframe(self):
        for kfctrl in self.keyframeCtrls:
            if kfctrl.HasKeyframe:
                return True
        return False

    @property
    def FirstFrameNo(self):
        f = 0
        for kfctrl in self.keyframeCtrls:
            if kfctrl.FirstFrameNo < f:
                f = kfctrl.FirstFrameNo
        return f

    @property
    def LastFrameNo(self):
        l = 0
        for kfctrl in self.keyframeCtrls:
            if kfctrl.LastFrameNo > l:
                l = kfctrl.LastFrameNo
        return l

    def copy(self):
        newCtrlSet = KeyframeCtrlSet(self.targetObj, self.targetId, self.targetType)
        newCtrlSet.updateNeedDiff = self.updateNeedDiff
        for kfctrl in self.keyframeCtrls:
            newCtrlSet.keyframeCtrls.append(kfctrl.copy())
        return newCtrlSet

    def exportSetting(self):
        setting = {}
        # general setting
        setting["targetId"] = self.targetId
        # contents
        setting["controllers"] = {}
        for kfctrl in self.keyframeCtrls:
            setting["controllers"][kfctrl.name] = kfctrl.exportSetting()
        return setting

    def getKeyframeNoList(self):
        # TODO: optimize
        allkfnos = set()
        for kfctrl in self.keyframeCtrls:
            allkfnos = allkfnos.union(kfctrl.getKeyframeNoList())
        return sorted(list(allkfnos))

    def getKeyframeNameList(self):
        return [kfctrl.name for kfctrl in self.keyframeCtrls]

    def getKeyframeCtrl(self, name):
        for kfctrl in self.keyframeCtrls:
            if kfctrl.name == name:
                return kfctrl
            if not kfctrl.SingleSet and name in kfctrl.keyframeSetKeys:
                return kfctrl
        return None

    def getCurrentFullStatus(self):
        if self.targetType == "actor":
            return self.targetObj.export_full_status2()
        elif self.targetType == "prop":
            return self.targetObj.export_full_status2()
        elif self.targetType == "cam":
            from vnactor import export_camera_status
            return export_camera_status(self.targetObj)
        elif self.targetType == "sys":
            from vnactor import export_sys_status
            return export_sys_status(self.targetObj)
        elif self.targetType == "clip":
            return self.targetObj.export_full_status()
        elif self.targetType == "vntxt":
            return self.targetObj.export_full_status()
        elif self.targetType == "vnsnd":
            return self.targetObj.export_full_status()
        elif self.targetType == "vntrigger":
            return self.targetObj.export_full_status()
        elif self.targetType == "vnaction":
            return self.targetObj.export_full_status()
        else:
            raise Exception("Fail to get full status for Keyframe Control Set: id=%s type=%s!"%(self.targetId, self.targetType))

    def getValidFullStatus(self):
        from vnactor import is_ini_value_true, set_ini_value, get_ini_options
        # backup current options, set to True for now
        ptemp = {}
        for p in get_ini_options():
            ptemp[p] = is_ini_value_true(p)
            set_ini_value(p, True)
        # get status
        vstatus = self.getCurrentFullStatus()
        # restore options
        for p in get_ini_options():
            set_ini_value(p, ptemp[p])
        # done
        return vstatus

    def getCurrentUpdateSequence(self):
        if self.targetType == "actor":
            from vnactor import char_act_update_sequence
            return char_act_update_sequence
        elif self.targetType == "prop":
            from vnactor import prop_act_update_sequence
            return prop_act_update_sequence
        elif self.targetType == "cam":
            from vnactor import cam_act_update_sequence
            return cam_act_update_sequence
        elif self.targetType == "sys":
            from vnactor import sys_act_update_sequence
            return sys_act_update_sequence
        elif self.targetType == "clip" or self.targetType == "clip_preload":
            return clp_act_update_sequence
        elif self.targetType == "vntxt":
            from vntext import vntxt_act_update_sequence
            return vntxt_act_update_sequence
        elif self.targetType == "vnsnd":
            from vnsound import vnsnd_act_update_sequence
            return vnsnd_act_update_sequence
        elif self.targetType == "vntrigger" or self.targetType == "vnaction":
            from vnphysics_core import vnphy_act_update_sequence
            return vnphy_act_update_sequence
        else:
            raise Exception("Fail to get update sequence for Keyframe Control Set: id=%s type=%s!"%(self.targetId, self.targetType))

    def getCurrentUpdateFuncInfo(self):
        if self.targetType == "actor":
            from vnactor import char_act_funcs
            return char_act_funcs
        elif self.targetType == "prop":
            from vnactor import prop_act_funcs
            return prop_act_funcs
        elif self.targetType == "cam":
            from vnactor import cam_act_funcs
            return cam_act_funcs
        elif self.targetType == "sys":
            from vnactor import sys_act_funcs
            return sys_act_funcs
        elif self.targetType == "clip" or self.targetType == "clip_preload":
            return clp_act_funcs
        elif self.targetType == "vntxt":
            from vntext import vntxt_act_funcs
            return vntxt_act_funcs
        elif self.targetType == "vnsnd":
            from vnsound import vnsnd_act_funcs
            return vnsnd_act_funcs
        elif self.targetType == "vntrigger" or self.targetType == "vnaction":
            from vnphysics_core import vnphy_act_funcs
            return vnphy_act_funcs
        else:
            raise Exception("Fail to get update function info for Keyframe Control Set: id=%s type=%s!"%(self.targetId, self.targetType))

    def addKeyframeCtrl(self, name, subkey):
        if subkey == None:
            # setup a keyframe ctrl
            updFuncInfo = self.getCurrentUpdateFuncInfo()[name]
            kfctrl = KeyframeController(name, self.targetObj, updFuncInfo)
            # create keyframe set
            if KeyframeCtrlSet.isNeedSubKeys(self.targetType, name):
                keys = sorted(self.getCurrentFullStatus()[name].keys())
            else:
                keys = None
            kfctrl.createKeyframeSet(keys)
            # insert to ctrl set
            insPos = 0
            for pname in self.getCurrentUpdateSequence():
                if pname == name:
                    break
                if pname == self.keyframeCtrls[insPos].name:
                    insPos += 1
                if insPos >= len(self.keyframeCtrls):
                    break
            self.keyframeCtrls.insert(insPos, kfctrl)
        else:
            # setup a sub key
            kfctrl = self.getKeyframeCtrl(name)
            kfctrl.insertKeyframeSet(subkey)
        return kfctrl

    def registerKeyframes(self, frameNo, targetProperties):
        newKfs = {}
        if self.targetType == "clip":
            # special process for clip, do not refer to current status but refer to clip sub window setting
            curStat = kfam_util_get_subclip_setting(self.targetObj)
            if curStat == None:
                print("Stop register keyframes for clip %s because of fail to get sub-clip status."%self.targetId)
                return newKfs   # no sub-window or not my sub-window, do not register keyframe.
        else:
            curStat = self.getCurrentFullStatus()
        for kfctrl in self.keyframeCtrls:
            if kfctrl.name not in curStat:
                # can not get status for keyframe ctrl, skip
                continue
            if kfctrl.name in targetProperties:
                # whole keyframe set control need register
                newKf = kfctrl.registerKeyframe(frameNo, curStat[kfctrl.name])
                if newKf: 
                    newKfs[kfctrl.name] = newKf
                if isinstance(newKf, Keyframe2Bundle):
                    for key in newKf.bundle.keys():
                        newKfs[key] = newKf.bundle[key]
            elif not kfctrl.SingleSet:
                # some of the multi set keyframe control need register
                subkeys = [k for k in curStat[kfctrl.name].keys() if k in targetProperties]
                if len(subkeys) == 0:
                    continue
                updValue = {}
                for subkey in subkeys:
                    updValue[subkey] = curStat[kfctrl.name][subkey]
                newKf = kfctrl.registerKeyframe(frameNo, updValue)
                print("registered bundle for %s: "%kfctrl.name + str(newKf))   # TODO, delete
                if newKf: 
                    newKfs[kfctrl.name] = newKf
                if isinstance(newKf, Keyframe2Bundle):
                    for key in newKf.bundle.keys():
                        newKfs[key] = newKf.bundle[key]
        return newKfs

    def copyKeyframesTo(self, tgtCtrlNames, lmtFrameLength, dstCtrlSet, moveMode):
        #print("Copy %s to %s, move mode = %s"%(str(tgtCtrlNames), str(dstCtrlSet), str(moveMode)))
        tgtCtrlNames = set(tgtCtrlNames)
        dstValidStat = dstCtrlSet.getValidFullStatus()
        copyCount = 0
        for kfctrl in self.keyframeCtrls:
            kfname = kfctrl.name
            if kfname not in dstValidStat:
                continue
            if kfctrl.SingleSet:
                # check if need copy
                if len(tgtCtrlNames) > 0 and kfname not in tgtCtrlNames:
                    continue
                # check if has keyframes to copy
                kfList = [kf for kf in kfctrl.getKeyframeList() if kf.frameNo <= lmtFrameLength]
                if len(kfList) == 0:
                    continue
                # check if ctrl existed in dst
                dstCtrl = dstCtrlSet.getKeyframeCtrl(kfname)
                if dstCtrl == None:
                    dstCtrl = dstCtrlSet.addKeyframeCtrl(kfname, None)
                if dstCtrl == None:
                    continue
                # copy keyframes, then remove original if move mode
                dstCtrl.updateKeyframes(kfList)
                copyCount += len(kfList)
                if moveMode:
                    kfctrl.clearKeyframes()
            else:
                # check if need copy
                tgtSubKeys = tgtCtrlNames.intersection(kfctrl.keyframeSetKeys)
                if len(tgtCtrlNames) > 0 and kfname not in tgtCtrlNames and len(tgtSubKeys) == 0:
                    continue
                # check if has keyframes to copy
                kfList = [kf for kf in kfctrl.getKeyframeList() if kf.frameNo <= lmtFrameLength]
                if len(kfList) == 0:
                    continue
                # check if ctrl existed in dst
                dstCtrl = dstCtrlSet.getKeyframeCtrl(kfname)
                if dstCtrl == None:
                    dstCtrl = dstCtrlSet.addKeyframeCtrl(kfname, kfctrl.keyframeSetKeys)
                if dstCtrl == None:
                    continue
                # copy keyframes, then remove original if move mode
                if len(tgtSubKeys) == 0:
                    dstCtrl.updateKeyframes(kfList)
                    copyCount += len(kfList)
                    if moveMode:
                        kfctrl.clearKeyframes()
                else:
                    for subkey in tgtSubKeys:
                        subkfs = [kf for kf in kfctrl.keyframeSets[subkey].getKeyframeList() if kf.frameNo <= lmtFrameLength]
                        if len(subkfs) > 0 and subkey in dstCtrl.keyframeSetKeys:
                            dstCtrl.keyframeSets[subkey].updateKeyframes(subkfs)
                            copyCount += len(subkfs)
                            if moveMode:
                                dstCtrl.keyframeSets[subkey].clearData()
                
        # done
        return copyCount

    def checkNeedDiff(self):
        self.updateNeedDiff = False
        for kfctrl in self.keyframeCtrls:
            if kfctrl.updateType == "diff":
                self.updateNeedDiff = True
                return

    def resetWorkingIndex(self):
        for kfctrl in self.keyframeCtrls:
            kfctrl.resetWorkingIndex()

    def setFrame(self, tgtFrameNo):
        if self.updateNeedDiff:
            referValue = self.getCurrentFullStatus()
            for kfctrl in self.keyframeCtrls:
                if kfctrl.updateType == "diff" and kfctrl.name in referValue:
                    kfctrl.setFrame(tgtFrameNo, referValue[kfctrl.name])
                else:
                    kfctrl.setFrame(tgtFrameNo, None)
        else:
            for kfctrl in self.keyframeCtrls:
                kfctrl.setFrame(tgtFrameNo, None)

    def statusDiffReport(self, tgtFrameNo):
        curStat = self.getCurrentFullStatus()
        result = {}
        for kfctrl in self.keyframeCtrls:
            if kfctrl.name in curStat:
                report = kfctrl.statusDiffReport(tgtFrameNo, curStat[kfctrl.name])
                result.update(report)
        """
        for key in curStat:
            kfctrl = self.getKeyframeCtrl(key)
            if kfctrl:
                report = kfctrl.statusDiffReport(tgtFrameNo, curStat[key])
                result.update(report)
            else:
                result[key] = "NewCtrl"
        """
        return result

    def solvePreload(self):
        if self.targetType == "clip_preload":
            game = self.targetObj
            clipTarget = game.gdata.kfaManagedClips[self.targetId]
            self.targetObj = clipTarget
            for ctrl in self.keyframeCtrls:
                ctrl.targetObj = clipTarget
            self.targetType = "clip"

    @staticmethod
    def createById(clip, targetId, referKeys=None):
        print("  Creating new keyframe control set for target object id = %s"%targetId)
        # phase 1: create keyframe ctrl set
        game = clip.game
        kcs = None
        try:
            # try actor
            tgtActor = game.scenef_get_actor(targetId)
            if tgtActor != None:
                targetObj = tgtActor
                targetObj.linkedClip = clip
                kcs = KeyframeCtrlSet(targetObj, targetId, "actor")
            # try prop
            tgtProp = game.scenef_get_propf(targetId)
            if tgtProp != None:
                targetObj = tgtProp
                targetObj.linkedClip = clip
                kcs = KeyframeCtrlSet(targetObj, targetId, "prop")
            # try clip
            if targetId in game.gdata.kfaManagedClips:
                targetObj = game.gdata.kfaManagedClips[targetId]
                kcs = KeyframeCtrlSet(targetObj, targetId, "clip")
            # try camera
            if targetId == "cam":
                targetObj = game
                kcs = KeyframeCtrlSet(targetObj, targetId, "cam")
            # try system
            if targetId == "sys":
                targetObj = game
                kcs = KeyframeCtrlSet(targetObj, targetId, "sys")
            # try vntext
            from vntext import get_vntext_manager
            ti = get_vntext_manager(game).getTextInfo(targetId)
            if ti != None:
                targetObj = ti
                kcs = KeyframeCtrlSet(targetObj, targetId, "vntxt")
            # try vnsound
            from vnsound import get_vnsound_manager
            if targetId in get_vnsound_manager(game).soundAlias:
                targetObj = get_vnsound_manager(game).soundDict[targetId]
                kcs = KeyframeCtrlSet(targetObj, targetId, "vnsnd")
            # try clip preload
            if hasattr(game.gdata, "kfaPreloadClipNames") and targetId in game.gdata.kfaPreloadClipNames:
                targetObj = None    # special
                kcs = KeyframeCtrlSet(game, targetId, "clip_preload")
                if referKeys == None:
                    raise Exception("Reference keys must exist for clip preload!")
            # try vnphy
            from vnphysics import initialize
            phyManager = initialize(game)
            if targetId in phyManager.triggerIdDict:
                targetObj = phyManager.triggerIdDict[targetId]
                kcs = KeyframeCtrlSet(targetObj, targetId, "vntrigger")
            if targetId in phyManager.actionIdDict:
                targetObj = phyManager.actionIdDict[targetId]
                kcs = KeyframeCtrlSet(targetObj, targetId, "vnaction")
            # check point
            if kcs == None:
                raise Exception("Cannot found supported object for id = <%s>"%targetId)
        except Exception as e:
            # failed
            game.logger_log_error("Fail to create keyframe control: %s"%str(e), True)
            return None

        # phase 2: create keyframe ctrl
        if referKeys == None:
            referKeys = kcs.getCurrentFullStatus().keys()
        act_funcs = kcs.getCurrentUpdateFuncInfo()
        for pname in kcs.getCurrentUpdateSequence():
            if pname in referKeys:
                # setup a keyframe ctrl for this act
                updFuncInfo = act_funcs[pname]
                kfctrl = KeyframeController(pname, targetObj, updFuncInfo)
                # create keyframe set
                if kcs.isNeedSubKeys(kcs.targetType, pname):
                    """
                    print("load for '%s'"%(kcs.targetId))
                    print("object '%s', linkedClip = %s"%(str(kcs.targetObj), str(kcs.targetObj.linkedClip)))
                    if hasattr(kcs.targetObj, "videoPlayerCtrl"):
                        print("videoplayerctrl = " + str(kcs.targetObj.videoPlayerCtrl))
                    else:
                        print("videoplayerctrl not existed")
                    print("Full status: " + str(kcs.getCurrentFullStatus()))
                    """
                    # TODO: may be fail when load from a target not enable subkey
                    curStat = kcs.getCurrentFullStatus()
                    if pname in curStat:
                        subkeys = sorted(curStat[pname].keys())
                    else:
                        subkeys = []
                else:
                    subkeys = None
                kfctrl.createKeyframeSet(subkeys)
                # add to ctrl set
                kcs.keyframeCtrls.append(kfctrl)
        return kcs

    @staticmethod
    def createFromSetting(clip, setting):
        tgtid = setting["targetId"]
        refkeys = setting["controllers"].keys()
        ctrlset = KeyframeCtrlSet.createById(clip, tgtid, refkeys)
        for kfctrl in ctrlset.keyframeCtrls:
            # load for each controller
            if kfctrl.name in setting["controllers"]:
                kfctrl.importSetting(setting["controllers"][kfctrl.name])
        ctrlset.checkNeedDiff()
        return ctrlset

    @staticmethod
    def isNeedSubKeys(type, pname):
        if type in KeyframeCtrlSet.SUB_KEY_REQ:
            if pname in KeyframeCtrlSet.SUB_KEY_REQ[type]:
                return True
        return False


class KeyFrameClip2:
    def __init__(self, game, setting = None):
        # init runtime
        self.game = game
        self.isPlaying = False
        self.curFrame = 0
        # setting
        self.name = ""
        self.frameLength = 300
        self.frameRate = 30.0
        self.priority = 100
        self.animeType = "absolute"
        self.loop = -1
        self.speed = 1
        self.autorun = False
        self.version = 2
        # v2 new setting
        self.autoRewindOnStop = False
        self.gcOnStart = False
        self.gcOnStop = False
        self.gcOnLoop = False
        # keyframe controllers
        self.keyframes = {}
        # import settings & keyframe or an empty one
        if setting:
            self.importSetting(setting)

    @property
    def lastFrameNo(self):  # the last frame no of all keyframes
        lfn = 0
        for kfctrlset in self.keyframes.values():
            if kfctrlset.LastFrameNo > lfn:
                lfn = kfctrlset.LastFrameNo
        return lfn

    @property
    def curTime(self):
        return float(self.curFrame) / self.frameRate / self.speed

    @property
    def timeLength(self):
        return float(self.frameLength) / self.frameRate / self.speed

    @staticmethod
    def createFromSetting(game, setting):
        try:
            return KeyFrameClip2(game, setting)
        except:
            print("Fail to import clip from setting!")
            traceback.print_exc()
            print("\n")
            return None

    @staticmethod
    def createFromV1(clip1, newName):
        clip2 = KeyFrameClip2(clip1.game)
        # setting
        clip2.name = newName
        clip2.frameLength = clip1.frameLength
        clip2.frameRate = clip1.frameRate
        clip2.priority = clip1.priority
        clip2.animeType = clip1.animeType
        clip2.loop = clip1.loop
        clip2.speed = clip1.speed
        clip2.autorun = clip1.autorun
        clip2.version = 2
        # keyframe controllers
        clip2.keyframes = {}
        for tid in clip1.keyframes:
            # get all properties from all keyframes
            refkeys = set()
            for kf in clip1.keyframes[tid]:
                refkeys.update(kf.status.keys())
            # update ik_set/fk_set
            if "ik_set" in refkeys:
                refkeys.discard("ik_set")
                refkeys.add("ik_set2")
            if "fk_set" in refkeys:
                refkeys.discard("fk_set")
                refkeys.add("fk_set2")
            if tid == "cam" and "camera" in refkeys:
                from vnactor import export_camera_status
                refkeys.discard("camera")
                refkeys.update(export_camera_status(clip1.game).keys())
            # create control set
            ctrlset = KeyframeCtrlSet.createById(clip2, tid, refkeys)
            # create keyframes
            oldStatus = ctrlset.getCurrentFullStatus()
            oldRestoreReq = False
            updFuncInfo = ctrlset.getCurrentUpdateFuncInfo()
            groupedKeyframes = {}
            def groupKeyframe(key, newKeyframe):
                if key in groupedKeyframes:
                    groupedKeyframes[key].append(newKeyframe)
                else:
                    groupedKeyframes[key] = [newKeyframe,]

            for kf in clip1.keyframes[tid]:
                # prepare curve data
                if hasattr(kf, "curve") and kf.curve != None:
                    kf_curve = kf.curve
                else:
                    kf_curve = [0.2, 0.8, 0.2, 0.8]
                # build every keyframe
                for key, value in kf.status.items():
                    # camera special process
                    if tid == "cam" and key == "camera":
                        groupKeyframe("cam_pos", Keyframe2(kf.frameNo, value[0], kf_curve))
                        groupKeyframe("cam_dst", Keyframe2(kf.frameNo, value[1], kf_curve))
                        groupKeyframe("cam_rot", Keyframe2(kf.frameNo, Quaternion.Euler(value[2]), kf_curve))
                        groupKeyframe("cam_fov", Keyframe2(kf.frameNo, value[3], kf_curve))
                        continue
                    # update ik_set/fk_set
                    if key == "ik_set" or key == "fk_set":
                        oldRestoreReq = True
                        ctrlset.targetObj.import_status_diff_optimized(kf.status)
                        if key == "ik_set":
                            key = "ik_set2"
                            value = ctrlset.targetObj.export_ik_target_info2()
                        if key == "fk_set":
                            key = "fk_set2"
                            value = ctrlset.targetObj.export_fk_bone_info2()
                    # check key
                    if key in updFuncInfo:
                        needCurve = updFuncInfo[key][1]
                        if needCurve:
                            curve = kf_curve
                        else:
                            curve = None
                    else:
                        print("Unsupported property found in update: id = %s, key = %s, frame = %d"%(tid, key, kf.frameNo))
                        continue
                    # sub key check
                    if KeyframeCtrlSet.isNeedSubKeys(ctrlset.targetType, key):
                        # build key bundle
                        kb = {}
                        for skey, svalue in value.items():
                            kb[skey] = Keyframe2(kf.frameNo, svalue, curve)
                        if len(kb) == 0:
                            print("    No sub keyframe found for %s@%d"%(key, kf.frameNo))
                            continue
                        nkf = Keyframe2Bundle(kb)
                    else:
                        # build key
                        nkf = Keyframe2(kf.frameNo, value, curve)
                    # add to groupedKeyframes
                    groupKeyframe(key, nkf)
            # update keyframes
            for key in groupedKeyframes.keys():
                kfctrl = ctrlset.getKeyframeCtrl(key)
                kfctrl.updateKeyframes(groupedKeyframes[key])
            # add to new clip
            clip2.keyframes[tid] = ctrlset
            # restore status if needed
            if oldRestoreReq:
                ctrlset.targetObj.import_status_diff_optimized(oldStatus)
        # done
        return clip2

    def copy(self, newName, assignInfo = None):
        newClip = KeyFrameClip2(self.game, None)
        # setting
        newClip.name = newName  # copy except name
        newClip.frameLength = self.frameLength
        newClip.frameRate = self.frameRate
        newClip.priority = self.priority
        newClip.animeType = self.animeType
        newClip.loop = self.loop
        newClip.speed = self.speed
        newClip.autorun = self.autorun
        newClip.version = self.version
        # v2 new setting
        newClip.autoRewindOnStop = self.autoRewindOnStop
        newClip.gcOnStart = self.gcOnStart
        newClip.gcOnStop = self.gcOnStop
        newClip.gcOnLoop = self.gcOnLoop
        # keyframes controllers
        newClip.keyframes = {}
        for actId in self.keyframes:
            if assignInfo == None:
                # no assign, just copy
                newClip.keyframes[actId] = self.keyframes[actId].copy()
            elif actId in assignInfo:
                # check assign
                if assignInfo[actId] == None:
                    # no assign, just copy
                    newClip.keyframes[actId] = self.keyframes[actId].copy()
                else:
                    # assign to new id
                    newId = assignInfo[actId]
                    css = self.keyframes[actId].exportSetting()
                    css["targetId"] = newId
                    newClip.keyframes[newId] = KeyframeCtrlSet.createFromSetting(newClip, css)
            else:
                pass    # exclude from copy
        # return
        return newClip

    def importSetting(self, setting):
        print("VNAnime2 clip <%s> importing..."%setting["name"])
        # general data
        self.name = setting["name"]
        self.frameLength = setting["frameLength"]
        self.frameRate = setting["frameRate"]
        self.priority = setting["priority"]
        self.animeType = setting["animeType"]
        self.loop = setting["loop"]
        self.speed = setting["speed"]
        self.autorun = setting["autorun"]
        self.version = setting["version"]
        # v2 new setting
        if hasattr(setting, "autoRewindOnStop"):
            self.autoRewindOnStop = setting["autoRewindOnStop"]
        if hasattr(setting, "gcOnStart"):
            self.gcOnStart = setting["gcOnStart"]
        if hasattr(setting, "gcOnStop"):
            self.gcOnStop = setting["gcOnStop"]
        if hasattr(setting, "gcOnLoop"):
            self.gcOnLoop = setting["gcOnLoop"]
        # keyframes
        self.keyframes = {}
        for tid in setting["keyframes"]:
            ctrlset = KeyframeCtrlSet.createFromSetting(self, setting["keyframes"][tid])
            self.keyframes[tid] = ctrlset
        # done
        print("...Done!")

    def exportSetting(self):
        setting = {}
        # genenal data
        setting["name"] = self.name
        setting["frameLength"] = self.frameLength
        setting["frameRate"] = self.frameRate
        setting["priority"] = self.priority
        setting["animeType"] = self.animeType
        setting["loop"] = self.loop
        setting["speed"] = self.speed
        setting["autorun"] = self.autorun
        setting["version"] = self.version
        # v2 new setting
        setting["autoRewindOnStop"] = self.autoRewindOnStop
        setting["gcOnStart"] = self.gcOnStart
        setting["gcOnStop"] = self.gcOnStop
        setting["gcOnLoop"] = self.gcOnLoop
        # keyframes
        setting["keyframes"] = {}
        for tid in self.keyframes:
            setting["keyframes"][tid] = self.keyframes[tid].exportSetting()
        # done
        return setting

    def import_status(self, script):
        if script.has_key("frame") and script["frame"] != self.curFrame:
            self.seek(script["frame"])
        if script.has_key("loop") and script["loop"] != self.loop:
            self.loop = script["loop"]
            self.playLoop = script["loop"]
        if script.has_key("speed") and script["speed"] != self.speed:
            self.speed = script["speed"]
        if script.has_key("play"):
            if script["play"] == 0 and self.isPlaying:
                self.stop()
            if script["play"] == 1 and not self.isPlaying:
                self.play()
            if script["play"] == 2 and self.isPlaying:
                self.pause()

    def export_full_status(self):
        fs = {}
        if self.isPlaying:
            fs["play"] = 1      # play
            # do not export frame when play
        elif self.curFrame == 0:
            fs["play"] = 0      # stop
            fs["frame"] = self.curFrame
        else:
            fs["play"] = 2      # pause
            fs["frame"] = self.curFrame
        fs["loop"] = self.loop
        fs["speed"] = self.speed
        return fs

    # play control
    def play(self, forceLoop=None):
        if forceLoop != None:
            self.playLoop = forceLoop
        else:
            self.playLoop = self.loop
        if self.gcOnStart:
            VNDoGarbageCollectNow()
        self.isPlaying = True
        self.applyFrames()
        
    def pause(self):
        self.isPlaying = False
        self.applyFrames()
        if self.gcOnStop:
            VNDoGarbageCollectNow()
        
    def stop(self):
        if self.isPlaying:
            self.isPlaying = False
            if not self.autoRewindOnStop:
                self.applyFrames()
        self.curFrame = 0
        if self.autoRewindOnStop:
            self.applyFrames()
        if self.gcOnStop:
            VNDoGarbageCollectNow()
        
    def seek(self, frameNo, apply=True):
        self.curFrame = frameNo if frameNo <= self.frameLength else frameNo % self.frameLength
        if apply:
            self.applyFrames()

    def seekNormalized(self, ntime, apply=True):
        self.curFrame = ntime * self.frameLength
        if apply:
            self.applyFrames()

    def gotoNextFrame(self):
        self.curFrame = int(self.curFrame) + 1
        if self.curFrame > self.frameLength:
            if self.loop != 0:
                self.curFrame %= self.frameLength
            else:
                self.curFrame = self.frameLength
        self.applyFrames()
        
    def gotoPrevFrame(self):
        self.curFrame = int(self.curFrame) - 1
        if self.curFrame < 0:
            if self.loop != 0:
                self.curFrame = self.frameLength
            else:
                self.curFrame = 0
        self.applyFrames()

    def gotoFirstFrame(self):
        self.curFrame = 0
        self.applyFrames()
        
    def gotoLastFrame(self):
        self.curFrame = self.frameLength
        self.applyFrames()
        
    def gotoNextSecond(self):
        self.curFrame = int(self.curFrame + self.frameRate)
        if self.curFrame > self.frameLength:
            if self.loop != 0:
                self.curFrame %= self.frameLength
            else:
                self.curFrame = self.frameLength
        self.applyFrames()
        
    def gotoPrevSecond(self):
        self.curFrame = int(self.curFrame - self.frameRate)
        if self.curFrame < 0:
            if self.loop != 0:
                self.curFrame = self.frameLength
            else:
                self.curFrame = 0
        self.applyFrames()

    def update(self, game):
        from UnityEngine import Time
        if self.isPlaying:
            deltaTime = Time.deltaTime
            self.curFrame += deltaTime * self.frameRate * self.speed
            if hasattr(game.scenedata, "kfamCustomEndFrame") and game.scenedata.kfamCustomEndFrame != -1 and self.curFrame >= game.scenedata.kfamCustomEndFrame:
                self.curFrame = game.scenedata.kfamCustomEndFrame
                self.isPlaying = False
            elif self.curFrame > self.frameLength:
                if self.playLoop != 0:
                    self.curFrame -= self.frameLength
                    self.resetWorkingIndex()
                    if self.playLoop > 0: self.playLoop -= 1
                    if self.gcOnLoop:
                        VNDoGarbageCollectNow()
                else:
                    self.curFrame = self.frameLength
                    self.isPlaying = False
            #print " clip [%s] playing: dt=%f, curFrame=%f"%(self.name, deltaTime, self.curFrame)
            try:
                self.applyFrames()
            except Exception as e:
                self.isPlaying = False  # stop on exception
                traceback.print_exc()
                print("update keyframe anime Error: " + str(e))
            
    def resetWorkingIndex(self):
        for kfset in self.keyframes.values():
            kfset.resetWorkingIndex()

    def applyFrames(self):
        for kfset in self.keyframes.values():
            kfset.setFrame(self.curFrame)

    # edit function
    def getKeyframeCtrl(self, tid, pid):
        if tid in self.keyframes:
            return self.keyframes[tid].getKeyframeCtrl(pid)
        else:
            return None

    def addTarget(self, id):
        newkeyframeset = KeyframeCtrlSet.createById(self, id)
        if newkeyframeset:
            self.keyframes[id] = newkeyframeset

    def solvePreload(self):
        for ctrlset in self.keyframes.values():
            ctrlset.solvePreload()

# clip status functions
def clip_set_play(clip, param):
    if param == 1:
        clip.play()
    elif param == 2:
        clip.pause()
    else:
        clip.stop()

def clip_set_frame(clip, param):
    clip.seek(param)

def clip_set_loop(clip, param):
    clip.loop = param

def clip_set_speed(clip, param):
    clip.speed = param

clp_act_funcs = {
    'play': (clip_set_play, False, "force", "T"),
    'frame': (clip_set_frame, True, "force", "T"),
    'loop': (clip_set_loop, False, "force", "T"),
    'speed': (clip_set_speed, True, "force", "T"),
}

clp_act_update_sequence = (
    'frame',
    'loop',
    'speed',
    'play',
)


# clip ui functions
def kfam_GUI_modify_clip2(sh):
    from libkfguictrl import KeyframeViewer, CurveEditor
    from vnframe import VNEnableStudioShortcuts, VNEnableCameraShortcuts
    from vnactor import get_ini_translation
    from vnanime import save_keyframe_anime_setting, changeGuiScreen, kfam_GUI_manager, kfam_GUI_curve_subwindow, kfam_GUI_subclip_subwindow, kfam_GUI_parts_seekbar, kfam_GUI_modify_clip_add
    from vntext import get_vntext_manager
    from vnsound import get_vnsound_manager
    from vnphysics import initialize
    # memory shortcuts
    game = sh.game
    sd = game.scenedata
    gd = game.gdata
    clip = sd.kfamOnModifyClip
    mClips = gd.kfaManagedClips
    sClips = gd.kfaSortedClips
    vntext_mgr = get_vntext_manager(game)
    vnsound_mgr = get_vnsound_manager(game)
    vnphy_mgr = initialize(game)
    ctrl, alt, shift = unity_util.metakey_state()
    # ui grid const
    wndWidthMargin = 10
    wndHeightMargin = 95
    kfvTopMargin = 60
    kfvGapSmall = 1
    kfvGapBig = 2
    curveWindowWidth = 124
    curveWindowHeight = 200
    clipWindowWidth = 500
    clipWindowHeight = 45

    def initClipEditorWindow():
        sd.kfamModifyTabIndex = 0
        # UI temp variable
        sd.kfamEditorTargetScroll = Vector2.zero
        sd.kfamHelpScroll = Vector2.zero
        sd.kfamLastPlayingStatus = clip.isPlaying
        sd.kfamCurSelTargetId = None
        sd.kfamCurSelKeyframeSetCache = None
        sd.kfamTargetKeyframeExpand = {}
        sd.kfamCustomStartFrame = -1
        sd.kfamCustomEndFrame = -1
        sd.kfamKeyframeClipboard = set()
        sd.kfamKeyframeClipMode = 0
        sd.kfamKeyframeClipboardLength = -1
        sd.kfamSetupCtrlDetail = False
        sd.kfamKeyframeEditorMode = True
        sd.kfamOperationMode = "normal"
        # window size
        sd.kfamOldWindowRect = game.windowRect
        game.windowRect = gd.kfaSetting.clipEditorWindowRect
        # target keyframe expand
        for tid in clip.keyframes.keys():
            sd.kfamTargetKeyframeExpand[tid] = False
        # sort keyframe id
        sd.kfamSortedTargetId = []
        actorids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in sd.actors]
        sd.kfamSortedTargetId.extend(sorted(actorids))
        propids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in sd.props]
        sd.kfamSortedTargetId.extend(sorted(propids))
        clipids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in mClips]
        sd.kfamSortedTargetId.extend(sorted(clipids))
        txtids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in vntext_mgr.allTextIds]
        sd.kfamSortedTargetId.extend(sorted(txtids))
        sndids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in vnsound_mgr.soundAlias]
        sd.kfamSortedTargetId.extend(sorted(sndids))
        trgids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in vnphy_mgr.triggerIdDict]
        sd.kfamSortedTargetId.extend(sorted(trgids))
        actids = [id for id in sd.kfamTargetKeyframeExpand.keys() if id in vnphy_mgr.actionIdDict]
        sd.kfamSortedTargetId.extend(sorted(actids))
        if "cam" in sd.kfamTargetKeyframeExpand:
            sd.kfamSortedTargetId.append("cam")
        if "sys" in sd.kfamTargetKeyframeExpand:
            sd.kfamSortedTargetId.append("sys")
        # build keyframeview
        if hasattr(sd, "kfamKeyframeView") and sd.kfamKeyframeView != None:
            oldExpanedIds = [tid for tid in sd.kfamKeyframeView.keyframeNameList.groupExpand.keys() if sd.kfamKeyframeView.keyframeNameList.groupExpand[tid]]
        else:
            oldExpanedIds = []
        kfRect = Rect(wndWidthMargin + gd.kfaSetting.targetPanelWidth + kfvGapBig, kfvTopMargin, game.windowRect.width - wndWidthMargin * 2 - gd.kfaSetting.targetPanelWidth - kfvGapBig - 1, game.windowRect.height - wndHeightMargin)
        sd.kfamKeyframeView = KeyframeViewer(kfRect, gd.kfaSetting.propertyNameWidth)
        sd.kfamKeyframeView.changeColors(gd.kfaSetting.keyframeColors)
        sd.kfamResizeRequest = False
        sd.kfamReloadRequest = False
        sd.kfamHidePropertiesRequest = False
        sd.kfamSubkeyLinkInfo = {}
        sd.kfamSubkeyExpand = {}
        reloadKeyframes()
        for tid in oldExpanedIds:
            sd.kfamKeyframeView.expendGroup(tid, True)
        # build curve editor
        sd.kfamCurveWndId = None
        sd.kfamCurveEditor = CurveEditor(7, 42, 110, None)
        sd.kfamCurveEditor.changeColors(gd.kfaSetting.curveColors)
        sd.kfamCurveEditor.onCurveUpdateCallback = curveChange
        sd.kfamValueNameOnEditor = None
        sd.kfamValueKeyframesOnEditor = None
        sd.kfamCurveValueMode = "curve"
        # build sub-clip editor
        sd.kfamSubClipWndId = None
        # build helper window
        sd.kfamHelpWndId = None
        # locate select properties btn
        sd.kfamSelectAllBtnRect = Rect(kfRect.x, kfRect.y - 1, sd.kfamKeyframeView.curNameWidth / 2 - 1, sd.kfamKeyframeView.curNoHeight + 1)
        sd.kfamSelectChgBtnRect = Rect(sd.kfamSelectAllBtnRect.right, sd.kfamSelectAllBtnRect.top, sd.kfamSelectAllBtnRect.width, sd.kfamSelectAllBtnRect.height)
        # prepare shortcuts
        prepareShortcuts()
        # VideoExport mode
        sd.kfamEatMouseInputBackup = game.skin.eatMouseInput
        game.skin.eatMouseInput = not gd.kfaSetting.videoExportMode
    def quitClipEditorWindow():
        # restore mouse mode
        game.skin.eatMouseInput = sd.kfamEatMouseInputBackup
        # cleanups
        cleanupShortcuts()
        sd.kfamModifyTabIndex = -1
        gd.kfaSetting.clipEditorWindowRect = game.windowRect
        gd.kfaSetting.propertyNameWidth = sd.kfamKeyframeView.frameNameWidth
        save_keyframe_anime_setting(game)
        game.windowRect = sd.kfamOldWindowRect
        closeCurveSubWindow()
        toggleClipSubWindow(None)
        kfam_util_toggle_help_window(game, True)
        # switch ui
        changeGuiScreen(sh, kfam_GUI_manager)
        # quit directly
        if hasattr(sd, "kfamClipBackToClose") and sd.kfamClipBackToClose:
            from vnframe import scriptHelperGUIClose
            sd.kfamClipBackToClose = False
            scriptHelperGUIClose()
    def openCurveSubWindow():
        if sd.kfamCurveWndId != None and game.get_extra_window(sd.kfamCurveWndId) != None:
            return  # one curve window only
        if game.windowRect.right + curveWindowWidth < Screen.width:
            cWndRect = Rect(game.windowRect.right, game.windowRect.y, curveWindowWidth, curveWindowHeight)
        else:
            cWndRect = Rect(game.windowRect.x - curveWindowWidth, game.windowRect.y, curveWindowWidth, curveWindowHeight)
        # setup func
        def skin_setup(g):
            g.windowName = "Curve / Value"
            g.windowRect = cWndRect
            g.windowStyle = g.windowStyleDefault
        # setup skin
        from skin_customwindow import SkinCustomWindow
        skin = SkinCustomWindow()
        skin.funcSetup = skin_setup
        skin.funcWindowGUI = kfam_GUI_curve_subwindow
        sd.kfamCurveWndId = game.new_extra_window_skin(skin)
    def closeCurveSubWindow():
        game.close_extra_window(sd.kfamCurveWndId)
        sd.kfamCurveWndId = None
    def toggleClipSubWindow(tgtId):
        # only one clip window allowed, so, always close existed window
        game.close_extra_window(sd.kfamSubClipWndId)
        if tgtId in mClips:
            tgtClip = mClips[tgtId]
            sd.kfamSubClipStatus = GData()
            sd.kfamSubClipStatus.errorMessage = None
            sd.kfamSubClipStatus.targetClip = tgtClip
            sd.kfamSubClipStatus.mainCurFrame = clip.curFrame
            sd.kfamSubClipStatus.subClipStatus = tgtClip.export_full_status()
            sd.kfamSubClipStatus.subClipStatus["frame"] = tgtClip.curFrame
            sd.kfamSubClipStatus.tempPlaying = tgtClip.isPlaying
            sd.kfamSubClipStatus.tempFrame = tgtClip.curFrame
            sd.kfamSubClipStatus.tempFrameStr = "%d"%tgtClip.curFrame
            sd.kfamSubClipStatus.tempLoopStr = "%d"%tgtClip.loop
            sd.kfamSubClipStatus.tempSpeedStr = "%.2f"%tgtClip.speed
            # open clip status window
            if game.windowRect.y - clipWindowHeight >= 0:
                cWndRect = Rect(game.windowRect.x, game.windowRect.y - clipWindowHeight, clipWindowWidth, clipWindowHeight)
            else:
                cWndRect = Rect(game.windowRect.x, game.windowRect.bottom, clipWindowWidth, clipWindowHeight)
            # setup func
            def skin_setup(g):
                g.windowName = "Clip <<color=#00ff00>%s</color>> Status"%tgtId
                g.windowRect = cWndRect
                g.windowStyle = g.windowStyleDefault
            # setup skin
            from skin_customwindow import SkinCustomWindow
            skin = SkinCustomWindow()
            skin.funcSetup = skin_setup
            skin.funcWindowGUI = kfam_GUI_subclip_subwindow
            sd.kfamSubClipWndId = game.new_extra_window_skin(skin)
        else:
            sd.kfamSubClipWndId = None
    def resizeClipEditorWindow():
        # resize keyframe view
        kfRect = Rect(wndWidthMargin + gd.kfaSetting.targetPanelWidth + kfvGapBig, kfvTopMargin, game.windowRect.width - wndWidthMargin * 2 - gd.kfaSetting.targetPanelWidth - kfvGapBig - 1, game.windowRect.height - wndHeightMargin)
        sd.kfamKeyframeView.resize(kfRect, gd.kfaSetting.propertyNameWidth)
        # relocate select properties btn
        sd.kfamSelectAllBtnRect = Rect(kfRect.x, kfRect.y - 1, sd.kfamKeyframeView.curNameWidth / 2 - 1, sd.kfamKeyframeView.curNoHeight + 1)
        sd.kfamSelectChgBtnRect = Rect(sd.kfamSelectAllBtnRect.right, sd.kfamSelectAllBtnRect.top, sd.kfamSelectAllBtnRect.width, sd.kfamSelectAllBtnRect.height)
        # clear request
        sd.kfamResizeRequest = False
    def reloadKeyframes():
        mainKeyColor = Color(0, 0.333, 1)
        if gd.kfaSetting.keyframeColors[-2][0] == "Name Text Sub Group":
            mainKeyColor = gd.kfaSetting.keyframeColors[-2][1]
        subKeyColor = Color(0, 0, 0)
        if gd.kfaSetting.keyframeColors[-1][0] == "Name Text Sub Normal":
            subKeyColor = gd.kfaSetting.keyframeColors[-1][1]

        def getInfoFromCtrlSet(ctrlset):
            # return (name, propertye translate dict)
            setType = ctrlset.targetType
            tgtObj = ctrlset.targetObj
            tgtId = ctrlset.targetId
            # get name string
            if setType == "actor":
                tname = tgtId + ":" + tgtObj.text_name
            elif setType == "prop":
                tname = tgtId + ":" + tgtObj.text_name
            elif setType == "clip":
                tname = tgtId + ":Clip"
            elif setType == "vntxt":
                tname = tgtId + ":VNText"
            elif setType == "vnsnd":
                tname = tgtId + ":VNSound"
            elif setType == "vntrigger":
                tname = tgtId + ":Trigger"
            elif setType == "vnaction":
                tname = tgtId + ":Action"
            elif setType == "cam":
                tname = "Camera"
            elif setType == "sys":
                tname = "Scene"
            else:
                raise Exception("Unknown target type <%s> of clip's keyframes id <%s>"%(setType, tgtId))
            # get a dict
            pdict = {}
            for pname in ctrlset.getKeyframeNameList():
                tst = get_ini_translation(pname)
                tsts = tst.split(":")
                stxt = tsts[1]
                pdict[pname] = stxt
            return tname, pdict

        kfView = sd.kfamKeyframeView
        # clear up to start
        kfView.clearKeyframe()
        # setup namelist, use export key when no keyframe setted in target object
        for tid in sd.kfamSortedTargetId:
            tname, pnamedict = getInfoFromCtrlSet(clip.keyframes[tid])
            referKeys = clip.keyframes[tid].getKeyframeNameList()
            for pkey in referKeys:
                kfView.importFrameName(tid, pkey)
                kfView.keyframeNameList.dispNameDic[pkey] = pnamedict[pkey]
                # subkey support
                kfctrl = clip.getKeyframeCtrl(tid, pkey)
                if kfctrl and not kfctrl.SingleSet:
                    mainkey = "%s#%s"%(tid, pkey)
                    kfView.keyframeNameList.dispNameDic[pkey] = "[" + pnamedict[pkey] + "]"
                    kfView.keyframeNameList.setNameCustomColor(mainkey, mainKeyColor)
                    subkeys = []
                    for subkey in kfctrl.keyframeSetKeys:
                        kfView.importFrameName(tid, subkey)
                        kfView.keyframeNameList.dispNameDic[subkey] = ">"+subkey
                        subkeys.append("%s#%s"%(tid, subkey))
                        kfView.keyframeNameList.setNameCustomColor("%s#%s"%(tid, subkey), subKeyColor)
                    sd.kfamSubkeyLinkInfo[mainkey] = subkeys
                    sd.kfamSubkeyExpand[mainkey] = False
            kfView.keyframeNameList.dispNameDic[tid] = tname
        # import keyframes
        for tid in sd.kfamSortedTargetId:
            for kfctrl in clip.keyframes[tid].keyframeCtrls:
                pid = kfctrl.name
                for kf in kfctrl.getKeyframeList():
                    kfView.importKeyframe(tid, pid, kf.frameNo, kf)
                if not kfctrl.SingleSet:
                    for i in range(len(kfctrl.keyframeSetKeys)):
                        subkey = kfctrl.keyframeSetKeys[i]
                        for kf in kfctrl.keyframeSets[i].getKeyframeList():
                            kfView.importKeyframe(tid, subkey, kf.frameNo, kf)
            kfView.keyframeNameList.groupExpand[tid] = False
        # hide unused properties
        processhideUnusedProperties(False)
        # done import
        kfView.arrangeImportedKeyframes()
        kfView.setFixFrameLengthMode(True, clip.frameLength)
        onFrameChange(int(clip.curFrame))
        # update subkey info, after import
        subkeyLinkUpdate(kfView)
        # setup callback
        kfView.onFrameChangeCallback = kfViewFrameChange
        kfView.onRowSelecCallback = kfViewRowSelect
        kfView.onDblClickRowCallback = kfViewDblClickRow
        kfView.onKeyframeMoveCallback = kfViewKeyframeMove
        kfView.onKeyframeSelectCallback = kfViewKeyframeSelect
        kfView.onFrameRightClickCallback = kfViewRightClickRegist
        kfView.onKeyframeNameResizeCallback = kfViewNameResize
        # clear request
        sd.kfamReloadRequest = False
    def processhideUnusedProperties(refresh):
        hiddenNames = sd.kfamKeyframeView.keyframeNameList.hiddenNames
        hiddenNames.clear()
        if gd.kfaSetting.hideUnusedProperties:
            for tid in clip.keyframes:
                if not clip.keyframes[tid].HasKeyframe:
                    continue    # no-keyframe at all, do not hide any properties for it
                unusedP = set(sd.kfamKeyframeView.keyframeNameList.groupContents[tid])
                for kfctrl in clip.keyframes[tid].keyframeCtrls:
                    if kfctrl.HasKeyframe:
                        unusedP.discard(kfctrl.name)
                        if not kfctrl.SingleSet:
                            mainkey = tid + "#" + kfctrl.name
                            if mainkey in sd.kfamSubkeyExpand and sd.kfamSubkeyExpand[mainkey]:
                                for subkey, kfset in kfctrl.getKeySetPair:
                                    if kfset.HasKeyframe:
                                        unusedP.discard(subkey)
                for pid in unusedP:
                    key = tid + "#" + pid
                    hiddenNames.add(key)
        else:
            for mainkey in sd.kfamSubkeyExpand:
                if not sd.kfamSubkeyExpand[mainkey]:
                    hiddenNames = hiddenNames.union(sd.kfamSubkeyLinkInfo[mainkey])
            sd.kfamKeyframeView.keyframeNameList.hiddenNames = hiddenNames
        if refresh:
            sd.kfamKeyframeView.refreshKeyframe()
        sd.kfamHidePropertiesRequest = False
    def prepareShortcuts():
        # register helper
        gd.kfaSetting.schelp_register = registerKeyframe
        gd.kfaSetting.schelp_delete = deleteKeyframe
        gd.kfaSetting.schelp_copyKeyframe = copyKeyframe
        gd.kfaSetting.schelp_pasteKeyframe = pasteKeyframe
        # register a handle for shortcut
        game.event_unreg_listener("update", kfam_editor_update_v2, "kfam2_editor_update")
        game.event_reg_listener("update", kfam_editor_update_v2, "kfam2_editor_update")
        # disable vanilla keys
        if gd.kfaSetting.disableStudioKeys:
            VNEnableStudioShortcuts(False)
        if gd.kfaSetting.disableCameraKeys:
            VNEnableCameraShortcuts(False)
    def cleanupShortcuts():
        # un-register helper
        gd.kfaSetting.schelp_register = None
        gd.kfaSetting.schelp_delete = None
        gd.kfaSetting.schelp_copyKeyframe = None
        gd.kfaSetting.schelp_pasteKeyframe = None
        # un-register shortcut handle
        game.event_unreg_listener("update", kfam_editor_update_v2, "kfam2_editor_update")
        # enable vanilla keys
        VNEnableStudioShortcuts(True)
        VNEnableCameraShortcuts(True)
    # keyframe view callback
    def kfViewFrameChange(kfView):
        clip.seek(kfView.curFrame)
    def kfViewRowSelect(kfView):
        sd.kfamCurSelKeyframeSetCache = None
        selkeys = copy.copy(kfView.keyframeNameList.highlightKeys)
        needupd = False
        for selkey in selkeys:
            if selkey in sd.kfamSubkeyExpand and sd.kfamSubkeyExpand[selkey]:
                for skey in sd.kfamSubkeyLinkInfo[selkey]:
                    if skey not in selkeys and not kfView.isHiddenBoneKey(skey):
                        selkeys.append(skey)
                        needupd = True
        if needupd:
            kfView.selectBonesByKey(selkeys)
    def kfViewDblClickRow(kfView, tgtRowKey):
        if tgtRowKey in sd.kfamSubkeyExpand:
            sd.kfamSubkeyExpand[tgtRowKey] = not sd.kfamSubkeyExpand[tgtRowKey]
            kfView.selectKeyframes(set())
            closeCurveSubWindow()
            subkeyLinkUpdate(kfView)
    def kfViewKeyframeMove(kfView, keyMoveInfo):
        # regroup move info (fromFrame, ToFrame) by tid and pid
        regrouped = {}
        additionMove = {}
        for (bKey, oldFrame, newFrame, tgtObj) in keyMoveInfo:
            tid, pid = bKey.split("#", 1)
            if tid in regrouped:
                if pid in regrouped[tid]:
                    regrouped[tid][pid].add((oldFrame, newFrame))
                else:
                    regrouped[tid][pid] = {(oldFrame, newFrame),}
            else:
                regrouped[tid] = {pid: {(oldFrame, newFrame),}}
            if bKey in sd.kfamSubkeyLinkInfo:   # if key is a bundle, save for ui change
                if bKey in additionMove:
                    additionMove[bKey].add((oldFrame, newFrame))
                else:
                    additionMove[bKey] = {(oldFrame, newFrame)}
        # move in clip
        for tid in regrouped:
            for pid in regrouped[tid]:
                kfctrl = clip.getKeyframeCtrl(tid, pid)
                if kfctrl.name == pid:
                    kfctrl.moveKeyframes(regrouped[tid][pid])
                else:
                    kfctrl.moveKeyframes({pid: regrouped[tid][pid]})
        # addition move in ui, if moved one is bundle key
        for bKey in additionMove:
            tid, pid = bKey.split("#", 1)
            kfctrl = clip.getKeyframeCtrl(tid, pid)
            for oldFrame, newFrame in additionMove[bKey]:
                kfbundle = kfView.keyframeBuffer[bKey][newFrame]    # bundle keyframe already moved
                for subkey in kfbundle.bundle.keys():
                    #print("need to move keyframe of %s from %d to %d"%(subkey, oldFrame, newFrame))
                    kfView.deleteKeyframes({(tid+"#"+subkey, oldFrame)})
                    kfView.registerKeyframe(tid, subkey, newFrame, kfctrl.getKeyframe(subkey, newFrame))
        # refresh view
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
        # update clip
        clip.applyFrames()
    def kfViewKeyframeSelect(kfView):
        curveKeyframes = set()
        valueKeyframes = {}
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            kf = kfbtn.frameObj
            if hasattr(kf, "curve") and kf.curve != None:
                curveKeyframes.add(kf)
            if isinstance(kf, Keyframe2Bundle):
                for key in kf.bundle.keys():
                    skf = kf.bundle[key]
                    if hasattr(skf, "curve") and skf.curve != None:
                        curveKeyframes.add(skf)
            if isinstance(kf, Keyframe2):
                if kfbtn.key in valueKeyframes:
                    valueKeyframes[kfbtn.key].append(kf)
                else:
                    valueKeyframes[kfbtn.key] = [kf]
        updateCurveValueInput(curveKeyframes, valueKeyframes)
    def kfViewRightClickRegist(kfView, key, frame, isGroup):
        if frame > clip.frameLength:
            return
        if isGroup:
            tid = key.split("#")[0]
            tgtProps = set()
            for pid in kfView.keyframeNameList.groupContents[tid]:
                if not kfView.isHiddenBoneName(tid, pid):
                    tgtProps.add(pid)
        else:
            tid, pid = key.split("#", 1)
            tgtProps = {pid}
        newKfs = clip.keyframes[tid].registerKeyframes(frame, tgtProps)
        for pn in newKfs.keys():
            mainkey = tid + "#" + pn
            if mainkey in sd.kfamSubkeyExpand and sd.kfamSubkeyExpand[mainkey]:
                continue    # skip add to mainkey if mainkey is expanded
            kfView.registerKeyframe(tid, pn, frame, newKfs[pn])
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
    def kfViewNameResize(kfView, nameWidth):
        sd.kfamSelectAllBtnRect = Rect(kfView.viewRect.x, kfView.viewRect.y - 1, kfView.curNameWidth / 2 - 1, kfView.curNoHeight + 1)
        sd.kfamSelectChgBtnRect = Rect(sd.kfamSelectAllBtnRect.right, sd.kfamSelectAllBtnRect.top, sd.kfamSelectAllBtnRect.width, sd.kfamSelectAllBtnRect.height)
    # curve editor callback
    def curveChange(curveEditor):
        selectedKeyframes = set()
        kfView = sd.kfamKeyframeView
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            kf = kfbtn.frameObj
            if hasattr(kf, "curve") and kf.curve != None:
                selectedKeyframes.add(kf)
            if isinstance(kf, Keyframe2Bundle):
                for key in kf.bundle.keys():
                    skf = kf.bundle[key]
                    if hasattr(skf, "curve") and skf.curve != None:
                        selectedKeyframes.add(skf)
        ec = curveEditor.editCurveData
        for skf in selectedKeyframes:
            skf.curve = [ec[0], ec[2], ec[1], ec[3]]
    # seekbar callback
    def onFrameChange(newFrameNo):
        if not clip.isPlaying and newFrameNo != sd.kfamKeyframeView.curFrame:
            sd.kfamKeyframeView.setCurrentFrame(newFrameNo)
    def onJumpToKeyframe(curFrame, direction):
        kfView = sd.kfamKeyframeView
        selKeys = getCurSelectedKeys(kfView)
        if not clip.isPlaying and len(selKeys) > 0:
            if sd.kfamCurSelKeyframeSetCache:
                kfset = sd.kfamCurSelKeyframeSetCache
            else:
                kfset = buildSelKeyframesSetCache(selKeys)
            if curFrame not in kfset:
                kfset.add(curFrame)
                tempDelFlg = True
            else:
                tempDelFlg = False
            kfsortlist = sorted(list(kfset))
            cidx = kfsortlist.index(curFrame)
            if tempDelFlg:
                kfset.remove(curFrame)
            if direction == 1 and cidx < len(kfsortlist) - 1:
                return kfsortlist[cidx + 1]
            if direction == -1 and cidx > 0:
                return kfsortlist[cidx - 1]
        return None
    # keyframe operation
    def registerKeyframe():
        # check keys
        kfView = sd.kfamKeyframeView
        selKeys = getCurSelectedKeys(kfView)
        canRegister = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(selKeys) > 0
        if not canRegister:
            return
        # group up
        groupKeys = {}
        for key in selKeys:
            gn, pn = key.split("#", 1)
            if gn in groupKeys:
                groupKeys[gn].add(pn)
            else:
                groupKeys[gn] = {pn}
        # build and add keyframe
        for gn in groupKeys.keys():
            kfCtrlSet = clip.keyframes[gn]
            newKfs = kfCtrlSet.registerKeyframes(kfView.curFrame, groupKeys[gn])
            # to UI, check new sub-bone
            newKeys = set()
            for pn in newKfs.keys():
                if pn not in kfView.keyframeNameList.groupContents[gn]:
                    newKeys.add(pn)
            if len(newKeys):
                sd.kfamReloadRequest = True
                raise Exception("New properties found, reload all keyframes")
            # add keyframes
            for pn in newKfs.keys():
                mainkey = gn + "#" + pn
                if mainkey in sd.kfamSubkeyExpand and sd.kfamSubkeyExpand[mainkey]:
                    continue    # skip add to mainkey if mainkey is expanded
                kfView.registerKeyframe(gn, pn, kfView.curFrame, newKfs[pn])
        processhideUnusedProperties(False)
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
    def copyKeyframe():
        # check
        kfView = sd.kfamKeyframeView
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        canCopy = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(selectInfo) > 0
        if not canCopy:
            return
        # clear old clipboard
        sd.kfamKeyframeClipboard.clear()
        sd.kfamKeyframeClipboardLength = -1
        # build clipboard
        baseFrame = min([si[1] for si in selectInfo])
        for (kfKey, kfFrame) in selectInfo:
            diffFrame = kfFrame - baseFrame
            orgFrame = kfView.keyframeBuffer[kfKey][kfFrame]
            sd.kfamKeyframeClipboard.add((kfKey, diffFrame, orgFrame.copy()))
            if diffFrame > sd.kfamKeyframeClipboardLength:
                sd.kfamKeyframeClipboardLength = diffFrame
    def pasteKeyframe():
        # check
        kfView = sd.kfamKeyframeView
        canPaste = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(sd.kfamKeyframeClipboard) > 0 and sd.kfamKeyframeClipboardLength >= 0
        if not canPaste:
            return
        if kfView.curFrame + sd.kfamKeyframeClipboardLength > clip.frameLength:
            scriptHelperGUIMessage("Can not paste copied keyframes here. Copied keyframe span is %d, if you want to copy at #%d, you need to extend clip length to %d first."%(sd.kfamKeyframeClipboardLength, kfView.curFrame, sd.kfamKeyframeClipboardLength + kfView.curFrame), ("OK",))
            return
        # paste to clip
        newSelectKeyframeInfo = set()
        groupedKfInfo = {} 
        for (kfKey, diffFrame, orgFrame) in sd.kfamKeyframeClipboard:
            tid, pid = kfKey.split("#", 1)
            frameNo = kfView.curFrame + diffFrame
            newkf = orgFrame.copy()
            newkf.frameNo = frameNo
            if isinstance(newkf, Keyframe2Bundle):
                # align for bundle
                for key in newkf.bundle:
                    nkf = newkf.bundle[key].copy()
                    nkf.frameNo = frameNo
                    newkf.bundle[key] = nkf
            # group up 
            if tid in groupedKfInfo:
                if pid in groupedKfInfo[tid]:
                    groupedKfInfo[tid][pid].add(newkf)
                else:
                    groupedKfInfo[tid][pid] = {newkf}
            else:
                groupedKfInfo[tid] = {pid: {newkf}}
        # update clip
        for tid in groupedKfInfo.keys():
            for pid in groupedKfInfo[tid].keys():
                kfctrl = clip.getKeyframeCtrl(tid, pid)
                if pid != kfctrl.name:
                    # paste a subkey
                    kfs = set()
                    for kf in groupedKfInfo[tid][pid]:
                        bkf = Keyframe2Bundle({pid:kf})
                        kfs.add(bkf)
                    groupedKfInfo[tid][pid] = kfs
                else:
                    kfs = groupedKfInfo[tid][pid]
                kfctrl.updateKeyframes(kfs)
        
                # update ui
                for newkf in groupedKfInfo[tid][pid]:
                    frameNo = newkf.frameNo
                    if isinstance(newkf, Keyframe2Bundle):
                        # paste a bundle key
                        newSelectInvKey = set()
                        for key in newkf.bundle:
                            kfView.registerKeyframe(tid, key, frameNo, newkf.bundle[key])
                            # select later
                            kfKey = tid + "#" + key
                            newSelectInvKey.add((kfKey, frameNo))
                        # update keyframe to main key if main key is not expaned
                        mainkey = tid + "#" + kfctrl.name
                        if mainkey in sd.kfamSubkeyExpand and not sd.kfamSubkeyExpand[mainkey]:
                            mainBundle = kfctrl.getKeyframe(None, frameNo)
                            kfView.registerKeyframe(tid, kfctrl.name, frameNo, mainBundle)
                            kfKey = tid + "#" + kfctrl.name
                            newSelectKeyframeInfo.add((kfKey, frameNo))
                        else:
                            newSelectKeyframeInfo.update(newSelectInvKey)   # select bundle or subkey, not both
                    else:
                        kfView.registerKeyframe(tid, pid, frameNo, newkf)
                        # select later
                        kfKey = tid + "#" + pid
                        newSelectKeyframeInfo.add((kfKey, frameNo))
        # update status
        processhideUnusedProperties(False)
        kfView.refreshKeyframe()
        kfView.selectKeyframes(newSelectKeyframeInfo)
        sd.kfamCurSelKeyframeSetCache = None
        kfViewKeyframeSelect(kfView)
        # update clip
        clip.applyFrames()
    def deleteKeyframe():
        kfView = sd.kfamKeyframeView
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        canDelete = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(selectInfo) > 0
        if not canDelete:
            return
        # from clip
        selInfoArranged = {}
        for (kfKey, frameNo) in selectInfo:
            tid, pid = kfKey.split("#", 1)
            #print("Keyframe to delete: %s>%s>%d"%(tid, pid, frameNo))
            if tid in selInfoArranged:
                if pid in selInfoArranged[tid]:
                    selInfoArranged[tid][pid].add(frameNo)
                else:
                    selInfoArranged[tid][pid] = {frameNo}
            else:
                selInfoArranged[tid] = {pid: {frameNo}}
        for tid in selInfoArranged:
            for pid in selInfoArranged[tid]:
                kfctrl = clip.getKeyframeCtrl(tid, pid)
                kfctrl.deleteKeyframes(None if pid == kfctrl.name else pid, selInfoArranged[tid][pid])
        # from UI
        additionDelInfo = set()
        for (kfKey, frameNo) in selectInfo:
            if kfKey in sd.kfamSubkeyLinkInfo and kfKey in kfView.keyframeBuffer:
                kfbundle = kfView.keyframeBuffer[kfKey][frameNo]
                pid = kfKey.split("#")[0]
                for subkey in kfbundle.bundle:
                    additionDelInfo.add((pid + "#" + subkey, frameNo))
        kfView.deleteKeyframes(selectInfo.union(additionDelInfo))
        processhideUnusedProperties(False)
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
        kfViewKeyframeSelect(kfView)
        # update clip
        clip.applyFrames()
    # keyframe batch selection
    def selectKeyframeAll():
        kfView = sd.kfamKeyframeView
        selInfo = set()
        for key in kfView.keyframeBox.keyframes.keys():
            for col in kfView.keyframeBox.keyframes[key].keys():
                selInfo.add((key, col))
        kfView.selectKeyframes(selInfo)
    def selectKeyframeInvert():
        kfView = sd.kfamKeyframeView
        selInfo = set()
        for key in kfView.keyframeBox.keyframes.keys():
            for col in kfView.keyframeBox.keyframes[key].keys():
                if not kfView.keyframeBox.keyframes[key][col].selected:
                    selInfo.add((key, col))
        kfView.selectKeyframes(selInfo)
    def selectKeyframeVertical():
        kfView = sd.kfamKeyframeView
        selInfo = getCurSelectedKeyframeInfo(kfView)
        colToAdd = set()
        # orginal selected ones
        for (key, col) in selInfo:
            colToAdd.add(col)
        for key in kfView.keyframeBox.keyframes.keys():
            for col in colToAdd:
                if col in kfView.keyframeBox.keyframes[key]:
                    selInfo.add((key, col))
        kfView.selectKeyframes(selInfo)
    def selectKeyframeRange(tgtProps, fromFrame, toFrame):
        kfView = sd.kfamKeyframeView
        selInfo = set()
        for key in tgtProps:
            if not key in kfView.keyframeBox.keyframes:
                continue
            for col in kfView.keyframeBox.keyframes[key].keys():
                if fromFrame <= col <= toFrame:
                    selInfo.add((key, col))
        kfView.selectKeyframes(selInfo)
    # keyframe batch shift
    def shiftKeyframes(keyframes, shiftOffset):
        #print("Shift %d keyframes by %d"%(len(keyframes), shiftOffset))
        # shift in clip regroup
        kfView = sd.kfamKeyframeView
        regroupedKfs = {}
        for (bkey, frame) in keyframes:
            tid, pid = bkey.split("#", 1)
            kfctrl = clip.getKeyframeCtrl(tid, pid)
            moveInfo = (frame, frame + shiftOffset)
            if kfctrl.SingleSet:
                if kfctrl in regroupedKfs:
                    regroupedKfs[kfctrl].add(moveInfo)
                else:
                    regroupedKfs[kfctrl] = {moveInfo}
            else:
                if kfctrl.name == pid:
                    pass    # pid is not key, skip
                elif kfctrl in regroupedKfs:
                    if pid in regroupedKfs[kfctrl]:
                        regroupedKfs[kfctrl][pid].add(moveInfo)
                    else:
                        regroupedKfs[kfctrl][pid] = {moveInfo}
                else:
                    regroupedKfs[kfctrl] = {pid: {moveInfo}}
        for kfctrl in regroupedKfs.keys():
            kfctrl.moveKeyframes(regroupedKfs[kfctrl])
        # shift in ui
        uiToRegist = set()
        for (bkey, frame) in keyframes:
            tid, pid = bkey.split("#", 1)
            newFrame = frame + shiftOffset
            kfctrl = clip.getKeyframeCtrl(tid, pid)
            kfView.deleteKeyframes({(bkey, frame)})
            uiToRegist.add((tid, pid, newFrame, kfctrl.getKeyframe(None if kfctrl.name == pid else pid, newFrame)))
        for tid, pid, frameNo, keyframe in uiToRegist:
            kfView.registerKeyframe(tid, pid, frameNo, keyframe)
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
        # update clip
        clip.applyFrames()
    def shiftKeyframesAfterCurrentFrame(shiftOffset):
        kfView = sd.kfamKeyframeView
        #print("Shift keyframes from #%d by %d"%(kfView.curFrame, shiftOffset))
        tgtkeyframes = set()
        for bkey in kfView.keyframeBuffer.keys():
            for frame in kfView.keyframeBuffer[bkey].keys():
                if frame >= kfView.curFrame:
                    tgtkeyframes.add((bkey, frame))
        if len(tgtkeyframes):
            #print("Shift targets:" + str(tgtkeyframes))
            shiftKeyframes(tgtkeyframes, shiftOffset)
    def shiftHasKeyframeOnCurrentFrame():
        kfView = sd.kfamKeyframeView
        for ctrlset in clip.keyframes.values():
            if kfView.curFrame in ctrlset.getKeyframeNoList():
                return True
        return False
    # util
    def getCurSelectedKeys(kfView):
        return kfView.keyframeNameList.highlightKeys
    def getCurSelectedKeyframeInfo(kfView):
        selectedKfInfo = set()
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            selectedKfInfo.add((kfbtn.key, kfbtn.column))
        return selectedKfInfo    
    def buildSelKeyframesSetCache(selKeys):
        sd.kfamCurSelKeyframeSetCache = set()
        for bKey in selKeys:
            sd.kfamCurSelKeyframeSetCache.update(sd.kfamKeyframeView.keyframeBuffer[bKey].keys())
        return sd.kfamCurSelKeyframeSetCache
    def updateCurveValueInput(curveKeyframes, valueKeyframes):
        if len(curveKeyframes) > 0 or len(valueKeyframes) == 1:
            openCurveSubWindow()
        else:
            closeCurveSubWindow()
        # curve part
        if len(curveKeyframes) > 0:
            sd.kfamCurveEditor.clearCurveData()
            for skf in curveKeyframes:
                xcurve = [skf.curve[0], skf.curve[2], skf.curve[1], skf.curve[3]]
                sd.kfamCurveEditor.importCurveData(xcurve, False)
            sd.kfamCurveEditor.selectTargetCurve()
            sd.kfamCurveEditor.curveNeedRepaint = True
        else:
            sd.kfamCurveEditor.clearCurveData()
        # value part
        if len(valueKeyframes) == 1:
            sd.kfamValueNameOnEditor = valueKeyframes.keys()[0]
            sd.kfamValueKeyframesOnEditor = valueKeyframes[sd.kfamValueNameOnEditor]
            sd.kfamValueOrginal = script2string(sd.kfamValueKeyframesOnEditor[0].value)
            sd.kfamValueInputTemp = sd.kfamValueOrginal
        else:
            sd.kfamValueNameOnEditor = None
            sd.kfamValueKeyframesOnEditor = None
            sd.kfamValueInputTemp = sd.kfamValueOrginal = ""
    def selectPropertiesById(tid):
        kfView = sd.kfamKeyframeView
        selkeys = []
        if tid in kfView.keyframeNameList.groupContents.keys():
            savedPname = set()
            for kfctrl in clip.keyframes[tid].keyframeCtrls:
                if kfctrl.HasKeyframe:
                    savedPname.add(kfctrl.name)
            if len(savedPname) == 0:
                savedPname = set(kfView.keyframeNameList.groupContents[tid])
            for pname in savedPname:
                key = tid + "#" + pname
                if not kfView.isHiddenBoneName(tid, pname):
                    selkeys.append(key)
        sd.kfamCurSelKeyframeSetCache = None
        kfView.selectBonesByKey(selkeys)
    def selectPropertiesAll():
        kfView = sd.kfamKeyframeView
        selkeys = getCurSelectedKeys(kfView)
        # check current checked tid
        groupCheck = {}
        for skey in selkeys:
            gn, pn = skey.split("#", 1)
            if gn in groupCheck:
                groupCheck[gn].add(pn)
            else:
                groupCheck[gn] = {pn}
        # check fully selection
        fullSelected = {}
        allFullSelected = True
        for gn in groupCheck:
            allInGroup = [pn for pn in kfView.keyframeNameList.groupContents[gn] if not kfView.isHiddenBoneName(gn, pn)]
            fullSelected[gn] = len(allInGroup) == len(groupCheck[gn])
            allFullSelected = allFullSelected and fullSelected[gn]
        # all action pattern
        if allFullSelected:
            # select all of tid and group
            allSelTgt = clip.keyframes.keys()
        else:
            # select all properties of tid which had some properties selected
            allSelTgt = groupCheck.keys()
        # do select
        newSelKeys = []
        for tid in allSelTgt:
            for pname in kfView.keyframeNameList.groupContents[tid]:
                key = tid + "#" + pname
                if not kfView.isHiddenBoneName(tid, pname):
                    newSelKeys.append(key)
        sd.kfamCurSelKeyframeSetCache = None
        kfView.selectBonesByKey(newSelKeys)
    def selectPropertiesCHG():
        kfView = sd.kfamKeyframeView
        selkeys = []
        # check all target object, get the changed properties. If no keyframe, call properties mark as changed, or, only existed and changed one marked.
        for tid in clip.keyframes.keys():
            diffReport = clip.keyframes[tid].statusDiffReport(kfView.curFrame)
            for pid in diffReport:
                key = tid + "#" + pid
                if key in sd.kfamSubkeyExpand:
                    if sd.kfamSubkeyExpand[key]:
                        isDiff = diffReport[pid] in {"Diff", "NewValue"}
                    else:
                        isDiff = diffReport[pid] in {"Diff", "NewValue", "PartSame"}
                else:
                    isDiff = diffReport[pid] != "Same"
                if isDiff and not kfView.isHiddenBoneName(tid, pid):
                    selkeys.append(key)
        sd.kfamCurSelKeyframeSetCache = None
        kfView.selectBonesByKey(selkeys)
    def deleteTargetObjectId():
        del clip.keyframes[sd.kfamCurSelTargetId]
        if sd.kfamCurSelTargetId in mClips:
            toggleClipSubWindow(None)
        closeCurveSubWindow()
        sd.kfamReloadRequest = True
    def getTargetInfo(game, tid):
        sd = game.scenedata
        if tid in sd.actors:
            tno = sd.actors[tid].objctrl.treeNodeObject
            isSel = tno in game.studio.treeNodeCtrl.selectNodes and len(game.studio.treeNodeCtrl.selectNodes) == 1
            ttitle = "<color=#ff0000>" + tid + ":</color>" + sd.actors[tid].text_name
        elif tid in sd.props:
            tno = sd.props[tid].objctrl.treeNodeObject
            isSel = tno in game.studio.treeNodeCtrl.selectNodes and len(game.studio.treeNodeCtrl.selectNodes) == 1
            ttitle = "<color=#00ffff>" + tid + ":</color>" + sd.props[tid].text_name
        elif tid in mClips:
            tno = None
            isSel = False
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "Clip"
        elif tid in vntext_mgr.allTextIds:
            tno = vntext_mgr.getTextInfo(tid).anchorFolder.objctrl.treeNodeObject
            isSel = tno in game.studio.treeNodeCtrl.selectNodes and len(game.studio.treeNodeCtrl.selectNodes) == 1
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "VNText"
        elif tid in vnsound_mgr.soundAlias:
            tno = None
            isSel = False
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "VNSound"
        elif tid in vnphy_mgr.triggerIdDict:
            tno = None
            isSel = False
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "Trigger"
        elif tid in vnphy_mgr.actionIdDict:
            tno = None
            isSel = False
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "Action"
        elif tid == "cam":
            tno = None
            isSel = False
            ttitle = "<color=#ffff00>" + "Camera" + "</color>"
        elif tid == "sys":
            tno = None
            isSel = False
            ttitle = "<color=#ffff00>" + "Scene" + "</color>"
        else:
            raise Exception("Unknown target id <%s> in clip's keyframes"%tid)
        return tno, isSel, ttitle
    def subkeyLinkUpdate(kfView):
        for mainkey in sd.kfamSubkeyLinkInfo:
            for subkey in sd.kfamSubkeyLinkInfo[mainkey]:
                # change show/hide for every subkey
                tid, pid = subkey.split("#", 1)
                kfctrl = clip.getKeyframeCtrl(tid, pid)
                hide = not sd.kfamSubkeyExpand[mainkey]
                if not hide and gd.kfaSetting.hideUnusedProperties and kfctrl.HasKeyframe:
                    if len(kfView.keyframeBuffer[subkey]) == 0:
                        hide = True
                kfView.keyframeNameList.hideName(tid, pid, hide)
            if sd.kfamSubkeyExpand[mainkey]:
                # remove keyframes in mainkey
                mainKfs = set()
                for col in kfView.keyframeBuffer[mainkey]:
                    mainKfs.add((mainkey, col))
                kfView.deleteKeyframes(mainKfs)
            else:
                # add bundle keyframs in mainkey
                tid, pid = mainkey.split("#", 1)
                kfctrl = clip.getKeyframeCtrl(tid, pid)
                for kf in kfctrl.getKeyframeList():
                    kfView.registerKeyframe(tid, pid, kf.frameNo, kf)
        kfView.refreshKeyframe()

    # init clip editor window
    if sd.kfamModifyTabIndex == -1 or sd.kfamReloadRequest:
        initClipEditorWindow()
    if sd.kfamResizeRequest:
        resizeClipEditorWindow()
    if sd.kfamHidePropertiesRequest:
        processhideUnusedProperties(True)
    fullw = game.windowRect.width - wndWidthMargin
    contentsHeight = game.windowRect.height - wndHeightMargin
    smbtnstyle = GUIStyle("button")
    smbtnstyle.fontSize = 12
    tgtbtnstyle = GUIStyle("button")
    tgtbtnstyle.fontSize = 12
    tgtbtnstyle.alignment = TextAnchor.MiddleLeft
    tgtbtnstyle.margin = RectOffset(0,0,2,2)
    frmbtnstyle = GUIStyle("button")
    frmbtnstyle.fontSize = 10
    frmbtnstyle.margin = RectOffset(0,0,1,1)

    # seek bar
    kfam_GUI_parts_seekbar(sh, onFrameChange, None if len(getCurSelectedKeys(sd.kfamKeyframeView)) == 0 else onJumpToKeyframe)

    # main body
    GUILayout.BeginHorizontal()
    # left panel: target list
    GUILayout.BeginVertical(GUILayout.Width(gd.kfaSetting.targetPanelWidth), GUILayout.Height(contentsHeight))
    # target operation
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Add", smbtnstyle):
        if sd.kfamCurveWndId:
            closeCurveSubWindow()
            sd.kfamKeyframeView.selectKeyframes(set())
        if sd.kfamSubClipWndId:
            toggleClipSubWindow(None)
            sd.kfamCurSelTargetId = None
        changeGuiScreen(sh, kfam_GUI_modify_clip_add)
    if sd.kfamCurSelTargetId:
        if GUILayout.Button("Delete", smbtnstyle):
            scriptHelperGUIMessage("Sure to delete %s from clip?\n<color=#ff0000>WARNING: ALL KEYFRAMES WILL BE DELETED!!</color>"%sd.kfamCurSelTargetId, (("Yes, delete it", deleteTargetObjectId), "Cancel"))
        if GUILayout.Button("<<" if sd.kfamSetupCtrlDetail else ">>", smbtnstyle, GUILayout.Width(25)):
            sd.kfamSetupCtrlDetail = not sd.kfamSetupCtrlDetail
    else:
        sd.kfamSetupCtrlDetail = False
    GUILayout.EndHorizontal()
    # targets selector
    sd.kfamEditorTargetScroll = GUILayout.BeginScrollView(sd.kfamEditorTargetScroll, GUI.skin.box)
    for tid in sd.kfamSortedTargetId:
        tno, isSel, ttitle = getTargetInfo(game, tid)
        # +/- target button
        GUILayout.BeginHorizontal()
        if GUILayout.Button("-" if sd.kfamTargetKeyframeExpand[tid] else "+", tgtbtnstyle, GUILayout.Width(20)):
            sd.kfamTargetKeyframeExpand[tid] = not sd.kfamTargetKeyframeExpand[tid]
        oColor = GUI.color
        if sd.kfamCurSelTargetId == tid: 
            GUI.color = Color.green
        if GUILayout.Button(ttitle, tgtbtnstyle):
            if sd.kfamCurSelTargetId != tid:
                sd.kfamCurSelTargetId = tid
            else:
                sd.kfamCurSelTargetId = None
            if sd.kfamCurSelTargetId and tno:
                if not isSel:
                    game.studio.treeNodeCtrl.selectNode = tno
            else:
                game.studio.treeNodeCtrl.selectNode = None
            selectPropertiesById(sd.kfamCurSelTargetId)
            toggleClipSubWindow(sd.kfamCurSelTargetId)
        GUILayout.EndHorizontal()
        # target keyframe
        if not clip.isPlaying and sd.kfamTargetKeyframeExpand[tid]:
            kfnos = clip.keyframes[tid].getKeyframeNoList()
            if len(kfnos):
                capped = True
                for i in range(len(kfnos)):
                    kfn = kfnos[i]
                    if i % 3 == 0:
                        GUILayout.BeginHorizontal()
                        GUILayout.Space(10)
                        capped = False
                    GUI.color = Color.yellow if kfn == sd.kfamKeyframeView.curFrame else oColor
                    if GUILayout.Button(str(kfn), frmbtnstyle, GUILayout.Width(39), GUILayout.Height(16)):
                        clip.seek(kfn)
                        onFrameChange(kfn)
                    if i % 3 == 2:
                        GUILayout.EndHorizontal()
                        capped = True
                if not capped:
                    GUILayout.EndHorizontal()
            else:
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUI.color = Color.yellow
                GUILayout.Label("No keyframe")
                GUILayout.EndHorizontal()
        GUI.color = oColor
    GUILayout.FlexibleSpace()
    GUILayout.EndScrollView()
    # target operations
    GUILayout.EndVertical()

    # right panel:
    ctrlDetailView = sd.kfamCurSelTargetId and sd.kfamSetupCtrlDetail
    if ctrlDetailView:
        sd.kfamKeyframeEditorMode = False
        # target anime controller detaile setting - new in anime2
        GUILayout.BeginVertical(GUILayout.Width(sd.kfamKeyframeView.viewRect.width), GUILayout.Height(contentsHeight))
        # title
        no, isSel, ttitle = getTargetInfo(game, sd.kfamCurSelTargetId)
        GUILayout.Label("Detail setting for <%s>"%ttitle)
        # detail
        kfam_GUI_parts_ctrlset_detail(game, clip.keyframes[sd.kfamCurSelTargetId])
        GUILayout.EndVertical()
    else:
        sd.kfamKeyframeEditorMode = True
        # keyframe view and buttons
        sd.kfamKeyframeView.paint()
        if GUI.Button(sd.kfamSelectAllBtnRect, "ALL" if sd.kfamSelectAllBtnRect.width > 35 else "A", smbtnstyle):
            selectPropertiesAll()
        if GUI.Button(sd.kfamSelectChgBtnRect, "CHG" if sd.kfamSelectChgBtnRect.width > 35 else "C", smbtnstyle):
            selectPropertiesCHG()
    # main body end
    GUILayout.EndHorizontal()

    # operation control buttons
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    #GUILayout.FlexibleSpace()
    if not ctrlDetailView and sd.kfamOperationMode == "normal" and ctrl:
        # select operations
        GUILayout.BeginHorizontal()
        GUILayout.Label("Batch Select")
        if GUILayout.Button("All", btnstyle):
            selectKeyframeAll()
        if GUILayout.Button("Invert", btnstyle):
            selectKeyframeInvert()
        if GUILayout.Button("Vertical", btnstyle):
            selectKeyframeVertical()
        if GUILayout.Button("Range...", btnstyle):
            sd.kfamOperationMode = "range"
            sd.kfamOperationRangeSelectData = GData()
            sd.kfamOperationRangeSelectData.allProperties = len(getCurSelectedKeys(sd.kfamKeyframeView)) == 0
            sd.kfamOperationRangeSelectData.fromFrame = ""
            sd.kfamOperationRangeSelectData.toFrame = ""
            sd.kfamOperationRangeSelectData.error = None
        GUILayout.EndHorizontal()
    elif not ctrlDetailView and sd.kfamOperationMode == "normal" and shift:
        # shift operations
        GUILayout.BeginHorizontal()
        GUILayout.Label("Batch Shift")
        if GUILayout.Button("Insert frame at <color=#00ff00>#%d</color>"%sd.kfamKeyframeView.curFrame, btnstyle):
            if clip.lastFrameNo == clip.frameLength:
                scriptHelperGUIMessage("Keyframe existed on the last frame, so there is no space to insert empty frame anymore! Increase frame length in setting first.", ("OK",))
            else:
                shiftKeyframesAfterCurrentFrame(1)
        if GUILayout.Button("Remove frame <color=#00ff00>#%d</color>"%sd.kfamKeyframeView.curFrame, btnstyle):
            if shiftHasKeyframeOnCurrentFrame():
                scriptHelperGUIMessage("Keyframes existed on frame #<color=#00ff00>%d</color>! This function can only remove empty frame to shift keyframes."%sd.kfamKeyframeView.curFrame, ("OK",))
            else:
                shiftKeyframesAfterCurrentFrame(-1)
        if GUILayout.Button("Set Offset...", btnstyle):
            sd.kfamOperationMode = "shift"
            sd.kfamOperationShiftData = GData()
            sd.kfamOperationShiftData.selectedKeyframes = len(sd.kfamKeyframeView.keyframeBox.selectedKeyframes) > 0
            sd.kfamOperationShiftData.shiftStep = ""
            sd.kfamOperationShiftData.error = None
        GUILayout.EndHorizontal()
    elif not ctrlDetailView and sd.kfamOperationMode == "range":
        # range selection setup
        GUILayout.BeginHorizontal()
        rdi = sd.kfamOperationRangeSelectData
        if rdi.error:
            GUILayout.Label("Error: <color=#ff0000>%s</color>"%rdi.error)
            GUILayout.FlexibleSpace()
            if GUILayout.Button("<color=#00ff00>OK</color>", btnstyle):
                rdi.error = None
        else:
            GUILayout.Label("For")
            if GUILayout.Button("<color=#00ffff>ALL</color>" if rdi.allProperties else "<color=#ffff00>SELECTED</color>"):
                rdi.allProperties = not rdi.allProperties
            GUILayout.Label("properties, From")
            rdi.fromFrame = GUILayout.TextField(rdi.fromFrame, GUILayout.Width(50))
            GUILayout.Label("To")
            rdi.toFrame = GUILayout.TextField(rdi.toFrame, GUILayout.Width(50))
            GUILayout.FlexibleSpace()
            if GUILayout.Button("<color=#00ff00>Select</color>", btnstyle):
                try:
                    # check properties
                    if rdi.allProperties:
                        tgtProps = sd.kfamKeyframeView.keyframeBuffer.keys()
                    else:
                        tgtProps = getCurSelectedKeys(sd.kfamKeyframeView)
                    if len(tgtProps) == 0:
                        raise Exception("No property for select")
                    # check frame range
                    ff = int(rdi.fromFrame)
                    tf = int(rdi.toFrame)
                    if ff > tf:
                        raise Exception("From cannot be greater than To")
                    # select and quit
                    selectKeyframeRange(tgtProps, ff, tf)
                    del sd.kfamOperationRangeSelectData
                    sd.kfamOperationMode = "normal"
                except Exception as e:
                    rdi.error = str(e)
            if GUILayout.Button("<color=#ff0000>Cancel</color>", btnstyle):
                del sd.kfamOperationRangeSelectData
                sd.kfamOperationMode = "normal"
        GUILayout.EndHorizontal()
    elif not ctrlDetailView and sd.kfamOperationMode == "shift":
        # shift setup
        GUILayout.BeginHorizontal()
        sdi = sd.kfamOperationShiftData
        if sdi.error:
            GUILayout.Label("Error: <color=#ff0000>%s</color>"%sdi.error)
            GUILayout.FlexibleSpace()
            if GUILayout.Button("<color=#00ff00>OK</color>", btnstyle):
                sdi.error = None
        else:
            GUILayout.Label("Shift")
            if sdi.selectedKeyframes:
                if GUILayout.Button("<color=#ff0000>selected Keyframes</color>"):
                    sdi.selectedKeyframes = False
            else:
                if GUILayout.Button("<color=#00ffff>all keyframes after #%d</color>"%sd.kfamKeyframeView.curFrame):
                    sdi.selectedKeyframes = True
            if len(sd.kfamKeyframeView.keyframeBox.selectedKeyframes) == 0:
                sdi.selectedKeyframes = False
            # calcuate valid offset
            shiftLeft = set()
            shiftRight = set()
            if sdi.selectedKeyframes:
                selInfo = getCurSelectedKeyframeInfo(sd.kfamKeyframeView)
                for bkey, frame in selInfo:
                    tid, pid = bkey.split("#", 1)
                    kfctrl = clip.getKeyframeCtrl(tid, pid)
                    if kfctrl.name == pid:
                        kfNoList = kfctrl.getKeyframeNoList()
                    else:
                        kfNoList = kfctrl.keyframeSets[kfctrl.keyframeSetKeys.index(pid)].keyframeNos
                    fIdx = kfNoList.index(frame)
                    if fIdx == 0:
                        setLeftLimit = -max(frame-1, 0)
                        shiftLeft.add(setLeftLimit)
                    else:
                        llmt = kfNoList[fIdx - 1]
                        if (bkey, llmt) not in selInfo:
                            setLeftLimit = -(frame - llmt - 1)
                            shiftLeft.add(setLeftLimit)
                    if fIdx == len(kfNoList) - 1:
                        setRightLimit = clip.frameLength - frame
                        shiftRight.add(setRightLimit)
                    else:
                        rlmt = kfNoList[fIdx + 1]
                        if (bkey, rlmt) not in selInfo:
                            setRightLimit = rlmt - frame - 1
                            shiftRight.add(setRightLimit)
            else:
                for ctrlset in clip.keyframes.values():
                    if not ctrlset.HasKeyframe:
                        continue
                    kfNoList = ctrlset.getKeyframeNoList()
                    setLeftLimit = 0
                    setFirstTgt = 0
                    for fn in kfNoList:
                        if fn < sd.kfamKeyframeView.curFrame:
                            setLeftLimit = fn
                        else:
                            setFirstTgt = fn
                            break
                    setMaxLeft = -max(setFirstTgt - setLeftLimit - 1, 0)
                    setMaxRight = clip.frameLength - kfNoList[-1]
                    shiftLeft.add(setMaxLeft)
                    shiftRight.add(setMaxRight)
            maxShiftLeft = max(shiftLeft)
            maxShiftRight = min(shiftRight)
            GUILayout.Label("by")
            sdi.shiftStep = GUILayout.TextField(sdi.shiftStep, GUILayout.Width(40))
            GUILayout.Label("(%d ~ %d) frames"%(maxShiftLeft, maxShiftRight))
            GUILayout.FlexibleSpace()
            if GUILayout.Button("<color=#00ff00>Shift</color>", btnstyle):
                try:
                    # check properties
                    shiftStep = int(sdi.shiftStep)
                    if shiftStep > maxShiftRight or shiftStep < maxShiftLeft:
                        raise Exception()
                    # shift
                    if shiftStep != 0:
                        if sdi.selectedKeyframes:
                            shiftKeyframes(selInfo, shiftStep)
                        else:
                            shiftKeyframesAfterCurrentFrame(shiftStep)
                    # quit
                    del sd.kfamOperationShiftData
                    sd.kfamOperationMode = "normal"
                except Exception as e:
                    sdi.error = "Input a valid shift offset between %d ~ %d!"%(maxShiftLeft, maxShiftRight)
            if GUILayout.Button("Cancel", btnstyle):
                del sd.kfamOperationShiftData
                sd.kfamOperationMode = "normal"
        GUILayout.EndHorizontal()
    else:
        try:
            GUILayout.BeginHorizontal()
            sd.kfamOperationMode = "normal"
            # normal keyframes operation
            canRegister = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(getCurSelectedKeys(sd.kfamKeyframeView)) > 0
            if GUILayout.Button("Register" if canRegister else "<color=#808080>Register</color>", btnstyle) and canRegister:
                registerKeyframe()
            canCopy = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(sd.kfamKeyframeView.keyframeBox.selectedKeyframes) > 0
            if GUILayout.Button("Copy" if canCopy else "<color=#808080>Copy</color>", btnstyle) and canCopy:
                copyKeyframe()
            canPaste = not clip.isPlaying and sd.kfamKeyframeEditorMode and len(sd.kfamKeyframeClipboard) > 0 and sd.kfamKeyframeClipboardLength >= 0
            if GUILayout.Button("Paste" if canPaste else "<color=#808080>Paste</color>", btnstyle) and canPaste:
                pasteKeyframe()
            canDelete = canCopy
            if GUILayout.Button("Delete" if canDelete else "<color=#808080>Delete</color>", btnstyle) and canDelete:
                deleteKeyframe()
            if GUILayout.Button("Setting", btnstyle):
                if sd.kfamCurveWndId:
                    closeCurveSubWindow()
                    sd.kfamKeyframeView.selectKeyframes(set())
                if sd.kfamSubClipWndId:
                    toggleClipSubWindow(None)
                    sd.kfamCurSelTargetId = None
                changeGuiScreen(sh, kfam_GUI_modify_clip_setting2)
            if GUILayout.Button("<color=#00ff00>?</color>", btnstyle, GUILayout.Width(20)):
                kfam_util_toggle_help_window(game)
            if GUILayout.Button("Back", btnstyle, GUILayout.Width(50)):
                if sd.kfamKeyframeEditorMode:
                    quitClipEditorWindow()
                else:
                    sd.kfamSetupCtrlDetail = False
            GUILayout.EndHorizontal()
        except:
            # print(">> trap group position error, sorry")
            # traceback.print_exc()
            pass

    # updates
    if sd.kfamLastPlayingStatus != clip.isPlaying:
        sd.kfamLastPlayingStatus = clip.isPlaying
        sd.kfamKeyframeView.setEnable(not sd.kfamLastPlayingStatus)
        sd.kfamCurveEditor.enable = not sd.kfamLastPlayingStatus
        if not sd.kfamLastPlayingStatus:
            onFrameChange(int(clip.curFrame))
    if not clip.isPlaying and sd.kfamKeyframeView.curFrame != clip.curFrame:
        onFrameChange(int(clip.curFrame))

def kfam_GUI_parts_ctrlset_detail(game, ctrlset):
    from vnanime import translateAndSortStatusKeys
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    mClips = game.gdata.kfaManagedClips
    sClips = game.gdata.kfaSortedClips

    # self initialize
    if not hasattr(sd, "kfamCtrlDetailInfo") or sd.kfamCtrlDetailInfo.lastCtrlSet != ctrlset:
        sd.kfamCtrlDetailInfo = GData()
        sd.kfamCtrlDetailInfo.detailScroll = Vector2.zero
        sd.kfamCtrlDetailInfo.addPropertyScroll = Vector2.zero
        sd.kfamCtrlDetailInfo.detailMode = "normal"
        sd.kfamCtrlDetailInfo.propertiesSelectInfo = {}
        for kfctrl in ctrlset.keyframeCtrls:
            # [selected, isMain, ctrl]
            sd.kfamCtrlDetailInfo.propertiesSelectInfo[kfctrl.name] = [False, True, kfctrl]
            if not kfctrl.SingleSet:
                for subkey in kfctrl.keyframeSetKeys:
                    sd.kfamCtrlDetailInfo.propertiesSelectInfo[subkey] = [False, False, kfctrl]
        sd.kfamCtrlDetailInfo.addPropertyCandidate = []
        fullstatus = ctrlset.getCurrentFullStatus()
        for pname in ctrlset.getCurrentUpdateSequence():
            # [selected, name, isMain, mainCtrl]
            if pname not in fullstatus:
                continue
            kfctrl = ctrlset.getKeyframeCtrl(pname)
            if kfctrl == None:
                sd.kfamCtrlDetailInfo.addPropertyCandidate.append([False, pname, True, None])
            else:
                if not kfctrl.SingleSet:
                    for subpname in fullstatus[pname].keys():
                        if subpname not in kfctrl.keyframeSetKeys:
                            sd.kfamCtrlDetailInfo.addPropertyCandidate.append([False, subpname, False, kfctrl])
        sd.kfamCtrlDetailInfo.addPropertyCandidate.sort()
        sd.kfamCtrlDetailInfo.renewDetailRequest = False
    di = sd.kfamCtrlDetailInfo
    di.lastCtrlSet = ctrlset

    def addNewPropertyes():
        for cdInfo in di.addPropertyCandidate:
            # cdInfo: [selected, name, isMain, mainCtrl]
            selected, name, isMain, mainCtrl = cdInfo
            if selected:
                if isMain:
                    ctrlset.addKeyframeCtrl(name, None)
                else:
                    ctrlset.addKeyframeCtrl(mainCtrl.name, name)
        di.renewDetailRequest = True
        sd.kfamReloadRequest = True
    def exportKeyframes(filename):
        import codecs
        if filename and len(filename) > 0:
            try:
                # prepare data
                tgtpnames = selectedProperty()
                if len(tgtpnames) == 0:
                    tgtpnames = di.propertiesSelectInfo.keys()
                # group data
                toExport = {}
                for pname in tgtpnames:
                    _, isMain, ctrl = di.propertiesSelectInfo[pname]
                    if not ctrl.HasKeyframe:
                        continue
                    if isMain:
                        toExport[ctrl] = []
                    else:
                        if ctrl in toExport:
                            toExport[ctrl].append(pname)
                        else:
                            toExport[ctrl] = [pname]
                # check data
                if len(toExport) == 0:
                    scriptHelperGUIMessage("No keyframe found to export.", ("OK",))
                else:
                    # collect data
                    exportData = {}
                    for ctrl in toExport:
                        if ctrl.SingleSet or len(toExport[ctrl]) == 0:
                            exportData[ctrl.name] = ctrl.exportSetting()
                        else:
                            exportData[ctrl.name] = ctrl.exportSetting(toExport[ctrl])
                    # do export
                    jsonStr = dumpToJsonString(exportData, True)
                    fp = codecs.open(filename, "w", 'utf-8')
                    fp.write(jsonStr)
                    fp.close()
                    scriptHelperGUIMessage("Keyframe data exported to <color=#00ff00>%s</color>."%filename, ("OK",))
            except Exception as e:
                traceback.print_exc()
                scriptHelperGUIMessage("Fail to export to <color=#ff0000>%s</color>!\n%s"%(filename, str(e)), ("OK",))
    def importKeyframes(filename):
        import codecs
        if filename and len(filename) > 0:
            sd.kfamReloadRequest = True
            try:
                fp = codecs.open(filename, "r", 'utf-8')
                fileContent = fp.read()
                fp.close()
                ctrlDict = loadFromJsonString(fileContent)
                # update to every ctrl
                skipped = []
                for pname in ctrlDict.keys():
                    ctrl = ctrlset.getKeyframeCtrl(pname)
                    if ctrl != None:
                        ctrl.appendSetting(ctrlDict[pname])
                    else:
                        skipped.append(pname)
                if len(skipped) > 0:
                    scriptHelperGUIMessage("Keyframe imported from <color=#00ff00>%s</color>!\nBut following properties were skipped, import new properties not implemented yet: %s"%(filename, str(skipped)), ("OK",))
                else:
                    scriptHelperGUIMessage("Keyframe imported from <color=#00ff00>%s</color>!\n"%(filename), ("OK",))
            except Exception as e:
                traceback.print_exc()
                scriptHelperGUIMessage("Fail to import from <color=#ff0000>%s</color>!\n%s"%(filename, str(e)), ("OK",))
    def nextUpdateType(curtype):
        i = KeyframeController.VALID_UPDATE_TYPE.index(curtype) + 1
        i %= len(KeyframeController.VALID_UPDATE_TYPE)
        return KeyframeController.VALID_UPDATE_TYPE[i]
    def nextInterpType(curType, interpolatable):
        i = KeyframeController.VALID_INTERPOLATE_TYPE.index(curType) + 1
        if i >= len(KeyframeController.VALID_INTERPOLATE_TYPE):
            if interpolatable:
                i = 0
            else:
                i = KeyframeController.VALID_CONTINUOUS_INTER_COUNT
        return KeyframeController.VALID_INTERPOLATE_TYPE[i]
    def activeText(text, active):
        return ("<color=#00ff00>%s</color>" if active else "<color=#808080>%s</color>")%text
    def selectedProperty():
        return [pname for pname in di.propertiesSelectInfo.keys() if di.propertiesSelectInfo[pname][0]]
    def propertiesBatchTargets(pname):
        if di.propertiesSelectInfo[pname][0]:
            tgtNames = selectedProperty()
        else:
            tgtNames = [pname]
        return tgtNames
    def emptyProperty():
        emptyNames = []
        for pname in di.propertiesSelectInfo:
            _, isMain, kfctrl = di.propertiesSelectInfo[pname]
            if isMain and not kfctrl.HasKeyframe:
                emptyNames.append(pname)
            if not isMain and not kfctrl.getKeyframeSetByKey(pname).HasKeyframe:
                emptyNames.append(pname)
        return emptyNames
    def deleteProperties(tgtPNames):
        for pname in tgtPNames:
            _, isMain, ctrl = di.propertiesSelectInfo[pname]
            if isMain and ctrl in ctrlset.keyframeCtrls:
                ctrlset.keyframeCtrls.remove(ctrl)
            if not isMain and pname in ctrl.keyframeSetKeys:
                ctrl.deleteKeyframeSet(pname)
        di.renewDetailRequest = True
        sd.kfamReloadRequest = True
    def deleteEmptyProperties():
        deleteProperties(emptyProperty())
    def deleteSelectedProperties():
        deleteProperties(selectedProperty())
    def moveProperty(pname, up):
        pnameList = ctrlset.getKeyframeNameList()
        pindex = pnameList.index(pname)
        if pindex == 0 and up:
            return
        if pindex == len(pnameList) - 1 and not up:
            return
        newIndex = (pindex - 1) if up else (pindex + 1)
        ctrlset.keyframeCtrls[newIndex], ctrlset.keyframeCtrls[pindex] = ctrlset.keyframeCtrls[pindex], ctrlset.keyframeCtrls[newIndex]
    def moveProperitiesUP(pname):
        for pn in propertiesBatchTargets(pname):
            moveProperty(pn, True)
    def moveProperitiesDOWN(pname):
        pns = propertiesBatchTargets(pname)
        pns.reverse()
        for pn in pns:
            moveProperty(pn, False)
    def updateCopyDstObjCanidate(dstClip):
        di.ct_dstObjCanidate = [cs for cs in dstClip.keyframes.values() if cs != ctrlset and cs.targetType == ctrlset.targetType]
        di.ct_dstObjIndex = None if len(di.ct_dstObjCanidate) == 0 else 0
    def getTargetName(tid):
        from vntext import get_vntext_manager
        from vnsound import get_vnsound_manager
        if tid in sd.actors:
            ttitle = "<color=#ff0000>" + tid + ":</color>" + sd.actors[tid].text_name
        elif tid in sd.props:
            ttitle = "<color=#00ffff>" + tid + ":</color>" + sd.props[tid].text_name
        elif tid in mClips:
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "Clip"
        elif tid in get_vntext_manager(game).allTextIds:
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "VNText"
        elif tid in get_vnsound_manager(game).soundAlias:
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "VNSound"
        elif tid == "cam":
            ttitle = "<color=#ffff00>" + "Camera" + "</color>"
        elif tid == "sys":
            ttitle = "<color=#ffff00>" + "Scene" + "</color>"
        else:
            raise Exception("Unknown target id <%s> in clip's keyframes"%tid)
        return ttitle
    # normal view: operations and existed properties list
    if di.detailMode == "normal":
        di.detailScroll = GUILayout.BeginScrollView(di.detailScroll, GUI.skin.box)
        # keyframes operation
        GUILayout.BeginHorizontal()
        GUILayout.Label("Keyframes: ")
        if GUILayout.Button("Export"):
            kfam_util_txt_file_dialog(game, exportKeyframes, False)
        if GUILayout.Button("Import"):
            kfam_util_txt_file_dialog(game, importKeyframes, False)
        if GUILayout.Button("Copy To"):
            di.detailMode = "copyto"
            di.ct_dstClip = clip
            updateCopyDstObjCanidate(clip)
            di.ct_tgtProps = [p for p in di.propertiesSelectInfo.keys() if di.propertiesSelectInfo[p][0]]
            di.ct_hasSelect = len(di.ct_tgtProps) > 0
            di.ct_allFrame = not di.ct_hasSelect
            di.ct_moveMode = False
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # properties operation
        GUILayout.BeginHorizontal()
        GUILayout.Label("Properties: ")
        if GUILayout.Button("Add"):
            if len(di.addPropertyCandidate):
                di.detailMode = "addproperty"
            else:
                scriptHelperGUIMessage("No more property valid for current object to add. Check VNActor setting for additional supported properties.", ("OK",))
        if GUILayout.Button("Delete Empty"):
            if len(emptyProperty()):
                scriptHelperGUIMessage("Sure to delete all properties which has no keyframe included?", (("Yes, delete them", deleteEmptyProperties), "Cancel"))
            else:
                scriptHelperGUIMessage("No property without keyframe included.", ("OK",))
        if GUILayout.Button("Delete"):
            if len(selectedProperty()):
                scriptHelperGUIMessage("Sure to delete ALL SELECTED properties of current target?\n<color=#ff0000>WARNING: ALL KEYFRAMES IN THESE PROPERTIES WILL BE DELETED!!</color>", (("Yes, delete them", deleteSelectedProperties), "Cancel"))
            else:
                scriptHelperGUIMessage("No property was selected yet.", ("OK",))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # properties selection shortcuts
        GUILayout.BeginHorizontal()
        GUILayout.Label("Properties Details: ")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # properties selection shortcuts
        GUILayout.BeginHorizontal()
        GUILayout.Label("Batch selection")
        if GUILayout.Button("All"):
            for pname in di.propertiesSelectInfo:
                di.propertiesSelectInfo[pname][0] = True
        if GUILayout.Button("Empty"):
            for pname in di.propertiesSelectInfo:
                _, isMain, kfctrl = di.propertiesSelectInfo[pname]
                di.propertiesSelectInfo[pname][0] = not (kfctrl.HasKeyframe if isMain else kfctrl.getKeyframeSetByKey(pname).HasKeyframe)
        if GUILayout.Button("Invert"):
            for pname in di.propertiesSelectInfo:
                di.propertiesSelectInfo[pname][0] = not di.propertiesSelectInfo[pname][0]
        if GUILayout.Button("None"):
            for pname in di.propertiesSelectInfo:
                di.propertiesSelectInfo[pname][0] = False
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # properties detail list
        _, propDict = translateAndSortStatusKeys(ctrlset.getKeyframeNameList(), False)
        for kfctrl in ctrlset.keyframeCtrls:
            GUILayout.BeginHorizontal()
            di.propertiesSelectInfo[kfctrl.name][0] = GUILayout.Toggle(di.propertiesSelectInfo[kfctrl.name][0], "%s(%d)"%(propDict[kfctrl.name], kfctrl.KeyframeCount))
            GUILayout.FlexibleSpace()
            # update mode
            if GUILayout.Button(kfctrl.updateType):
                newUpdateType = nextUpdateType(kfctrl.updateType)
                for pname in propertiesBatchTargets(kfctrl.name):
                    di.propertiesSelectInfo[pname][2].updateType = newUpdateType
                ctrlset.checkNeedDiff()
            # update before first
            if GUILayout.Button(activeText(u'\u00AB', kfctrl.updateBeforeFirst)):
                newUpdateBeforeFirst = not kfctrl.updateBeforeFirst
                for pname in propertiesBatchTargets(kfctrl.name):
                    di.propertiesSelectInfo[pname][2].updateSetting(newUpdateBeforeFirst, None, None)
            # update after last
            if GUILayout.Button(activeText(u'\u00BB', kfctrl.updateAfterLast)):
                newUpdateAfterLast = not kfctrl.updateAfterLast
                for pname in propertiesBatchTargets(kfctrl.name):
                    di.propertiesSelectInfo[pname][2].updateSetting(None, newUpdateAfterLast, None)
            # interpolate consecutive
            if GUILayout.Button(kfctrl.interpolateType):
                newIntpType = nextInterpType(kfctrl.interpolateType, kfctrl.interpolatable)
                for pname in propertiesBatchTargets(kfctrl.name):
                    di.propertiesSelectInfo[pname][2].updateSetting(None, None, newIntpType)
            # go up
            if GUILayout.Button(u'\u2191'):
                moveProperitiesUP(kfctrl.name)
            # go down
            if GUILayout.Button(u'\u2193'):
                moveProperitiesDOWN(kfctrl.name)
            GUILayout.EndHorizontal()
            # subkeys
            if not kfctrl.SingleSet:
                for subkey, subset in kfctrl.getKeySetPair:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(15)
                    di.propertiesSelectInfo[subkey][0] = GUILayout.Toggle(di.propertiesSelectInfo[subkey][0], "%s(%d)"%(subkey, subset.KeyframeCount))
                    GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
    # add property view: new properties list
    elif di.detailMode == "addproperty":
        # title
        GUILayout.BeginHorizontal()
        GUILayout.Label("Select properties:")
        GUILayout.FlexibleSpace()
        if GUILayout.Button("<color=#00ff00>Append Selected</color>"):
            addNewPropertyes()
        if GUILayout.Button("<color=#ff0000>Cancel</color>"):
            di.detailMode = "normal"
        GUILayout.EndHorizontal()
        # property list
        di.addPropertyScroll = GUILayout.BeginScrollView(di.addPropertyScroll, GUI.skin.box)
        for cdInfo in di.addPropertyCandidate:
            # cdInfo: [selected, name, isMain, mainCtrl]
            _, name, isMain, mainCtrl = cdInfo
            pname = name if isMain else mainCtrl.name + " > " + name
            GUILayout.BeginHorizontal()
            cdInfo[0] = GUILayout.Toggle(cdInfo[0], pname)
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
    # copy to view: some option for copy keyframes
    elif di.detailMode == "copyto":
        # dest clip
        GUILayout.BeginHorizontal()
        GUILayout.Label("Copy to clip:", GUILayout.Width(100))
        if GUILayout.Button("<"):
            idx = sClips.index(di.ct_dstClip)
            idx = (idx - 1) % len(sClips)
            di.ct_dstClip = sClips[idx]
            updateCopyDstObjCanidate(di.ct_dstClip)
        GUILayout.Label("<color=#00ff00>" + di.ct_dstClip.name + "</color>")
        if GUILayout.Button(">"):
            idx = sClips.index(di.ct_dstClip)
            idx = (idx + 1) % len(sClips)
            di.ct_dstClip = sClips[idx]
            updateCopyDstObjCanidate(di.ct_dstClip)
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # dest object
        GUILayout.BeginHorizontal()
        GUILayout.Label("Copy to object:", GUILayout.Width(100))
        if di.ct_dstObjIndex != None:
            if GUILayout.Button("<"):
                di.ct_dstObjIndex = (di.ct_dstObjIndex - 1) % len(di.ct_dstObjCanidate)
            GUILayout.Label(getTargetName(di.ct_dstObjCanidate[di.ct_dstObjIndex].targetId))
            if GUILayout.Button(">"):
                di.ct_dstObjIndex = (di.ct_dstObjIndex + 1) % len(di.ct_dstObjCanidate)
        else:
            GUILayout.Label("No target found. Add one first")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # target keyframes
        GUILayout.BeginHorizontal()
        GUILayout.Label("Copy keyframes:", GUILayout.Width(100))
        if di.ct_hasSelect:
            di.ct_allFrame = GUILayout.Toggle(di.ct_allFrame, "ALL")
            di.ct_allFrame = not GUILayout.Toggle(not di.ct_allFrame, "Selected properties")
        else:
            GUILayout.Label("No property selected, copy ALL")
            di.ct_allFrame = True
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # move mode
        GUILayout.BeginHorizontal()
        GUILayout.Label("Remove keyframes after copy:")
        if GUILayout.Button("<color=#ff0000>Yes</color>" if di.ct_moveMode else "<color=#00ff00>No</color>"):
            di.ct_moveMode = not di.ct_moveMode
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # do button
        GUILayout.BeginHorizontal()
        if di.ct_dstObjIndex != None:
            if GUILayout.Button("<color=#ff0000>Do Keyframes Move</color>" if di.ct_moveMode else "<color=#00ff00>Do Keyframes Copy</color>", GUILayout.Width(200)):
                cfn = ctrlset.copyKeyframesTo([] if di.ct_allFrame else di.ct_tgtProps, di.ct_dstClip.frameLength, di.ct_dstObjCanidate[di.ct_dstObjIndex], di.ct_moveMode)
                di.detailMode = "normal"
                sd.kfamReloadRequest = True
                scriptHelperGUIMessage("%d keyframes %s to destination."%(cfn, "moved" if di.ct_moveMode else "copied"), ("OK",))
        else:
            GUILayout.Button("<color=#808080>Do Keyframes Move</color>" if di.ct_moveMode else "<color=#808080>Do Keyframes Copy</color>", GUILayout.Width(200))
        if GUILayout.Button("Cancel"):
            di.detailMode = "normal"
        GUILayout.EndHorizontal()
    else:
        di.detailMode = "normal"

    # renew
    if di.renewDetailRequest:
        del sd.kfamCtrlDetailInfo
    
def kfam_GUI_modify_clip_setting2(sh):
    from vnanime import kfam_GUI_modify_clip, changeGuiScreen, kfa_resort, kfa_rename
    from vngameengine import parseKeyCode
    # memory shortcuts
    game = sh.game
    sd = game.scenedata
    gd = game.gdata
    clip = sd.kfamOnModifyClip
    mClips = game.gdata.kfaManagedClips

    def updateClipProperties():
        try:
            oldName = clip.name
            name = td.name.strip()
            if len(name) == 0:
                raise Exception("Clip name can not be empty!")
            if name in sd.actors or name in sd.props or name == "sys" or name == "cam":
                raise Exception("Clip name can not dupulicate with actor, prop, camera or system!")
            if name != clip.name and mClips.has_key(name):
                raise Exception("Clip name dupulicated!")
            priority = int(td.priority)
            frameLength = int(td.length)
            if frameLength < clip.lastFrameNo:
                raise Exception("Clip's length must be equal or greater than last key frame No. #%d!"%clip.lastFrameNo)
            frameRate = int(td.FPS)
            speed = float(td.speed)
            loop = int(td.loop)
            if name != clip.name:
                kfa_rename(game, oldName, name)
            if priority != clip.priority:
                kfa_resort(game, name, priority)
                sd.kfamSelectedClipIndex = -1
            clip.frameLength = frameLength
            clip.frameRate = frameRate
            clip.speed = speed
            clip.loop = loop
            clip.autorun = td.autorun
            clip.autoRewindOnStop = td.autorewind
            clip.gcOnStart = td.gcOnStart
            clip.gcOnStop = td.gcOnStop
            clip.gcOnLoop = td.gcOnLoop
            # update keyframe view
            sd.kfamKeyframeView.setFixFrameLengthMode(True, clip.frameLength)
            # revert to update
            revertClipProperties()
        except Exception as e:
            scriptHelperGUIMessage("Apply setting failed: " + str(e), ("OK",))
    def revertClipProperties():
        td = sd.kfamSettingTempData
        td.name = clip.name
        td.priority = "%d"%clip.priority
        td.length = "%d"%clip.frameLength
        td.FPS = "%d"%clip.frameRate
        td.speed = "%.2f"%clip.speed
        td.loop = "%d"%clip.loop
        td.autorun = clip.autorun
        td.autorewind = clip.autoRewindOnStop
        td.gcOnStart = clip.gcOnStart
        td.gcOnStop = clip.gcOnStop
        td.gcOnLoop = clip.gcOnLoop
    def backToKeyframeEditor():
        # GC setting
        if td.gcSupendValid:
            if not gd.kfaSetting.suspendGC:
                VNEnableGarbageCollect(True)
            if td.gcLimit == "inf":
                gd.kfaSetting.maxGCSize = -1
            else:
                try:
                    maxGC = int(td.gcLimit)
                    if maxGC >= 2000:
                        gd.kfaSetting.maxGCSize = maxGC
                except:
                    pass
        else:
            gd.kfaSetting.suspendGC = False
        # reload request
        if td.reloadRequest:
            sd.kfamReloadRequest = True
        # clear
        del sd.kfamSettingTempData
        changeGuiScreen(sh, kfam_GUI_modify_clip)
    def checkClipPropertiesChange():
        return td.name != clip.name or \
            td.priority != "%d"%clip.priority or \
            td.length != "%d"%clip.frameLength or \
            td.FPS != "%d"%clip.frameRate or \
            td.speed != "%.2f"%clip.speed or \
            td.loop != "%d"%clip.loop or \
            td.autorun != clip.autorun or \
            td.autorewind != clip.autoRewindOnStop or \
            td.gcOnStart != clip.gcOnStart or \
            td.gcOnStop != clip.gcOnStop or \
            td.gcOnLoop != clip.gcOnLoop
    def exportClip(filename):
        import codecs
        if filename and len(filename) > 0:
            try:
                jsonStr = dumpToJsonString(clip.exportSetting(), True)
                fp = codecs.open(filename, "w", 'utf-8')
                fp.write(jsonStr)
                fp.close()
                scriptHelperGUIMessage("Clip data exported to <color=#00ff00>%s</color>."%filename, ("OK",))
            except Exception as e:
                traceback.print_exc()
                scriptHelperGUIMessage("Fail to export to <color=#ff0000>%s</color>!\n%s"%(filename, str(e)), ("OK",))
    def importClip(filename):
        import codecs
        if filename and len(filename) > 0:
            try:
                oldname = clip.name
                fp = codecs.open(filename, "r", 'utf-8')
                fileContent = fp.read()
                fp.close()
                settings = loadFromJsonString(fileContent)
                clip.importSetting(settings)
                revertClipProperties()
                td.reloadRequest = True
                if clip.name != oldname:
                    renameNote = "\n\nNote: Clip name change from <color=#00ff00>'%s'</color> to <color=#00ff00>'%s'</color> was holden back. Use [Update Porperties] button to do the name change or [Revert] button to cancel rename in setting."%(oldname, clip.name)
                    td.name = clip.name
                    clip.name = oldname
                else:
                    renameNote = ""
                kfa_resort(game, clip.name, clip.priority)
                scriptHelperGUIMessage("Clip data imported from <color=#00ff00>%s</color>.%s"%(filename, renameNote), ("OK",))
            except Exception as e:
                traceback.print_exc()
                scriptHelperGUIMessage("Fail to import from <color=#ff0000>%s</color>!\n%s"%(filename, str(e)), ("OK",))
    fullw = game.windowRect.width - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    blableStyle = GUIStyle("Label")
    blableStyle.fontSize = 14
    obwidth = 100
    cbwidth = 100

    if not hasattr(sd, "kfamSettingTempData"):
        sd.kfamSettingTempData = GData()
        td = sd.kfamSettingTempData
        revertClipProperties()
        td.scrollPos = Vector2.zero
        td.gcSupendValid = VNCheckGarbageCollect() != None
        td.gcLimit = "%d"%gd.kfaSetting.maxGCSize if gd.kfaSetting.maxGCSize > 0 else "inf"
        td.wndWidth = "%d"%game.windowRect.width
        td.wndHeight = "%d"%game.windowRect.height
        td.tgtPanelWidth = "%d"%gd.kfaSetting.targetPanelWidth
        gd.kfaSetting.propertyNameWidth = sd.kfamKeyframeView.frameNameWidth
        td.propertyNameWidth = "%d"%gd.kfaSetting.propertyNameWidth
        td.reloadRequest = False
    else:
        td = sd.kfamSettingTempData

    # begin of body
    td.scrollPos = GUILayout.BeginScrollView(td.scrollPos, GUILayout.Width(fullw))
    
    # clip properties
    GUILayout.Label("<color=#00ff00>Clip properties:</color>", blableStyle)
    GUILayout.BeginHorizontal()
    GUILayout.Label("Name:", GUILayout.Width(50))
    td.name = GUILayout.TextField(td.name, GUILayout.Width(100))
    GUILayout.Space(5)
    GUILayout.Label("Priority:", GUILayout.Width(55))
    td.priority = GUILayout.TextField(td.priority, GUILayout.Width(40))
    GUILayout.Space(5)
    td.autorun = GUILayout.Toggle(td.autorun, "Auto run on load")
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("Length:", GUILayout.Width(50))
    td.length = GUILayout.TextField(td.length, GUILayout.Width(60))
    GUILayout.Space(5)
    GUILayout.Label("FPS:", GUILayout.Width(45))
    td.FPS = GUILayout.TextField(td.FPS, GUILayout.Width(35))
    GUILayout.Space(5)
    GUILayout.Label("Speed:", GUILayout.Width(50))
    td.speed = GUILayout.TextField(td.speed, GUILayout.Width(45))
    GUILayout.Space(5)
    GUILayout.Label("Loop:", GUILayout.Width(50))
    td.loop = GUILayout.TextField(td.loop, GUILayout.Width(40))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("Auto rewind:")
    td.autorewind = GUILayout.Toggle(td.autorewind, "Auto rewind when stop clip (Check readme for more details)")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("Do garbage collect:")
    td.gcOnStart = GUILayout.Toggle(td.gcOnStart, "On start")
    td.gcOnStop = GUILayout.Toggle(td.gcOnStop, "On stop/pause")
    td.gcOnLoop = GUILayout.Toggle(td.gcOnLoop, "On loop")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    settingModified = checkClipPropertiesChange()
    if GUILayout.Button("Update Porperties" if settingModified else "<color=#808080>Update Porperties</color>") and settingModified:
        updateClipProperties()
    if GUILayout.Button("Revert" if settingModified else "<color=#808080>Revert</color>") and settingModified:
        revertClipProperties()
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # clip operation
    GUILayout.Label("<color=#00ff00>Clip operation:</color>", blableStyle)
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Optimize", GUILayout.Width(obwidth)):
        td.reloadRequest = True
        scriptHelperGUIMessage("Under construction... m(_ _)m", ("OK",))
        #scriptHelperGUIMessage("Optimize keyframes for all contents. The optimization will delete unchanged status to make the output shorter and clear, and better for performence.\n'Manual': Choice to delete status stay unchanged in all keyframes.\n'Deep': Delete status unchanged between keyframes.\n'Deep -1st': Run deep clear except for the first keyframe.\n<color=#ff0000>Save you work before do this action if you are not sure!</color>",
        #                        (("Manual", startManualOptimize, sh), ("Deep", clip.optimizeKeyFrames, True), ("Deep -1st", clip.optimizeKeyFrames, False), "Cancel"))
    GUILayout.Label("Remove unused properties for better performance")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Export clip
    if GUILayout.Button("Export", GUILayout.Width(obwidth)):
        kfam_util_txt_file_dialog(game, exportClip, False)
    GUILayout.Label("Save clip data to text file")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # import clip
    if GUILayout.Button("Import", GUILayout.Width(obwidth)):
        kfam_util_txt_file_dialog(game, importClip, True)
    GUILayout.Label("Load clip data from text file")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
 
    # global setting
    GUILayout.Label("<color=#00ff00>Global setting:</color>", blableStyle)
    GUILayout.BeginHorizontal() # Hide ununsed properties
    hideOpt = GUILayout.Toggle(gd.kfaSetting.hideUnusedProperties, "Auto hide unused properties")
    if gd.kfaSetting.hideUnusedProperties != hideOpt:
        gd.kfaSetting.hideUnusedProperties = hideOpt
        sd.kfamHidePropertiesRequest = True
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # VideoExport mode
    veMode = GUILayout.Toggle(gd.kfaSetting.videoExportMode, "VideoExport Mode (Note, mouse click though this UI)")
    if gd.kfaSetting.videoExportMode != veMode:
        gd.kfaSetting.videoExportMode = veMode
        game.skin.eatMouseInput = not veMode
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # GC suspend
    if td.gcSupendValid:
        susGC = GUILayout.Toggle(gd.kfaSetting.suspendGC, "Suspend GC when any clip playing")
        if gd.kfaSetting.suspendGC != susGC:
            gd.kfaSetting.suspendGC = susGC
        if gd.kfaSetting.suspendGC:
            oldColor = GUI.color
            GUILayout.Label(", max GC limit")
            GUI.color = oldColor if td.gcLimit.isdecimal() or td.gcLimit == "inf" else Color.red
            td.gcLimit = GUILayout.TextField(td.gcLimit, GUILayout.Width(50))
            GUI.color = oldColor
            GUILayout.Label("MB")
    else:
        gd.kfaSetting.suspendGC = False
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Window size
    GUILayout.Label("Editor window size:")
    GUILayout.Space(5)
    td.wndWidth = GUILayout.TextField(td.wndWidth, GUILayout.Width(50))
    GUILayout.Label("x")
    td.wndHeight = GUILayout.TextField(td.wndHeight, GUILayout.Width(50))
    GUILayout.Space(5)
    if GUILayout.Button("Resize"):
        try:
            nWidth = int(td.wndWidth)
            if nWidth < 500:
                raise Exception("Minimum window width = 500")
            nHeight = int(td.wndHeight)
            if nHeight < 265:
                raise Exception("Minimum window height = 265")
            gd.kfaSetting.clipEditorWindowRect = Rect(game.windowRect.x, game.windowRect.y, nWidth, nHeight)
            game.windowRect = gd.kfaSetting.clipEditorWindowRect
            sd.kfamResizeRequest = True
        except Exception as e:
            scriptHelperGUIMessage("Invalid window size input: %s"%str(e), ("OK",))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # left panel width
    GUILayout.Label("Target panel width:")
    GUILayout.Space(5)
    td.tgtPanelWidth = GUILayout.TextField(td.tgtPanelWidth, GUILayout.Width(50))
    GUILayout.Space(5)
    if GUILayout.Button("Resize"):
        try:
            nWidth = int(td.tgtPanelWidth)
            if nWidth < 150:
                raise Exception("Minimum target panel width = 150")
            if nWidth > game.windowRect.width / 2:
                raise Exception("Maximum target panel width is half of window width = %d"%(game.windowRect.width / 2))
            gd.kfaSetting.targetPanelWidth = nWidth
            sd.kfamResizeRequest = True
        except Exception as e:
            scriptHelperGUIMessage("Invalid target panel width input: %s"%str(e), ("OK",))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # property name width
    GUILayout.Label("Property name width:")
    GUILayout.Space(5)
    td.propertyNameWidth = GUILayout.TextField(td.propertyNameWidth, GUILayout.Width(50))
    GUILayout.Space(5)
    if GUILayout.Button("Resize"):
        try:
            nWidth = int(td.propertyNameWidth)
            if nWidth < 50:
                raise Exception("Minimum property name width = 50")
            if nWidth > sd.kfamKeyframeView.viewRect.width / 2:
                raise Exception("Maximum property name width is half of keyframe view width = %d"%(game.windowRect.width / 2))
            gd.kfaSetting.propertyNameWidth = nWidth
            sd.kfamResizeRequest = True
        except Exception as e:
            scriptHelperGUIMessage("Invalid property name width input: %s"%str(e), ("OK",))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # shortcuts
    def releaseShortcut(param):
        gd.kfaSetting.suspendShortcut = False
    def releaseShortcutLater():
        gd.kfaSetting.shortcutInput = None
        game.updFunc = releaseShortcutLater1
    def releaseShortcutLater1(param):
        game.updFunc = releaseShortcut
    def dispNameForShortcut(shortcut):
        if gd.kfaSetting.shortcutInput == shortcut:
            return "<i><color=#00ffff>-Press A Key-</color></i>"
        elif gd.kfaSetting.shortcuts[shortcut][1] and len(gd.kfaSetting.shortcuts[shortcut][1][0]):
            return gd.kfaSetting.shortcuts[shortcut][1][0]
        else:
            return "<i><color=#ffff00>-Not Set-</color></i>"
    def setupShortcut(shortcut):
        if gd.kfaSetting.shortcutInput == shortcut:
            gd.kfaSetting.shortcuts[gd.kfaSetting.shortcutInput][1] = None
            releaseShortcutLater()
        else:
            gd.kfaSetting.shortcutInput = shortcut
            gd.kfaSetting.suspendShortcut = True
    shortcutTxtWidth = 80
    shortcutBtnWidth = 120
    if gd.kfaSetting.shortcutInput:
        evt = Event.current
        if evt.isKey:
            if evt.keyCode == KeyCode.Escape:
                releaseShortcutLater()
            elif evt.keyCode != KeyCode.LeftControl and evt.keyCode != KeyCode.RightControl and \
               evt.keyCode != KeyCode.LeftAlt and evt.keyCode != KeyCode.RightAlt:
                keyname = str(evt.keyCode)
                if evt.shift: keyname = "Shift+" + keyname
                if evt.alt: keyname = "Alt+" + keyname
                if evt.control: keyname = "Ctrl+" + keyname
                print("detected <%s> press"%keyname)
                gd.kfaSetting.shortcuts[gd.kfaSetting.shortcutInput][1] = parseKeyCode(keyname)
                releaseShortcutLater()
    GUILayout.Label("<color=#00ff00>Shortcuts:</color>", blableStyle)
    GUILayout.BeginHorizontal() # Start/Stop
    GUILayout.Label("Start/Stop", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Start/Stop"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Start/Stop")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Prev/Next Frame
    GUILayout.Label("Prev Frame", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Prev Frame"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Prev Frame")
    GUILayout.FlexibleSpace()
    GUILayout.Label("Next Frame", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Next Frame"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Next Frame")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Prev/Next Second
    GUILayout.Label("Prev Second", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Prev Second"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Prev Second")
    GUILayout.FlexibleSpace()
    GUILayout.Label("Next Second", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Next Second"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Next Second")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Start/End
    GUILayout.Label("Goto Start", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Goto Start"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Goto Start")
    GUILayout.FlexibleSpace()
    GUILayout.Label("Goto End", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Goto End"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Goto End")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Register/Delete
    GUILayout.Label("Register", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Register"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Register")
    GUILayout.FlexibleSpace()
    GUILayout.Label("Delete", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Delete"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Delete")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # Copy/Paste
    GUILayout.Label("Copy", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Copy"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Copy")
    GUILayout.FlexibleSpace()
    GUILayout.Label("Paste", GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Paste"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Paste")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # disable studio shortcuts
    GUILayout.Label("Suspend vanilla studio shortcut keys:", GUILayout.Width(250))
    if GUILayout.Button("<color=#ffa500>ON</color>" if gd.kfaSetting.disableStudioKeys else "OFF", GUILayout.Width(50)):
        from vnframe import VNEnableStudioShortcuts
        gd.kfaSetting.disableStudioKeys = not gd.kfaSetting.disableStudioKeys
        VNEnableStudioShortcuts(not gd.kfaSetting.disableStudioKeys)
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # disable camera shortcuts
    GUILayout.Label("Suspend vanilla camera shortcut keys:", GUILayout.Width(250))
    if GUILayout.Button("<color=#ffa500>ON</color>" if gd.kfaSetting.disableCameraKeys else "OFF", GUILayout.Width(50)):
        from vnframe import VNEnableCameraShortcuts
        gd.kfaSetting.disableCameraKeys = not gd.kfaSetting.disableCameraKeys
        VNEnableCameraShortcuts(not gd.kfaSetting.disableCameraKeys)
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # end for body
    GUILayout.EndScrollView()

    # Control button
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    if GUILayout.Button("<color=#00ff00>?</color>", btnstyle, GUILayout.Width(20)):
        kfam_util_toggle_help_window(game)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(50)):
        if settingModified:
            scriptHelperGUIMessage("Clip setting modified but not applied! Discard all change and back to keyframe view?", (("OK, discard them", backToKeyframeEditor), "Cancel"))
        else:
            backToKeyframeEditor()
    GUILayout.EndHorizontal()

def kfam_util_toggle_help_window(game, forceClose = False):
    sd = game.scenedata
    setting = game.gdata.kfaSetting
    helpWindowWidth = setting.clipEditorWindowRect.width
    helpWindowHeight = 500

    if sd.kfamHelpWndId != None and game.get_extra_window(sd.kfamHelpWndId) != None or forceClose:
        # help is shown, close it
        game.close_extra_window(sd.kfamHelpWndId)
        sd.kfamHelpWndId = None
    else:
        from vnframe import VNLoadHelpTexts
        sd.kfamHelpContents = VNLoadHelpTexts("vnanime2_help_")
        if sd.kfamHelpContents == None or len(sd.kfamHelpContents) == 0:
            print("Cannot open help window because no contents loaded!")
            return
        else:
            if hasattr(setting, "defaultHelpId") and setting.defaultHelpId in sd.kfamHelpContents:
                sd.kfamHelpCurrentId = setting.defaultHelpId
            else:
                setting.defaultHelpId = sd.kfamHelpCurrentId = sd.kfamHelpContents.keys()[0]
        # open help
        if game.windowRect.y - helpWindowHeight >= 0:
            cWndRect = Rect(game.windowRect.x, game.windowRect.y - helpWindowHeight, helpWindowWidth, helpWindowHeight)
        else:
            cWndRect = Rect(game.windowRect.x, game.windowRect.bottom, helpWindowWidth, helpWindowHeight)

        def skin_setup(g):
            g.windowName = "Help for VNAnime Clip V2"
            g.windowRect = cWndRect
            g.windowStyle = g.windowStyleDefault

        from skin_customwindow import SkinCustomWindow
        skin = SkinCustomWindow()
        skin.funcSetup = skin_setup
        skin.funcWindowGUI = kfam_GUI_help_subwindow

        sd.kfamHelpWndId = game.new_extra_window_skin(skin)

def kfam_GUI_help_subwindow(baseController, windowNo):
    from vnframe import _sh
    game = _sh.game
    sd = game.scenedata

    def printContents(id, section):
        contents = sd.kfamHelpContents[id]
        if section in contents:
            sectionContents = contents[section]
            #for sline in sectionContents:
            #    GUILayout.Label(sline)
            GUILayout.Label("\n".join(sectionContents))

    # by ui
    sd.kfamHelpScroll = GUILayout.BeginScrollView(sd.kfamHelpScroll)
    if sd.kfaGuiFunction == kfam_GUI_modify_clip_setting2:
        printContents(sd.kfamHelpCurrentId, "SettingView")
    elif sd.kfamCurSelTargetId and sd.kfamSetupCtrlDetail:
        printContents(sd.kfamHelpCurrentId, "DetailView")
    else:
        printContents(sd.kfamHelpCurrentId, "KeyframeView")
    GUILayout.EndScrollView()

    # choose language
    GUILayout.BeginHorizontal()
    for id in sd.kfamHelpContents.keys():
        if GUILayout.Button("<color=#00ff00>%s</color>"%id if id == sd.kfamHelpCurrentId else id):
            game.gdata.kfaSetting.defaultHelpId = sd.kfamHelpCurrentId = id
    GUILayout.FlexibleSpace()
    # close btn
    if GUILayout.Button("Close"):
        game.close_extra_window(windowNo)
    GUILayout.EndHorizontal()

def kfam_util_get_subclip_setting(tgtClip):
    from vngameengine import vnge_game
    sd = vnge_game.scenedata
    if sd.kfamSubClipWndId == None or sd.kfamSubClipStatus.errorMessage != None or sd.kfamSubClipStatus.targetClip != tgtClip:
        return None
    else:
        return sd.kfamSubClipStatus.subClipStatus

def kfam_util_txt_file_dialog(game, callback, isImport, initFolder=None, useApi=True):
    jsonExt = ".json"

    def callbackWrapper_api(files):
        if files and len(files) > 0:
            callback(files[0])
    def callbackWrapper_vn(file):
        if not isImport and not file.endswith(jsonExt):
            file += jsonExt
        callback(file)

    if useApi:
        from extplugins import ILLAPI
        illApi = ILLAPI()
    else:
        illApi = None
    title = "%s json text file"%("Import" if isImport else "Export")
    if illApi and illApi.isDetected:
        extFilter = "JSON Files(*%s)|*%s|All files (*.*)|*.*"%(jsonExt, jsonExt)
        illApi.Utilities_OpenFileDialog(callbackWrapper_api, isImport, initFolder, title, extFilter, jsonExt)
    else:
        extFilter = jsonExt
        from vnframe import VNFileBrowser
        VNFileBrowser(game, callbackWrapper_vn, isImport, initFolder, title, extFilter, jsonExt)

def kfam_editor_update_v2(game, evid, u_param):
    setting = game.gdata.kfaSetting
    setting.frameCount += 1
    # shortcuts
    if not setting.suspendShortcut:
        for commands, param in setting.shortcuts.items():
            if param[1] == None:
                continue
            (keyname, icode, ictrl, ialt, ishift) = param[1]
            if param[2] and Input.GetKey(icode):
                # unity sucks for checking meta keys
                ctrl, alt, shift = unity_util.metakey_state()
                if ctrl == ictrl and alt == ialt and shift == ishift:
                    # do the work
                    #print("* shortcut [%s] detected for [%s]"%(keyname, commands))
                    param[0]()
                    break
            elif not param[2] and Input.GetKeyDown(icode):
                # unity sucks for checking meta keys
                ctrl, alt, shift = unity_util.metakey_state()
                if ctrl == ictrl and alt == ialt and shift == ishift:
                    # do the work
                    #print("* shortcut [%s] detected for [%s]"%(keyname, commands))
                    param[0]()
                    break

def clipExportToString2(game):
    # collect data
    exportSetting = []
    for clip in game.gdata.kfaSortedClips:
        if clip.version != 2:
            continue    # only for version2
        exportSetting.append(clip.exportSetting())
    # to string
    return dumpToJsonString(exportSetting)

def clipImportFromString2(game, settingString):
    from vnanime import kfa_register
    # to data
    settings = loadFromJsonString(settingString)
    if not isinstance(settings, list):
        print("Unknown setting format...")
        return
    # preload
    game.gdata.kfaPreloadClipNames = []
    for setting in settings:
        clipname = setting["name"]
        game.gdata.kfaPreloadClipNames.append(clipname)
    # import
    for setting in settings:
        clip = KeyFrameClip2.createFromSetting(game, setting)
        if clip:
            kfa_register(game, clip)
    # solve and remove preload
    for clip in game.gdata.kfaSortedClips:
        if clip.version != 2:
            continue    # only for version2
        clip.solvePreload()
    del game.gdata.kfaPreloadClipNames

def loadScriptFromFile2(filename):
    import codecs
    try:
        fp = codecs.open(filename, "r", 'utf-8')
        fileContent = fp.read()
        fp.close()
        setting = loadFromJsonString(fileContent)
        return setting
    except Exception as e:
        traceback.print_exc()
        raise Exception("Fail to import from <color=#ff0000>%s</color>! %s"%(filename, str(e)))


